importzugferd/import.php
data c412d6c5a3 Einkaufspreise aller Lieferanten bei zugeordneten Produkten anzeigen
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 07:05:51 +01:00

2850 lines
134 KiB
PHP
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/* Copyright (C) 2026 ZUGFeRD Import Module
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*/
/**
* \file import.php
* \ingroup importzugferd
* \brief Manual ZUGFeRD import page with persistent workflow
*/
// Load Dolibarr environment
$res = 0;
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
}
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
$tmp2 = realpath(__FILE__);
$i = strlen($tmp) - 1;
$j = strlen($tmp2) - 1;
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
$i--;
$j--;
}
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
}
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
}
if (!$res && file_exists("../main.inc.php")) {
$res = @include "../main.inc.php";
}
if (!$res && file_exists("../../main.inc.php")) {
$res = @include "../../main.inc.php";
}
if (!$res && file_exists("../../../main.inc.php")) {
$res = @include "../../../main.inc.php";
}
if (!$res) {
die("Include of main fails");
}
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
dol_include_once('/importzugferd/class/zugferdparser.class.php');
dol_include_once('/importzugferd/class/zugferdimport.class.php');
dol_include_once('/importzugferd/class/importline.class.php');
dol_include_once('/importzugferd/class/productmapping.class.php');
dol_include_once('/importzugferd/class/actions_importzugferd.class.php');
dol_include_once('/importzugferd/class/datanorm.class.php');
dol_include_once('/importzugferd/class/datanormparser.class.php');
dol_include_once('/importzugferd/class/importnotification.class.php');
dol_include_once('/importzugferd/lib/importzugferd.lib.php');
// Load translation files
$langs->loadLangs(array("importzugferd@importzugferd", "bills", "products", "companies"));
// Security check
if (!$user->hasRight('importzugferd', 'import', 'write')) {
accessforbidden();
}
// Get parameters
$action = GETPOST('action', 'aZ09');
$confirm = GETPOST('confirm', 'alpha');
$id = GETPOST('id', 'int'); // Import ID for editing existing imports
$supplier_id = GETPOST('supplier_id', 'int');
$line_id = GETPOST('line_id', 'int');
$product_id = GETPOST('product_id', 'int');
$template_product_id = GETPOST('template_product_id', 'int');
// Zeilenspezifische Produkt-IDs (wegen eindeutiger select2-IDs pro Zeile)
if (empty($product_id) && $line_id > 0) {
$product_id = GETPOST('product_id_'.$line_id, 'int');
}
if (empty($template_product_id) && $line_id > 0) {
$template_product_id = GETPOST('template_product_id_'.$line_id, 'int');
}
// Initialize objects
$form = new Form($db);
$formfile = new FormFile($db);
$actions = new ActionsImportZugferd($db);
$import = new ZugferdImport($db);
$importLine = new ImportLine($db);
$notification = new ImportNotification($db);
$error = 0;
$message = '';
/*
* Helper-Funktionen (DRY)
*/
/**
* Extrafields fuer Lieferantenpreis aus Datanorm-Daten zusammenstellen
*
* @param Datanorm $datanorm Datanorm-Objekt
* @param ImportLine|null $lineObj Import-Zeile (optional, fuer ZUGFeRD-Daten)
* @return array Extrafields-Array
*/
function datanormBuildSupplierPriceExtrafields($datanorm, $lineObj = null)
{
$extrafields = array();
// Kupferzuschlag
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$extrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge;
} elseif ($lineObj && !empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
$extrafields['options_kupferzuschlag'] = $lineObj->copper_surcharge;
}
// Preiseinheit
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
$extrafields['options_preiseinheit'] = $datanorm->price_unit;
} elseif ($lineObj && !empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
$extrafields['options_preiseinheit'] = $lineObj->basis_quantity;
}
// Warengruppe
if (!empty($datanorm->product_group)) {
$extrafields['options_warengruppe'] = $datanorm->product_group;
}
return $extrafields;
}
/**
* Lieferantenpreis aus Datanorm hinzufuegen
*
* @param DoliDB $db Datenbank
* @param int $productId Produkt-ID
* @param Datanorm $datanorm Datanorm-Objekt
* @param Societe $supplier Lieferant-Objekt
* @param User $user Benutzer
* @param float $purchasePrice Einkaufspreis
* @param float $taxPercent MwSt-Satz
* @param array $extrafields Extrafields
* @return int >0 bei Erfolg, <0 bei Fehler
*/
function datanormAddSupplierPrice($db, $productId, $datanorm, $supplier, $user, $purchasePrice, $taxPercent = 19, $extrafields = array())
{
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
$prodfourn = new ProductFournisseur($db);
$prodfourn->id = $productId;
$supplierEan = !empty($datanorm->ean) ? $datanorm->ean : '';
$supplierEanType = !empty($datanorm->ean) ? 2 : 0;
$description = trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : ''));
return $prodfourn->update_buyprice(
1, $purchasePrice, $user, 'HT', $supplier, 0,
$datanorm->article_number, $taxPercent,
0, 0, 0, 0, 0, 0, array(), '',
0, 'HT', 1, '',
$description, $supplierEan, $supplierEanType,
$extrafields
);
}
/**
* Extrafields in product_fournisseur_price_extrafields einfuegen
*
* @param DoliDB $db Datenbank
* @param int $priceId ID des Lieferantenpreises
* @param array $extrafields Extrafields-Array
*/
function datanormInsertPriceExtrafields($db, $priceId, $extrafields)
{
if (empty($priceId) || empty($extrafields)) {
return;
}
$kupferzuschlag = !empty($extrafields['options_kupferzuschlag']) ? (float)$extrafields['options_kupferzuschlag'] : 'NULL';
$preiseinheit = !empty($extrafields['options_preiseinheit']) ? (int)$extrafields['options_preiseinheit'] : 1;
$warengruppe = !empty($extrafields['options_warengruppe']) ? "'".$db->escape($extrafields['options_warengruppe'])."'" : 'NULL';
// Pruefen ob bereits vorhanden
$sqlCheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields WHERE fk_object = ".(int)$priceId;
$resCheck = $db->query($sqlCheck);
if ($resCheck && $db->num_rows($resCheck) > 0) {
return; // Bereits vorhanden
}
$sql = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sql .= " (fk_object, kupferzuschlag, preiseinheit, warengruppe) VALUES (";
$sql .= (int)$priceId.", ";
$sql .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
$sql .= $preiseinheit.", ";
$sql .= $warengruppe.")";
if (!$db->query($sql)) {
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
}
}
/*
* Actions
*/
// AJAX: Get raw Datanorm lines for debugging
if ($action == 'get_raw_lines' && GETPOST('article_number', 'alphanohtml')) {
header('Content-Type: application/json');
$article_number = GETPOST('article_number', 'alphanohtml');
$ajax_fk_soc = GETPOSTINT('fk_soc');
$result = array(
'datanorm_line' => '',
'datpreis_line' => '',
'article_number' => $article_number
);
// Get the upload directory for this supplier
$upload_dir = $conf->importzugferd->dir_output.'/datanorm/'.$ajax_fk_soc;
if (is_dir($upload_dir)) {
$allFiles = glob($upload_dir . '/*');
// Search in DATANORM files
foreach ($allFiles as $file) {
$basename = strtoupper(basename($file));
if (preg_match('/^DATANORM\.\d{3}$/', $basename)) {
$handle = fopen($file, 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
// A-Satz starts with A; and contains the article number
if (preg_match('/^A;/', $line)) {
$parts = explode(';', $line);
if (isset($parts[2]) && trim($parts[2]) == $article_number) {
$result['datanorm_line'] = trim($line);
break;
}
}
}
fclose($handle);
}
if (!empty($result['datanorm_line'])) break;
}
}
// Search in DATPREIS files
foreach ($allFiles as $file) {
$basename = strtoupper(basename($file));
if (preg_match('/^DATPREIS\.\d{3}$/', $basename)) {
$handle = fopen($file, 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
// P-Satz contains article numbers at various positions
if (preg_match('/^P;/', $line) && strpos($line, $article_number) !== false) {
$result['datpreis_line'] = trim($line);
break;
}
}
fclose($handle);
}
if (!empty($result['datpreis_line'])) break;
}
}
$result['upload_dir'] = $upload_dir;
} else {
$result['error'] = 'Upload directory not found: ' . $upload_dir;
}
echo json_encode($result);
exit;
}
// Upload and parse PDF - creates import record immediately
if ($action == 'upload') {
if (!empty($_FILES['zugferd_file']['tmp_name'])) {
$upload_dir = $conf->importzugferd->dir_output.'/temp';
if (!is_dir($upload_dir)) {
dol_mkdir($upload_dir);
}
$filename = dol_sanitizeFileName($_FILES['zugferd_file']['name']);
$destfile = $upload_dir.'/'.$filename;
if (move_uploaded_file($_FILES['zugferd_file']['tmp_name'], $destfile)) {
$force_reimport = GETPOST('force_reimport', 'int');
// Check for duplicate
$file_hash = hash_file('sha256', $destfile);
$isDuplicate = $import->isDuplicate($file_hash);
if ($isDuplicate && !$force_reimport) {
$error++;
$message = $langs->trans('ErrorDuplicateInvoice');
@unlink($destfile);
} else {
// If force reimport, delete the old record first
if ($isDuplicate && $force_reimport) {
$oldImport = new ZugferdImport($db);
$oldImport->fetch(0, null, $file_hash);
if ($oldImport->id > 0) {
$db->begin();
// Alten Import-Datensatz komplett loeschen (Transaktion)
$oldLines = new ImportLine($db);
$oldLines->deleteAllByImport($oldImport->id);
$old_dir = $conf->importzugferd->dir_output.'/imports/'.$oldImport->id;
if (is_dir($old_dir)) {
dol_delete_dir_recursive($old_dir);
}
$oldImport->delete($user);
$db->commit();
}
}
// Parse the file
$parser = new ZugferdParser($db);
$res = $parser->extractFromPdf($destfile);
if ($res > 0) {
$res = $parser->parse();
if ($res > 0) {
$parsed_data = $parser->getInvoiceData();
// Create import record immediately
$import->invoice_number = $parsed_data['invoice_number'];
$import->invoice_date = $parsed_data['invoice_date'];
$import->seller_name = $parsed_data['seller']['name'];
$import->seller_vat = $parsed_data['seller']['vat_id'];
$import->buyer_reference = $parsed_data['buyer']['reference'] ?: $parsed_data['buyer']['id'];
$import->total_ht = $parsed_data['totals']['net'];
$import->total_ttc = $parsed_data['totals']['gross'];
$import->currency = $parsed_data['totals']['currency'];
$import->xml_content = $parser->getXmlContent();
$import->pdf_filename = $filename;
$import->file_hash = $file_hash;
// Find supplier
$supplier_id = $actions->findSupplier($parsed_data);
$import->fk_soc = $supplier_id;
// Process line items to find products
$processed_lines = $actions->processLineItems($parsed_data['lines'], $supplier_id);
// Check if all lines have products
$all_have_products = true;
$has_any_product = false;
$total_lines = count($processed_lines);
foreach ($processed_lines as $line) {
if ($line['fk_product'] <= 0) {
$all_have_products = false;
} else {
$has_any_product = true;
}
}
// Set status based on product matching
// STATUS_IMPORTED only if: supplier found, has lines, ALL lines have products
if ($all_have_products && $supplier_id > 0 && $total_lines > 0 && $has_any_product) {
$import->status = ZugferdImport::STATUS_IMPORTED;
} else {
$import->status = ZugferdImport::STATUS_PENDING;
}
$import->date_creation = dol_now();
$result = $import->create($user);
if ($result > 0) {
// Store line items in database
foreach ($processed_lines as $line) {
$importLineObj = new ImportLine($db);
$importLineObj->fk_import = $import->id;
$importLineObj->line_id = $line['line_id'];
$importLineObj->supplier_ref = $line['supplier_ref'];
$importLineObj->product_name = $line['name'];
$importLineObj->description = $line['description'];
$importLineObj->quantity = $line['quantity'];
$importLineObj->unit_code = $line['unit_code'];
$importLineObj->unit_price = $line['unit_price'];
$importLineObj->unit_price_raw = $line['unit_price_raw'];
$importLineObj->basis_quantity = $line['basis_quantity'];
$importLineObj->basis_quantity_unit = $line['basis_quantity_unit'];
$importLineObj->line_total = $line['line_total'];
$importLineObj->tax_percent = $line['tax_percent'];
$importLineObj->ean = $line['ean'];
$importLineObj->fk_product = $line['fk_product'];
$importLineObj->match_method = $line['match_method'];
$importLineObj->create($user);
}
// Move PDF to permanent storage
$final_dir = $conf->importzugferd->dir_output.'/imports/'.$import->id;
if (!is_dir($final_dir)) {
dol_mkdir($final_dir);
}
if (!@rename($destfile, $final_dir.'/'.$filename)) {
// Fallback: copy + delete (z.B. bei verschiedenen Dateisystemen)
if (@copy($destfile, $final_dir.'/'.$filename)) {
@unlink($destfile);
} else {
dol_syslog('ImportZugferd: Fehler beim Verschieben der PDF nach '.$final_dir, LOG_ERR);
}
}
// Send notification if manual intervention required
if ($import->status == ZugferdImport::STATUS_PENDING) {
$storedLines = $importLine->fetchAllByImport($import->id);
$notification->sendManualInterventionNotification($import, $storedLines);
}
// Check for price differences
if ($import->status == ZugferdImport::STATUS_IMPORTED) {
$storedLines = $importLine->fetchAllByImport($import->id);
$notification->checkAndNotifyPriceDifferences($import, $storedLines);
}
// Redirect to edit page
$id = $import->id;
$action = 'edit';
setEventMessages($langs->trans('ImportRecordCreated'), null, 'mesgs');
} else {
$error++;
$message = $import->error;
@unlink($destfile);
// Send error notification
$notification->sendErrorNotification($import, $message, $filename);
}
} else {
$error++;
$message = $parser->error;
@unlink($destfile);
}
} else {
$error++;
$message = $parser->error;
@unlink($destfile);
}
}
} else {
$error++;
$message = $langs->trans('ErrorFileUploadFailed');
}
} else {
$error++;
$message = $langs->trans('ErrorNoFileUploaded');
}
}
// Load existing import for editing
if ($id > 0 && empty($action)) {
$action = 'edit';
}
if ($action == 'edit' && $id > 0) {
$result = $import->fetch($id);
if ($result <= 0) {
$error++;
$message = $langs->trans('ErrorRecordNotFound');
$action = '';
}
}
// Assign product to line
if ($action == 'assignproduct' && $line_id > 0 && $product_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
$lineObj->setProduct($product_id, $langs->trans('ManualAssignment'), $user);
setEventMessages($langs->trans('ProductAssigned'), null, 'mesgs');
// Get import ID to reload
$id = $lineObj->fk_import;
// Check if all lines now have products
$allHaveProducts = $importLine->allLinesHaveProducts($id);
if ($allHaveProducts) {
// Update import status
$import->fetch($id);
if ($import->status == ZugferdImport::STATUS_PENDING) {
$import->status = ZugferdImport::STATUS_IMPORTED;
$import->update($user);
// Check for price differences now that all products are assigned
$storedLines = $importLine->fetchAllByImport($id);
$notification->checkAndNotifyPriceDifferences($import, $storedLines);
}
}
}
$action = 'edit';
$import->fetch($id);
}
// Remove product assignment from line
if ($action == 'removeproduct' && $line_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
$id = $lineObj->fk_import;
$lineObj->setProduct(0, '', $user);
setEventMessages($langs->trans('ProductRemoved'), null, 'mesgs');
// Update import status to pending
$import->fetch($id);
if ($import->status == ZugferdImport::STATUS_IMPORTED) {
$import->status = ZugferdImport::STATUS_PENDING;
$import->update($user);
}
}
$action = 'edit';
$import->fetch($id);
}
// Fehlende Lieferantenpreise aus anderen Katalogen hinzufuegen
if ($action == 'addmissingprices' && $id > 0) {
$import->fetch($id);
$selectedPrices = GETPOST('add_prices', 'array');
if (!empty($selectedPrices)) {
$addedCount = 0;
$processedKeys = array();
foreach ($selectedPrices as $entry) {
// Duplikate ueberspringen
if (isset($processedKeys[$entry])) {
continue;
}
$processedKeys[$entry] = true;
$parts = explode(',', $entry);
if (count($parts) !== 3) {
continue;
}
$productId = (int) $parts[0];
$socId = (int) $parts[1];
$datanormId = (int) $parts[2];
if ($productId <= 0 || $socId <= 0 || $datanormId <= 0) {
continue;
}
$datanorm = new Datanorm($db);
if ($datanorm->fetch($datanormId) > 0) {
$altSupplier = new Societe($db);
$altSupplier->fetch($socId);
$purchasePrice = $datanorm->price;
if ($datanorm->price_unit > 1) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
$priceExtrafields = datanormBuildSupplierPriceExtrafields($datanorm);
$result = datanormAddSupplierPrice($db, $productId, $datanorm, $altSupplier, $user, $purchasePrice, 19, $priceExtrafields);
if ($result > 0) {
datanormInsertPriceExtrafields($db, $result, $priceExtrafields);
$mapping = new ProductMapping($db);
$mapping->fk_soc = $socId;
$mapping->supplier_ref = $datanorm->article_number;
$mapping->fk_product = $productId;
$mapping->ean = $datanorm->ean;
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
$mapping->description = $datanorm->short_text1;
$mapping->create($user);
$addedCount++;
}
}
}
if ($addedCount > 0) {
setEventMessages($langs->trans('SupplierPricesAdded', $addedCount), null, 'mesgs');
}
}
$action = 'edit';
}
// Update supplier
if ($action == 'setsupplier' && $id > 0) {
$import->fetch($id);
$import->fk_soc = $supplier_id;
$import->update($user);
setEventMessages($langs->trans('SupplierUpdated'), null, 'mesgs');
$action = 'edit';
}
// Duplicate product from template
if ($action == 'duplicateproduct' && $template_product_id > 0 && $line_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
// Load template product
$template = new Product($db);
if ($template->fetch($template_product_id) > 0) {
// Create new product as copy
$newproduct = new Product($db);
// Copy basic properties from template
$newproduct->type = $template->type;
$newproduct->status = $template->status;
$newproduct->status_buy = $template->status_buy;
$newproduct->status_batch = $template->status_batch;
$newproduct->fk_product_type = $template->fk_product_type;
$newproduct->price = $lineObj->unit_price;
$newproduct->price_base_type = 'HT';
$newproduct->tva_tx = $lineObj->tax_percent ?: $template->tva_tx;
$newproduct->weight = $template->weight;
$newproduct->weight_units = $template->weight_units;
$newproduct->fk_unit = $template->fk_unit;
// Set label from ZUGFeRD
$newproduct->label = $lineObj->product_name;
// Generate unique ref
$newproduct->ref = 'NEW-'.dol_print_date(dol_now(), '%Y%m%d%H%M%S');
// Build description with ZUGFeRD data
$zugferd_info = '';
if (!empty($lineObj->supplier_ref)) {
$zugferd_info .= $langs->trans('SupplierRef').': '.$lineObj->supplier_ref."\n";
}
if (!empty($lineObj->unit_code)) {
$zugferd_info .= $langs->trans('Unit').': '.zugferdGetUnitLabel($lineObj->unit_code)."\n";
}
if (!empty($lineObj->ean)) {
$zugferd_info .= 'EAN: '.$lineObj->ean."\n";
}
$zugferd_info .= "---\n";
$newproduct->description = $zugferd_info . ($template->description ?: '');
// Create the product
$result = $newproduct->create($user);
if ($result > 0) {
setEventMessages($langs->trans('ProductCreated'), null, 'mesgs');
// Redirect to product card for editing
header('Location: '.DOL_URL_ROOT.'/product/card.php?id='.$result);
exit;
} else {
setEventMessages($newproduct->error, $newproduct->errors, 'errors');
}
}
$id = $lineObj->fk_import;
}
$action = 'edit';
$import->fetch($id);
}
// Create product from Datanorm
if ($action == 'createfromdatanorm' && $line_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
$id = $lineObj->fk_import;
$import->fetch($id);
// Get Datanorm settings
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
// Search in Datanorm database
$datanorm = new Datanorm($db);
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
if (empty($results)) {
// Try with EAN if available
if (!empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
}
}
if (!empty($results)) {
$datanormArticle = $results[0];
$datanorm->fetch($datanormArticle['id']);
// Load supplier for ref prefix
$supplier = new Societe($db);
$supplier->fetch($import->fk_soc);
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
// Create new product
$newproduct = new Product($db);
$newproduct->type = 0; // Product
$newproduct->status = 1; // On sale
$newproduct->status_buy = 1; // On purchase
// Generate reference
$newproduct->ref = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
// Set default accounting codes from module settings
$newproduct->accountancy_code_sell = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL', '');
$newproduct->accountancy_code_sell_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA', '');
$newproduct->accountancy_code_sell_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT', '');
$newproduct->accountancy_code_buy = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY', '');
$newproduct->accountancy_code_buy_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA', '');
$newproduct->accountancy_code_buy_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT', '');
// Label from Datanorm
$newproduct->label = $datanorm->short_text1;
if (!empty($datanorm->short_text2) && strlen($newproduct->label) < 100) {
$newproduct->label .= ' '.$datanorm->short_text2;
}
// Description
$newproduct->description = $datanorm->getFullDescription();
// 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.")";
if (!$db->query($sqlInsertExtra)) {
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
}
}
}
// Create product mapping for future imports
$mapping = new ProductMapping($db);
$mapping->fk_soc = $import->fk_soc;
$mapping->supplier_ref = $datanorm->article_number;
$mapping->fk_product = $newproduct->id;
$mapping->ean = $datanorm->ean;
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
$mapping->description = $datanorm->short_text1;
$mapping->create($user);
// Assign to import line
$lineObj->setProduct($newproduct->id, 'datanorm', $user);
setEventMessages($langs->trans('ProductCreatedFromDatanorm', $newproduct->ref), null, 'mesgs');
// Check if all lines now have products
$allHaveProducts = $importLine->allLinesHaveProducts($id);
if ($allHaveProducts) {
$import->status = ZugferdImport::STATUS_IMPORTED;
$import->update($user);
}
} else {
setEventMessages($newproduct->error, $newproduct->errors, 'errors');
}
} else {
setEventMessages($langs->trans('DatanormArticleNotFound', $lineObj->supplier_ref), null, 'errors');
}
}
$action = 'edit';
$import->fetch($id);
}
// "Alle zuordnen" - Assign Datanorm matches to import lines
if ($action == 'assignallfromdatanorm' && $id > 0) {
$import->fetch($id);
if ($import->fk_soc > 0) {
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
// Get all lines without product
$lines = $importLine->fetchAllByImport($import->id);
$datanorm = new Datanorm($db);
$mapping = new ProductMapping($db);
$assignedCount = 0;
$datanormFoundCount = 0;
foreach ($lines as $lineObj) {
// Skip lines that already have a product
if ($lineObj->fk_product > 0) {
continue;
}
// Skip lines without supplier_ref
if (empty($lineObj->supplier_ref)) {
continue;
}
// Search in Datanorm database
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
if (empty($results) && !empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
}
if (!empty($results)) {
$datanormFoundCount++;
$datanormMatch = $results[0];
// Get Datanorm ID and article number (array access)
$datanormId = isset($datanormMatch['id']) ? $datanormMatch['id'] : (isset($datanormMatch['rowid']) ? $datanormMatch['rowid'] : 0);
$articleNumber = isset($datanormMatch['article_number']) ? $datanormMatch['article_number'] : '';
// Check if product already exists for this supplier ref
$existingProductId = $mapping->findProductBySupplierRef($import->fk_soc, $articleNumber);
if ($existingProductId > 0) {
// Product exists - assign both product and Datanorm to the line
$lineObj->fk_product = $existingProductId;
$lineObj->fk_datanorm = $datanormId;
$lineObj->match_method = 'datanorm_assign';
$lineObj->update($user);
$assignedCount++;
} else {
// No product yet - save Datanorm reference for later product creation
$lineObj->fk_datanorm = $datanormId;
$lineObj->match_method = 'datanorm_pending';
$lineObj->update($user);
}
}
}
if ($assignedCount > 0) {
setEventMessages($langs->trans('ProductsAssignedFromDatanorm', $assignedCount), null, 'mesgs');
}
if ($datanormFoundCount > $assignedCount) {
$pendingCount = $datanormFoundCount - $assignedCount;
setEventMessages($langs->trans('DatanormMatchesFoundNotAssigned', $pendingCount), null, 'mesgs');
}
if ($datanormFoundCount == 0) {
setEventMessages($langs->trans('DatanormBatchNoMatches'), null, 'warnings');
}
}
header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&id='.$id.'&token='.newToken());
exit;
}
// Create ALL products from Datanorm (batch)
if ($action == 'createallfromdatanorm' && $id > 0) {
$import->fetch($id);
if ($import->fk_soc > 0) {
// Get Datanorm settings
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
// Load supplier
$supplier = new Societe($db);
$supplier->fetch($import->fk_soc);
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
// Get all lines without product
$lines = $importLine->fetchAllByImport($import->id);
$datanorm = new Datanorm($db);
$createdCount = 0;
$assignedCount = 0;
$errorCount = 0;
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
foreach ($lines as $lineObj) {
// Skip lines that already have a product
if ($lineObj->fk_product > 0) {
continue;
}
// Skip lines without supplier_ref
if (empty($lineObj->supplier_ref)) {
continue;
}
// Search in Datanorm database
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
if (empty($results) && !empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
}
if (!empty($results)) {
$datanormArticle = $results[0];
$datanorm->fetch($datanormArticle['id']);
$purchasePrice = $datanorm->price;
if ($datanorm->price_unit > 1) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
// Get copper surcharge for selling price calculation
// Priority: 1. Datanorm, 2. ZUGFeRD
$copperSurchargeForPrice = 0;
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$copperSurchargeForPrice = $datanorm->metal_surcharge;
// Normalize to per-unit if price_unit > 1
if ($datanorm->price_unit > 1) {
$copperSurchargeForPrice = $datanorm->metal_surcharge / $datanorm->price_unit;
}
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
$copperSurchargeForPrice = $lineObj->copper_surcharge;
// Normalize based on copper_surcharge_basis_qty
if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) {
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty;
}
}
// Check if product already exists in Dolibarr
$existingProduct = new Product($db);
$productExists = false;
$existingProductId = 0;
// 1. Check by supplier reference (ProductFournisseur)
$sqlCheck = "SELECT DISTINCT pf.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pf";
$sqlCheck .= " WHERE pf.fk_soc = ".(int)$import->fk_soc;
$sqlCheck .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'";
$sqlCheck .= " AND pf.entity IN (".getEntity('product').")";
$resqlCheck = $db->query($sqlCheck);
if ($resqlCheck && $db->num_rows($resqlCheck) > 0) {
$objCheck = $db->fetch_object($resqlCheck);
$existingProductId = $objCheck->fk_product;
$productExists = true;
}
// 2. Check by product reference pattern
if (!$productExists) {
$expectedRef = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
$fetchResult = $existingProduct->fetch(0, $expectedRef);
if ($fetchResult > 0) {
$existingProductId = $existingProduct->id;
$productExists = true;
}
}
// 3. Check by EAN if available
if (!$productExists && !empty($datanorm->ean)) {
$sqlEan = "SELECT rowid FROM ".MAIN_DB_PREFIX."product";
$sqlEan .= " WHERE barcode = '".$db->escape($datanorm->ean)."'";
$sqlEan .= " AND entity IN (".getEntity('product').")";
$resqlEan = $db->query($sqlEan);
if ($resqlEan && $db->num_rows($resqlEan) > 0) {
$objEan = $db->fetch_object($resqlEan);
$existingProductId = $objEan->rowid;
$productExists = true;
}
}
if ($productExists && $existingProductId > 0) {
// Product exists - just assign it to the line
$lineObj->setProduct($existingProductId, 'datanorm', $user);
// Add additional supplier prices from selected alternatives (for existing products too)
$supplierPricesPost = GETPOST('supplier_prices', 'array');
if (!empty($supplierPricesPost[$lineObj->id])) {
foreach ($supplierPricesPost[$lineObj->id] as $altSocId => $altDatanormId) {
// Check if supplier price already exists for this product/supplier
$sqlCheckSupplier = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlCheckSupplier .= " WHERE fk_product = ".(int)$existingProductId;
$sqlCheckSupplier .= " AND fk_soc = ".(int)$altSocId;
$resCheckSupplier = $db->query($sqlCheckSupplier);
if ($resCheckSupplier && $db->num_rows($resCheckSupplier) > 0) {
continue; // Skip if supplier price already exists
}
// Fetch the alternative Datanorm article
$altDatanorm = new Datanorm($db);
if ($altDatanorm->fetch($altDatanormId) > 0) {
$altSupplier = new Societe($db);
$altSupplier->fetch($altSocId);
$altPurchasePrice = $altDatanorm->price;
if ($altDatanorm->price_unit > 1) {
$altPurchasePrice = $altDatanorm->price / $altDatanorm->price_unit;
}
// Prepare extrafields
$altExtrafields = array();
if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0) {
$altExtrafields['options_kupferzuschlag'] = $altDatanorm->metal_surcharge;
}
if (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) {
$altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit;
}
if (!empty($altDatanorm->product_group)) {
$altExtrafields['options_warengruppe'] = $altDatanorm->product_group;
}
// Add supplier price
$altProdfourn = new ProductFournisseur($db);
$altProdfourn->id = $existingProductId;
$altProdfourn->update_buyprice(
1, $altPurchasePrice, $user, 'HT', $altSupplier, 0,
$altDatanorm->article_number, $lineObj->tax_percent ?: 19,
0, 0, 0, 0, 0, 0, array(), '',
0, 'HT', 1, '',
trim($altDatanorm->short_text1 . ($altDatanorm->short_text2 ? ' ' . $altDatanorm->short_text2 : '')),
!empty($altDatanorm->ean) ? $altDatanorm->ean : '',
!empty($altDatanorm->ean) ? 2 : 0,
$altExtrafields
);
// Create product mapping
$altMapping = new ProductMapping($db);
$altMapping->fk_soc = $altSocId;
$altMapping->supplier_ref = $altDatanorm->article_number;
$altMapping->fk_product = $existingProductId;
$altMapping->ean = $altDatanorm->ean;
$altMapping->manufacturer_ref = $altDatanorm->manufacturer_ref;
$altMapping->description = $altDatanorm->short_text1;
$altMapping->create($user);
}
}
}
$assignedCount++;
} 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.")";
if (!$db->query($sqlInsertExtra)) {
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
}
}
}
// Create product mapping
$mapping = new ProductMapping($db);
$mapping->fk_soc = $import->fk_soc;
$mapping->supplier_ref = $datanorm->article_number;
$mapping->fk_product = $newproduct->id;
$mapping->ean = $datanorm->ean;
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
$mapping->description = $datanorm->short_text1;
$mapping->create($user);
// Add additional supplier prices from selected alternatives
$supplierPricesPost = GETPOST('supplier_prices', 'array');
if (!empty($supplierPricesPost[$lineObj->id])) {
foreach ($supplierPricesPost[$lineObj->id] as $altSocId => $altDatanormId) {
// Skip the main invoice supplier (already added above)
if ($altSocId == $import->fk_soc) {
continue;
}
// Fetch the alternative Datanorm article
$altDatanorm = new Datanorm($db);
if ($altDatanorm->fetch($altDatanormId) > 0) {
$altSupplier = new Societe($db);
$altSupplier->fetch($altSocId);
$altPurchasePrice = $altDatanorm->price;
if ($altDatanorm->price_unit > 1) {
$altPurchasePrice = $altDatanorm->price / $altDatanorm->price_unit;
}
// Prepare extrafields for alternative supplier price
$altExtrafields = array();
if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0) {
$altExtrafields['options_kupferzuschlag'] = $altDatanorm->metal_surcharge;
}
if (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) {
$altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit;
}
if (!empty($altDatanorm->product_group)) {
$altExtrafields['options_warengruppe'] = $altDatanorm->product_group;
}
// Add supplier price for alternative supplier
$altProdfourn = new ProductFournisseur($db);
$altProdfourn->id = $newproduct->id;
$altProdfourn->update_buyprice(
1, // Quantity
$altPurchasePrice, // Price
$user,
'HT', // Price base
$altSupplier, // Alternative supplier
0, // Availability
$altDatanorm->article_number, // Supplier ref
$lineObj->tax_percent ?: 19, // VAT
0, 0, 0, 0, 0, 0, array(), '',
0, 'HT', 1, '',
trim($altDatanorm->short_text1 . ($altDatanorm->short_text2 ? ' ' . $altDatanorm->short_text2 : '')),
!empty($altDatanorm->ean) ? $altDatanorm->ean : '',
!empty($altDatanorm->ean) ? 2 : 0,
$altExtrafields
);
// Create product mapping for alternative supplier
$altMapping = new ProductMapping($db);
$altMapping->fk_soc = $altSocId;
$altMapping->supplier_ref = $altDatanorm->article_number;
$altMapping->fk_product = $newproduct->id;
$altMapping->ean = $altDatanorm->ean;
$altMapping->manufacturer_ref = $altDatanorm->manufacturer_ref;
$altMapping->description = $altDatanorm->short_text1;
$altMapping->create($user);
}
}
}
// Assign to import line
$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 - get ALL supplier alternatives
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 10);
if (empty($results) && !empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 10);
}
if (!empty($results)) {
// Process the primary result (first = current supplier or cheapest)
$datanormArticle = $results[0];
$datanorm->fetch($datanormArticle['id']);
$purchasePrice = $datanorm->price;
if ($datanorm->price_unit > 1) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
// Get copper surcharge for selling price calculation
$copperSurchargeForPrice = 0;
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$copperSurchargeForPrice = $datanorm->metal_surcharge;
if ($datanorm->price_unit > 1) {
$copperSurchargeForPrice = $datanorm->metal_surcharge / $datanorm->price_unit;
}
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
$copperSurchargeForPrice = $lineObj->copper_surcharge;
if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) {
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty;
}
}
// Calculate selling price
$sellingPrice = ($purchasePrice + $copperSurchargeForPrice) * (1 + $markup / 100);
// Check if product already exists in Dolibarr
$existingProductId = 0;
$productAction = 'create'; // 'create' or 'assign'
// 1. Check by supplier reference (ProductFournisseur)
$sqlCheck = "SELECT DISTINCT pf.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pf";
$sqlCheck .= " WHERE pf.fk_soc = ".(int)$import->fk_soc;
$sqlCheck .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'";
$sqlCheck .= " AND pf.entity IN (".getEntity('product').")";
$resqlCheck = $db->query($sqlCheck);
if ($resqlCheck && $db->num_rows($resqlCheck) > 0) {
$objCheck = $db->fetch_object($resqlCheck);
$existingProductId = $objCheck->fk_product;
$productAction = 'assign';
}
// 2. Check by product reference pattern
if ($existingProductId <= 0) {
$expectedRef = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
$existingProduct = new Product($db);
$fetchResult = $existingProduct->fetch(0, $expectedRef);
if ($fetchResult > 0) {
$existingProductId = $existingProduct->id;
$productAction = 'assign';
}
}
// 3. Check by EAN if available
if ($existingProductId <= 0 && !empty($datanorm->ean)) {
$sqlEan = "SELECT rowid FROM ".MAIN_DB_PREFIX."product";
$sqlEan .= " WHERE barcode = '".$db->escape($datanorm->ean)."'";
$sqlEan .= " AND entity IN (".getEntity('product').")";
$resqlEan = $db->query($sqlEan);
if ($resqlEan && $db->num_rows($resqlEan) > 0) {
$objEan = $db->fetch_object($resqlEan);
$existingProductId = $objEan->rowid;
$productAction = 'assign';
}
}
// Build supplier alternatives array
$supplierAlternatives = array();
foreach ($results as $altResult) {
$altSupplier = new Societe($db);
$altSupplier->fetch($altResult['fk_soc']);
$altPurchasePrice = $altResult['price'];
if ($altResult['price_unit'] > 1) {
$altPurchasePrice = $altResult['price'] / $altResult['price_unit'];
}
$supplierAlternatives[] = array(
'datanorm_id' => $altResult['id'],
'fk_soc' => $altResult['fk_soc'],
'supplier_name' => $altSupplier->name,
'article_number' => $altResult['article_number'],
'short_text1' => $altResult['short_text1'],
'price' => $altResult['price'],
'price_unit' => $altResult['price_unit'],
'purchase_price' => $altPurchasePrice,
'ean' => $altResult['ean'],
'manufacturer_ref' => $altResult['manufacturer_ref'],
'is_invoice_supplier' => ($altResult['fk_soc'] == $import->fk_soc),
);
}
// Store match info for preview
$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,
'supplier_alternatives' => $supplierAlternatives
);
}
}
}
$action = 'edit';
}
// Create supplier invoice
if ($action == 'createinvoice' && $id > 0) {
$import->fetch($id);
// Check prerequisites
if ($import->fk_soc <= 0) {
$error++;
setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors');
} else {
// Check all lines have products
$lines = $importLine->fetchAllByImport($id);
$allHaveProducts = true;
foreach ($lines as $line) {
if ($line->fk_product <= 0) {
$allHaveProducts = false;
break;
}
}
if (!$allHaveProducts) {
$error++;
setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors');
} else {
// Load supplier to get default values
$supplier = new Societe($db);
$supplier->fetch($import->fk_soc);
// Create invoice
$invoice = new FactureFournisseur($db);
$invoice->socid = $import->fk_soc;
$invoice->ref_supplier = $import->invoice_number;
$invoice->date = $import->invoice_date;
$invoice->note_private = $langs->trans('ImportedFromZugferd').' ('.$import->ref.')';
// 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);
}
if (!@copy($source_pdf, $dest_dir.'/'.$import->pdf_filename)) {
dol_syslog('ImportZugferd: Fehler beim Kopieren der PDF nach '.$dest_dir, LOG_ERR);
}
}
// Update import record
$import->fk_facture_fourn = $invoice->id;
$import->status = ZugferdImport::STATUS_PROCESSED;
$import->date_import = dol_now();
$import->update($user);
$db->commit();
setEventMessages($langs->trans('InvoiceCreatedSuccessfully'), null, 'mesgs');
// Redirect to invoice
header('Location: '.DOL_URL_ROOT.'/fourn/facture/card.php?facid='.$invoice->id);
exit;
} else {
$db->rollback();
}
} else {
$error++;
setEventMessages($invoice->error, $invoice->errors, 'errors');
$db->rollback();
}
}
}
$action = 'edit';
}
// Finish import - check for existing invoice and update status
if ($action == 'finishimport' && $id > 0) {
$import->fetch($id);
// Check all lines have products
$lines = $importLine->fetchAllByImport($id);
$allHaveProducts = true;
foreach ($lines as $line) {
if ($line->fk_product <= 0) {
$allHaveProducts = false;
break;
}
}
if (!$allHaveProducts) {
$error++;
setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors');
} elseif ($import->fk_soc <= 0) {
$error++;
setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors');
} else {
// Search for existing supplier invoice with this ref_supplier
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_fourn";
$sql .= " WHERE fk_soc = ".((int) $import->fk_soc);
$sql .= " AND ref_supplier = '".$db->escape($import->invoice_number)."'";
$sql .= " LIMIT 1";
$resql = $db->query($sql);
if ($resql && $db->num_rows($resql) > 0) {
$obj = $db->fetch_object($resql);
// Found existing invoice - link it
$import->fk_facture_fourn = $obj->rowid;
$import->status = ZugferdImport::STATUS_PROCESSED;
$import->date_import = dol_now();
$result = $import->update($user);
if ($result > 0) {
$invoiceLink = '<a href="'.DOL_URL_ROOT.'/fourn/facture/card.php?facid='.$obj->rowid.'">'.$import->invoice_number.'</a>';
setEventMessages($langs->trans('ImportLinkedToExistingInvoice', $invoiceLink), null, 'mesgs');
} else {
setEventMessages($import->error, null, 'errors');
}
} else {
// No existing invoice - mark as imported (ready for invoice creation)
$import->status = ZugferdImport::STATUS_IMPORTED;
$result = $import->update($user);
if ($result > 0) {
setEventMessages($langs->trans('ImportFinished'), null, 'mesgs');
} else {
setEventMessages($import->error, null, 'errors');
}
}
}
$action = 'edit';
}
// Delete import record
if ($action == 'confirm_delete' && $confirm == 'yes' && $id > 0 && $user->hasRight('importzugferd', 'import', 'delete')) {
$import->fetch($id);
// Delete lines first
$importLine->deleteAllByImport($id);
// Delete files
$import_dir = $conf->importzugferd->dir_output.'/imports/'.$import->id;
if (is_dir($import_dir)) {
dol_delete_dir_recursive($import_dir);
}
// Delete import record
$import->delete($user);
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
/*
* View
*/
$title = $langs->trans('ZugferdImport');
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-import');
print load_fiche_titre($title, '', 'fa-file-import');
// Error message
if ($error && !empty($message)) {
setEventMessages($message, null, 'errors');
}
/*
* Upload form (shown when no import is being edited)
*/
if (empty($action) || ($action == 'upload' && $error)) {
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" enctype="multipart/form-data">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="upload">';
print '<div class="fichecenter">';
print '<div class="fichethirdleft">';
print '<div class="div-table-responsive-no-min">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td colspan="2">'.$langs->trans('UploadZugferdInvoice').'</td>';
print '</tr>';
print '<tr class="oddeven">';
print '<td class="titlefield">'.$langs->trans('File').' (PDF)</td>';
print '<td>';
print '<input type="file" name="zugferd_file" accept=".pdf" class="flat minwidth300" required>';
print '</td>';
print '</tr>';
print '<tr class="oddeven">';
print '<td>'.$langs->trans('ForceReimport').'</td>';
print '<td>';
print '<input type="checkbox" name="force_reimport" value="1"> ';
print '<span class="opacitymedium">'.$langs->trans('ForceReimportHelp').'</span>';
print '</td>';
print '</tr>';
print '</table>';
print '</div>';
print '<div class="center" style="margin-top: 20px;">';
print '<input type="submit" class="button button-primary" value="'.$langs->trans('Upload').'">';
print '</div>';
print '</div>';
// Show pending imports
print '<div class="fichetwothirdright">';
print '<div class="div-table-responsive-no-min">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td colspan="5">'.$langs->trans('PendingImports').'</td>';
print '</tr>';
$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 '<tr class="liste_titre">';
print '<td>'.$langs->trans('Ref').'</td>';
print '<td>'.$langs->trans('InvoiceNumber').'</td>';
print '<td>'.$langs->trans('Supplier').'</td>';
print '<td class="right">'.$langs->trans('TotalTTC').'</td>';
print '<td>'.$langs->trans('Status').'</td>';
print '</tr>';
while ($obj = $db->fetch_object($resql)) {
print '<tr class="oddeven">';
print '<td><a href="'.$_SERVER['PHP_SELF'].'?id='.$obj->rowid.'">'.$obj->ref.'</a></td>';
print '<td>'.$obj->invoice_number.'</td>';
print '<td>'.$obj->seller_name.'</td>';
print '<td class="right">'.price($obj->total_ttc).'</td>';
print '<td>';
$tmpimport = new ZugferdImport($db);
print $tmpimport->LibStatut($obj->status, 1);
print '</td>';
print '</tr>';
}
} else {
print '<tr class="oddeven"><td colspan="5" class="opacitymedium">'.$langs->trans('NoPendingImports').'</td></tr>';
}
}
print '</table>';
print '</div>';
print '</div>';
print '</div>';
print '</form>';
}
/*
* 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 '<div class="fichecenter">';
// Status banner
if ($import->status == ZugferdImport::STATUS_PENDING) {
print '<div class="warning">';
print '<i class="fas fa-exclamation-triangle paddingright"></i>';
print $langs->trans('ManualInterventionRequired');
if ($missingProducts > 0) {
print ' - '.$missingProducts.' '.$langs->trans('ProductsNotAssigned');
}
if ($import->fk_soc <= 0) {
print ' - '.$langs->trans('SupplierNotAssigned');
}
print '</div><br>';
} elseif ($allComplete) {
print '<div class="ok" style="padding: 10px; margin-bottom: 10px;">';
print '<i class="fas fa-check-circle paddingright"></i>';
print $langs->trans('ReadyToCreateInvoice');
print '</div>';
}
// Invoice data
print '<div class="div-table-responsive-no-min">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td colspan="4">'.$langs->trans('InvoiceData').' - '.$import->ref.'</td>';
print '</tr>';
print '<tr class="oddeven">';
print '<td class="titlefield">'.$langs->trans('InvoiceNumber').'</td>';
print '<td><strong>'.dol_escape_htmltag($import->invoice_number).'</strong></td>';
print '<td>'.$langs->trans('InvoiceDate').'</td>';
print '<td>'.dol_print_date($import->invoice_date, 'day').'</td>';
print '</tr>';
print '<tr class="oddeven">';
print '<td>'.$langs->trans('Supplier').'</td>';
print '<td>'.dol_escape_htmltag($import->seller_name).'</td>';
print '<td>'.$langs->trans('VATIntra').'</td>';
print '<td>'.dol_escape_htmltag($import->seller_vat).'</td>';
print '</tr>';
print '<tr class="oddeven">';
print '<td>'.$langs->trans('BuyerReference').'</td>';
print '<td>'.dol_escape_htmltag($import->buyer_reference).'</td>';
print '<td>'.$langs->trans('TotalHT').'</td>';
print '<td>'.price($import->total_ht).' '.$import->currency.'</td>';
print '</tr>';
print '<tr class="oddeven">';
print '<td>'.$langs->trans('Status').'</td>';
print '<td>'.$import->getLibStatut(1).'</td>';
print '<td>'.$langs->trans('TotalTTC').'</td>';
print '<td><strong>'.price($import->total_ttc).' '.$import->currency.'</strong></td>';
print '</tr>';
print '</table>';
print '</div>';
// Supplier selection
print '<br>';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="setsupplier">';
print '<input type="hidden" name="id" value="'.$import->id.'">';
print '<div class="div-table-responsive-no-min">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td colspan="2">'.$langs->trans('SupplierAssignment').'</td>';
print '</tr>';
print '<tr class="oddeven">';
print '<td class="titlefield">'.$langs->trans('SelectSupplier').' <span class="fieldrequired">*</span></td>';
print '<td>';
print $form->select_company($import->fk_soc, 'supplier_id', 's.fournisseur = 1', 'SelectThirdParty', 0, 0, null, 0, 'minwidth300');
print ' <input type="submit" class="button smallpaddingimp" value="'.$langs->trans('Save').'">';
print '</td>';
print '</tr>';
print '</table>';
print '</div>';
print '</form>';
// Line items
print '<br>';
print '<div class="div-table-responsive">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td>'.$langs->trans('Position').'</td>';
print '<td>'.$langs->trans('SupplierRef').'</td>';
print '<td>'.$langs->trans('ProductDescription').'</td>';
print '<td class="right">'.$langs->trans('Qty').'</td>';
print '<td class="right">'.$langs->trans('UnitPrice').'</td>';
print '<td class="right">'.$langs->trans('DolibarrPrice').'</td>';
print '<td class="right">'.$langs->trans('TotalHT').'</td>';
print '<td>'.$langs->trans('MatchedProduct').'</td>';
print '<td>'.$langs->trans('Action').'</td>';
print '</tr>';
// Initialize totals for summary row
$totalDolibarrHT = 0;
$totalZugferdHT = 0;
$hasDolibarrPrices = false;
$allProductsMatched = true;
$matchedLinesCount = 0;
$totalLinesCount = count($lines);
$allMissingPrices = array(); // Fehlende Lieferantenpreise sammeln
foreach ($lines as $line) {
$hasProduct = ($line->fk_product > 0);
$rowStyle = $hasProduct ? 'background-color: #dff0d8;' : ''; // Green for matched products
print '<tr class="oddeven" style="'.$rowStyle.'">';
print '<td>'.$line->line_id.'</td>';
print '<td>'.dol_escape_htmltag($line->supplier_ref).'</td>';
print '<td>';
print dol_escape_htmltag($line->product_name);
if (!empty($line->ean) && !$hasProduct) {
print '<br><span style="color: #666;">EAN: '.dol_escape_htmltag($line->ean).'</span>';
}
print '</td>';
print '<td class="right">'.price2num($line->quantity, 'MS').' '.zugferdGetUnitLabel($line->unit_code).'</td>';
print '<td class="right">';
print price($line->unit_price);
if (!empty($line->basis_quantity) && $line->basis_quantity != 1) {
print '<br><span style="color: #888; font-size: 0.9em;">('.price($line->unit_price_raw).'/'.price2num($line->basis_quantity, 'MS').zugferdGetUnitLabel($line->basis_quantity_unit).')</span>';
}
print '</td>';
// Dolibarr price column - show supplier price and difference
print '<td class="right nowraponall">';
$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 '<br>';
if ($priceDiff > 0) {
// ZUGFeRD price is higher
$iconColor = $isSignificant ? 'color: #d9534f;' : 'color: #f0ad4e;';
print '<span style="'.$iconColor.'" title="'.$langs->trans('PriceIncrease').'">';
print '<i class="fas fa-arrow-up"></i> +'.number_format($priceDiffPercent, 1).'%';
print '</span>';
} else {
// ZUGFeRD price is lower
$iconColor = $isSignificant ? 'color: #5cb85c;' : 'color: #5bc0de;';
print '<span style="'.$iconColor.'" title="'.$langs->trans('PriceDecrease').'">';
print '<i class="fas fa-arrow-down"></i> '.number_format($priceDiffPercent, 1).'%';
print '</span>';
}
} else {
print '<br><span class="opacitymedium"><i class="fas fa-equals"></i></span>';
}
} else {
print '<span class="opacitymedium">'.$langs->trans('NoPriceFound').'</span>';
$allProductsMatched = false; // No price found for matched product
}
} else {
print '<span class="opacitymedium">-</span>';
$allProductsMatched = false; // Product not matched
}
print '</td>';
print '<td class="right">'.price($line->line_total).'</td>';
print '<td>';
if ($hasProduct) {
$product = new Product($db);
$product->fetch($line->fk_product);
print $product->getNomUrl(1);
if (!empty($line->match_method)) {
print '<br><span class="opacitymedium">'.$langs->trans('MatchMethod').': '.$line->match_method.'</span>';
}
if (!empty($line->ean)) {
print '<br><span class="opacitymedium"><i class="fas fa-barcode"></i> '.dol_escape_htmltag($line->ean).'</span>';
}
print ' <i class="fas fa-check-circle" style="color: green;"></i>';
// Alle Einkaufspreise des Produktes anzeigen
$sqlPrices = "SELECT pfp.fk_soc, pfp.price, pfp.ref_fourn, pfp.quantity, s.nom as supplier_name";
$sqlPrices .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price pfp";
$sqlPrices .= " LEFT JOIN ".MAIN_DB_PREFIX."societe s ON s.rowid = pfp.fk_soc";
$sqlPrices .= " WHERE pfp.fk_product = ".(int)$line->fk_product;
$sqlPrices .= " ORDER BY pfp.price ASC";
$resPrices = $db->query($sqlPrices);
if ($resPrices && $db->num_rows($resPrices) > 0) {
print '<div style="margin-top: 4px; font-size: 0.85em; line-height: 1.4;">';
while ($objP = $db->fetch_object($resPrices)) {
$isInvoiceSupplier = ($objP->fk_soc == $import->fk_soc);
$style = $isInvoiceSupplier ? 'font-weight: bold;' : 'color: #666;';
print '<div style="'.$style.'">';
print dol_escape_htmltag($objP->supplier_name);
print ': <strong>'.price($objP->price).'</strong>';
if (!empty($objP->ref_fourn)) {
print ' <span class="opacitymedium">('.dol_escape_htmltag($objP->ref_fourn).')</span>';
}
if ($isInvoiceSupplier) {
print ' <i class="fas fa-file-invoice" style="color: #337ab7;" title="'.$langs->trans('SupplierAssignment').'"></i>';
}
print '</div>';
}
print '</div>';
}
} else {
print '<span class="warning">'.$langs->trans('NoProductMatch').'</span>';
}
print '</td>';
print '<td class="nowraponall">';
if ($hasProduct) {
// Remove assignment button
print '<a href="'.$_SERVER['PHP_SELF'].'?action=removeproduct&line_id='.$line->id.'&id='.$import->id.'&token='.newToken().'" class="button buttongen">';
print '<i class="fas fa-times"></i>';
print '</a>';
// Fehlende Lieferantenpreise aus anderen Katalogen sammeln (Anzeige weiter unten)
if ($import->fk_soc > 0 && getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL')) {
// Alle vorhandenen Lieferantenpreise fuer dieses Produkt laden
$sqlExistingPrices = "SELECT fk_soc, price, barcode FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlExistingPrices .= " WHERE fk_product = ".(int)$line->fk_product;
$resExistingPrices = $db->query($sqlExistingPrices);
$existingSupplierIds = array();
$currentSupplierPrice = 0;
$supplierEan = '';
if ($resExistingPrices) {
while ($objPrice = $db->fetch_object($resExistingPrices)) {
$existingSupplierIds[$objPrice->fk_soc] = true;
// Preis und EAN vom Rechnungslieferanten merken
if ($objPrice->fk_soc == $import->fk_soc) {
$currentSupplierPrice = $objPrice->price;
if (!empty($objPrice->barcode)) {
$supplierEan = $objPrice->barcode;
}
}
}
}
// EAN-Quellen: 1. Lieferantenpreis-Barcode, 2. Import-Zeile EAN
$searchEan = !empty($supplierEan) ? $supplierEan : (!empty($line->ean) ? $line->ean : '');
$datanormSearch = new Datanorm($db);
$allCatalogResults = array();
// Primaer: EAN-Suche in Datanorm-Katalogen (zuverlaessigste Methode)
if (!empty($searchEan)) {
$allCatalogResults = $datanormSearch->searchByArticleNumber($searchEan, $import->fk_soc, true, 10);
}
// Fallback: Lieferanten-Artikelnummer wenn EAN nichts fand
if (empty($allCatalogResults) && !empty($line->supplier_ref)) {
$allCatalogResults = $datanormSearch->searchByArticleNumber($line->supplier_ref, $import->fk_soc, true, 10);
}
if (!empty($allCatalogResults)) {
$missingSuppliers = array();
foreach ($allCatalogResults as $catalogResult) {
if (!isset($existingSupplierIds[$catalogResult['fk_soc']])) {
$altSupplier = new Societe($db);
$altSupplier->fetch($catalogResult['fk_soc']);
$altPurchasePrice = $catalogResult['price'];
if ($catalogResult['price_unit'] > 1) {
$altPurchasePrice = $catalogResult['price'] / $catalogResult['price_unit'];
}
$missingSuppliers[] = array(
'datanorm_id' => $catalogResult['id'],
'fk_soc' => $catalogResult['fk_soc'],
'supplier_name' => $altSupplier->name,
'article_number' => $catalogResult['article_number'],
'price' => $catalogResult['price'],
'price_unit' => $catalogResult['price_unit'],
'purchase_price' => $altPurchasePrice,
'ean' => $catalogResult['ean'],
);
}
}
if (!empty($missingSuppliers)) {
// Hinweis in der Zeile anzeigen
print ' <span class="badge badge-warning" title="'.$langs->trans('MissingSupplierPrices').'"><i class="fas fa-plus-circle"></i> '.count($missingSuppliers).'</span>';
// Duplikate vermeiden (gleiches Produkt auf mehreren Rechnungszeilen)
if (!isset($allMissingPrices[$line->fk_product])) {
$allMissingPrices[$line->fk_product] = array(
'product_id' => $line->fk_product,
'product_ref' => $product->ref,
'product_label' => $product->label,
'current_price' => $currentSupplierPrice,
'missing' => $missingSuppliers,
);
}
}
}
}
} else {
// Product selection form
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" class="inline-block">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="assignproduct">';
print '<input type="hidden" name="line_id" value="'.$line->id.'">';
print '<input type="hidden" name="id" value="'.$import->id.'">';
print $form->select_produits('', 'product_id_'.$line->id, '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth150 maxwidth200', 1, '', 0);
print ' <button type="submit" class="button buttongen" title="'.$langs->trans('AssignProduct').'">';
print '<i class="fas fa-link"></i>';
print '</button>';
print '</form>';
// 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 '<br><a href="'.$create_url.'" target="_blank" class="button buttongen margintoponlyshort">';
print '<i class="fas fa-plus-circle"></i> '.$langs->trans('CreateProduct');
print '</a>';
// Refresh-Button nach Produktanlage
print ' <a href="'.$_SERVER['PHP_SELF'].'?id='.$import->id.'" class="button buttongen margintoponlyshort" title="'.$langs->trans('RefreshProductListHelp').'">';
print '<i class="fas fa-sync-alt"></i>';
print '</a>';
// Product template
print '<br>';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" class="inline-block margintoponlyshort">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="duplicateproduct">';
print '<input type="hidden" name="line_id" value="'.$line->id.'">';
print '<input type="hidden" name="id" value="'.$import->id.'">';
print $form->select_produits('', 'template_product_id_'.$line->id, '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth100 maxwidth150', 1, '', 0);
print ' <button type="submit" class="button buttongen" title="'.$langs->trans('ProductTemplateHelp').'">';
print '<i class="fas fa-copy"></i>';
print '</button>';
print '</form>';
// 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 '<br>';
print '<a href="'.$_SERVER['PHP_SELF'].'?action=createfromdatanorm&line_id='.$line->id.'&id='.$import->id.'&token='.newToken().'" class="button buttongen margintoponlyshort" title="'.$langs->trans('CreateFromDatanormHelp').'">';
print '<i class="fas fa-database paddingright"></i>'.$langs->trans('CreateFromDatanorm');
print '</a>';
// Button to show raw Datanorm data
print ' <a href="#" class="button buttongen margintoponlyshort" onclick="showRawDatanorm(\''.dol_escape_js($line->supplier_ref).'\', '.$import->fk_soc.'); return false;" title="'.$langs->trans('ShowRawDatanorm').'">';
print '<i class="fas fa-file-code"></i>';
print '</a>';
// Show comparison: Invoice name vs Datanorm name
print '<div class="small" style="margin-top: 5px; padding: 5px; background-color: #e8f4fd; border-radius: 3px;">';
print '<table class="noborder" style="width: 100%; font-size: 0.85em;">';
print '<tr><td style="color: #666; width: 70px;"><i class="fas fa-file-invoice"></i> Rechnung:</td>';
print '<td><strong>'.dol_trunc($line->product_name, 50).'</strong></td></tr>';
print '<tr><td style="color: #666;"><i class="fas fa-database"></i> Datanorm:</td>';
print '<td style="color: #2980b9;"><strong>'.dol_trunc($datanormArticle['short_text1'], 50).'</strong>';
print ' <span style="color: #27ae60;">('.price($datanormArticle['price']).')</span></td></tr>';
print '</table>';
print '</div>';
}
}
}
print '</td>';
print '</tr>';
// 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 '<tr style="background-color: #f5f5f5; font-weight: bold;">';
print '<td colspan="5" class="right"><strong>'.$langs->trans('Total').' '.$langs->trans('TotalHT').'</strong></td>';
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 '<td class="right nowraponall" style="'.$cellStyle.'">';
print '<strong>'.price($totalDolibarrHT).'</strong>';
if (abs($totalDiffPercent) >= 0.01) {
print '<br>';
if ($totalDiff > 0) {
print '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($totalDiffPercent, 1).'%</span>';
} elseif ($totalDiff < 0) {
print '<span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.number_format($totalDiffPercent, 1).'%</span>';
}
}
print '</td>';
print '<td class="right" style="'.$cellStyle.'"><strong>'.price($totalZugferdHT).'</strong></td>';
print '<td colspan="2" class="nowraponall" style="'.$cellStyle.'">';
if ($isMatch) {
print '<span style="color: #3c763d;"><i class="fas fa-check-circle"></i> '.$langs->trans('SumValidationOk').'</span>';
} else {
print '<span style="color: #a94442;"><i class="fas fa-exclamation-triangle"></i> '.$langs->trans('Difference').': '.price($totalDiff).' '.$import->currency.'</span>';
}
print '</td>';
} else {
// Not all products matched - show totals but no comparison
print '<td class="right nowraponall">';
if ($hasDolibarrPrices) {
print '<span class="opacitymedium">'.price($totalDolibarrHT).'</span>';
print '<br><span class="opacitymedium small">('.$matchedLinesCount.'/'.$totalLinesCount.')</span>';
} else {
print '<span class="opacitymedium">-</span>';
}
print '</td>';
print '<td class="right"><strong>'.price($totalZugferdHT).'</strong></td>';
print '<td colspan="2" class="nowraponall">';
print '<span class="opacitymedium"><i class="fas fa-info-circle"></i> '.$langs->trans('ProductsNotAssigned').'</span>';
print '</td>';
}
print '</tr>';
print '</table>';
print '</div>';
// Fehlende Lieferantenpreise - konsolidierter Bereich
if (!empty($allMissingPrices)) {
print '<div style="margin-top: 15px; padding: 12px; background-color: #fcf8e3; border: 1px solid #faebcc; border-radius: 4px;">';
print '<div class="titre" style="margin-bottom: 10px; color: #8a6d3b;">';
print '<i class="fas fa-plus-circle paddingright"></i>'.$langs->trans('MissingSupplierPrices');
$totalMissing = 0;
foreach ($allMissingPrices as $mp) {
$totalMissing += count($mp['missing']);
}
print ' <span class="badge badge-warning">'.$totalMissing.'</span>';
print '</div>';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="addmissingprices">';
print '<input type="hidden" name="id" value="'.$import->id.'">';
foreach ($allMissingPrices as $mpData) {
print '<div style="margin-bottom: 10px; padding: 8px; background-color: #fff; border: 1px solid #eee; border-radius: 3px;">';
print '<div style="font-weight: bold; margin-bottom: 5px;">';
print '<i class="fas fa-cube paddingright" style="color: #555;"></i>';
print dol_escape_htmltag($mpData['product_ref'].' - '.$mpData['product_label']);
if ($mpData['current_price'] > 0) {
print ' <span class="opacitymedium">('.price($mpData['current_price']).')</span>';
}
print '</div>';
foreach ($mpData['missing'] as $missing) {
$priceDiffHtml = '';
if ($mpData['current_price'] > 0) {
$pDiff = $missing['purchase_price'] - $mpData['current_price'];
$pDiffPercent = ($pDiff / $mpData['current_price']) * 100;
if ($pDiff < 0) {
$priceDiffHtml = ' <span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.number_format(abs($pDiffPercent), 1).'%</span>';
} elseif ($pDiff > 0) {
$priceDiffHtml = ' <span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($pDiffPercent, 1).'%</span>';
} else {
$priceDiffHtml = ' <span class="opacitymedium">=</span>';
}
}
// Wert: productId,socId,datanormId
$cbValue = $mpData['product_id'].','.$missing['fk_soc'].','.$missing['datanorm_id'];
print '<div style="padding: 3px 0 3px 20px;">';
print '<label style="cursor: pointer; display: block;">';
print '<input type="checkbox" name="add_prices[]" value="'.dol_escape_htmltag($cbValue).'" checked class="cb-missing-price" style="margin-right: 5px;">';
print '<strong>'.dol_escape_htmltag($missing['supplier_name']).'</strong>';
print ' <code style="font-size: 0.9em;">'.dol_escape_htmltag($missing['article_number']).'</code>';
print ' @ <strong>'.price($missing['purchase_price']).'</strong>';
print $priceDiffHtml;
print '</label>';
print '</div>';
}
print '</div>';
}
// Alle auswaehlen / Keine auswaehlen + Submit
print '<div style="margin-top: 8px;">';
print '<a href="#" onclick="$(\'.cb-missing-price\').prop(\'checked\', true); return false;" class="small paddingright">'.$langs->trans('SelectAll').'</a>';
print ' / ';
print '<a href="#" onclick="$(\'.cb-missing-price\').prop(\'checked\', false); return false;" class="small paddingleft">'.$langs->trans('DeselectAll').'</a>';
print ' &nbsp; ';
print '<button type="submit" class="button buttongen">';
print '<i class="fas fa-plus paddingright"></i>'.$langs->trans('AddSelectedPrices');
print '</button>';
print '</div>';
print '</form>';
print '</div>';
}
// Datanorm Preview Section (shown when preview action was triggered)
if (!empty($datanormPreviewMatches)) {
print '<div class="div-table-responsive-no-min" style="margin-top: 20px;">';
print '<div class="titre" style="margin-bottom: 10px;">';
print '<i class="fas fa-database paddingright"></i>'.$langs->trans('DatanormPreview');
print ' <span class="badge badge-info">'.count($datanormPreviewMatches).' '.$langs->trans('Matches').'</span>';
print '</div>';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" id="datanorm_confirm_form">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="createallfromdatanorm">';
print '<input type="hidden" name="id" value="'.$import->id.'">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th class="center" style="width: 30px;"><input type="checkbox" id="checkall_datanorm" checked></th>';
print '<th>'.$langs->trans('SupplierRef').'</th>';
print '<th>'.$langs->trans('InvoiceProductName').'</th>';
print '<th>'.$langs->trans('DatanormProductName').'</th>';
print '<th class="right">'.$langs->trans('InvoicePrice').'</th>';
print '<th class="right">'.$langs->trans('DatanormPrice').'</th>';
print '<th class="right">'.$langs->trans('SellingPrice').'</th>';
print '<th class="center">'.$langs->trans('Action').'</th>';
print '</tr>';
$countCreate = 0;
$countAssign = 0;
foreach ($datanormPreviewMatches as $match) {
$rowClass = ($match['action'] == 'assign') ? 'background-color: #d9edf7;' : 'background-color: #dff0d8;';
print '<tr class="oddeven" style="'.$rowClass.'">';
print '<td class="center"><input type="checkbox" name="selected_lines[]" value="'.$match['line_id'].'" checked></td>';
print '<td><strong>'.$match['datanorm_article_number'].'</strong>';
if (!empty($match['datanorm_ean'])) {
print '<br><span class="small" style="color: #666;">EAN: '.$match['datanorm_ean'].'</span>';
}
print '</td>';
// Invoice product name
print '<td>';
print '<span style="color: #666;">'.dol_trunc($match['line_product_name'], 40).'</span>';
print '</td>';
// Datanorm product name
print '<td>';
print '<strong style="color: #2980b9;">'.dol_trunc($match['datanorm_short_text1'], 40).'</strong>';
if (!empty($match['datanorm_short_text2'])) {
print '<br><span class="small" style="color: #666;">'.dol_trunc($match['datanorm_short_text2'], 40).'</span>';
}
print '</td>';
// Invoice price (from ZUGFeRD)
print '<td class="right nowraponall">';
print '<strong>'.price($match['line_unit_price']).'</strong>';
print '</td>';
// Datanorm price - show original price and calculated unit price
print '<td class="right nowraponall">';
if ($match['datanorm_price_unit'] > 1) {
// Show original price and price unit
print '<span class="small" style="color: #666;">'.price($match['datanorm_price']).'/'.$match['datanorm_price_unit'].'</span>';
print '<br><strong>= '.price($match['purchase_price']).'</strong>';
} else {
print '<strong>'.price($match['purchase_price']).'</strong>';
}
if ($match['copper_surcharge'] > 0) {
print '<br><span class="small" style="color: #d9534f;">+ '.price($match['copper_surcharge']).' Cu</span>';
}
print '</td>';
// Selling price
print '<td class="right nowraponall"><strong style="color: #27ae60;">'.price($match['selling_price']).'</strong></td>';
// Action
print '<td class="center nowraponall">';
if ($match['action'] == 'assign') {
print '<span class="badge badge-info" title="'.$langs->trans('ProductAlreadyExists').'"><i class="fas fa-link"></i> '.$langs->trans('Assign').'</span>';
$countAssign++;
} else {
print '<span class="badge badge-success"><i class="fas fa-plus"></i> '.$langs->trans('Create').'</span>';
print '<br><span class="small" style="color: #666;">'.$match['new_ref'].'</span>';
$countCreate++;
}
print '</td>';
print '</tr>';
// Show supplier alternatives if available (more than 1 supplier found)
if (!empty($match['supplier_alternatives']) && count($match['supplier_alternatives']) > 1) {
print '<tr class="oddeven" style="background-color: #f9f9f9;">';
print '<td></td>'; // Empty checkbox column
print '<td colspan="7" style="padding: 10px;">';
print '<div style="background-color: #fff; border: 1px solid #ddd; border-radius: 5px; padding: 10px;">';
print '<div style="font-weight: bold; margin-bottom: 8px; color: #555;">';
print '<i class="fas fa-store-alt paddingright"></i>'.$langs->trans('SupplierAlternatives');
print ' <span class="badge" style="background-color: #5bc0de;">'.count($match['supplier_alternatives']).' '.$langs->trans('Suppliers').'</span>';
print '</div>';
print '<table class="noborder" style="width: 100%; font-size: 0.9em;">';
print '<tr style="background-color: #f5f5f5;">';
print '<th class="center" style="width: 40px;">'.$langs->trans('Select').'</th>';
print '<th>'.$langs->trans('Supplier').'</th>';
print '<th>'.$langs->trans('SupplierRef').'</th>';
print '<th>'.$langs->trans('EAN').'</th>';
print '<th>'.$langs->trans('ManufacturerRef').'</th>';
print '<th class="right">'.$langs->trans('UnitPrice').'</th>';
print '<th class="right">'.$langs->trans('Difference').'</th>';
print '</tr>';
$lowestPrice = PHP_FLOAT_MAX;
foreach ($match['supplier_alternatives'] as $alt) {
if ($alt['purchase_price'] < $lowestPrice) {
$lowestPrice = $alt['purchase_price'];
}
}
foreach ($match['supplier_alternatives'] as $altIdx => $alt) {
$isInvoiceSupplier = $alt['is_invoice_supplier'];
$isCheapest = ($alt['purchase_price'] == $lowestPrice);
$rowStyle = '';
if ($isInvoiceSupplier) {
$rowStyle = 'background-color: #d9edf7;'; // Blue for invoice supplier
} elseif ($isCheapest) {
$rowStyle = 'background-color: #dff0d8;'; // Green for cheapest
}
print '<tr style="'.$rowStyle.'">';
// Checkbox for selecting this supplier as purchase source
print '<td class="center">';
$checkboxName = 'supplier_prices['.$match['line_id'].']['.$alt['fk_soc'].']';
$checked = $isInvoiceSupplier ? ' checked' : '';
print '<input type="checkbox" name="'.$checkboxName.'" value="'.$alt['datanorm_id'].'"'.$checked.' title="'.$langs->trans('AddAsPurchasePrice').'">';
print '</td>';
// Supplier name
print '<td>';
print '<strong>'.dol_escape_htmltag($alt['supplier_name']).'</strong>';
if ($isInvoiceSupplier) {
print ' <span class="badge" style="background-color: #337ab7; font-size: 0.75em;"><i class="fas fa-file-invoice"></i></span>';
}
if ($isCheapest) {
print ' <span class="badge" style="background-color: #5cb85c; font-size: 0.75em;"><i class="fas fa-tag"></i></span>';
}
print '</td>';
// Supplier article number
print '<td><code>'.dol_escape_htmltag($alt['article_number']).'</code></td>';
// EAN
print '<td>';
if (!empty($alt['ean'])) {
print '<span class="small">'.dol_escape_htmltag($alt['ean']).'</span>';
} else {
print '<span class="opacitymedium">-</span>';
}
print '</td>';
// Manufacturer ref
print '<td>';
if (!empty($alt['manufacturer_ref'])) {
print '<span class="small">'.dol_escape_htmltag($alt['manufacturer_ref']).'</span>';
} else {
print '<span class="opacitymedium">-</span>';
}
print '</td>';
// Price
print '<td class="right nowraponall">';
if ($alt['price_unit'] > 1) {
print '<span class="small" style="color: #666;">'.price($alt['price']).'/'.$alt['price_unit'].'</span><br>';
}
print '<strong>'.price($alt['purchase_price']).'</strong>';
print '</td>';
// Difference from invoice supplier price
print '<td class="right nowraponall">';
if (!$isInvoiceSupplier && isset($match['purchase_price'])) {
$diff = $alt['purchase_price'] - $match['purchase_price'];
$diffPercent = ($match['purchase_price'] > 0) ? ($diff / $match['purchase_price'] * 100) : 0;
if ($diff < 0) {
print '<span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.price(abs($diff)).' ('.number_format(abs($diffPercent), 1).'%)</span>';
} elseif ($diff > 0) {
print '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.price($diff).' (+'.number_format($diffPercent, 1).'%)</span>';
} else {
print '<span class="opacitymedium">=</span>';
}
} else {
print '<span class="opacitymedium">-</span>';
}
print '</td>';
print '</tr>';
}
print '</table>';
print '<div class="small opacitymedium" style="margin-top: 5px;">';
print '<i class="fas fa-info-circle"></i> '.$langs->trans('SelectSuppliersForPurchasePrices');
print '</div>';
print '</div>';
print '</td>';
print '</tr>';
}
}
print '</table>';
// Summary and confirm button
print '<div class="center" style="margin-top: 15px; padding: 15px; background-color: #f5f5f5; border-radius: 5px;">';
print '<div style="margin-bottom: 10px;">';
if ($countCreate > 0) {
print '<span class="badge badge-success" style="margin-right: 10px;"><i class="fas fa-plus"></i> '.$countCreate.' '.$langs->trans('ToCreate').'</span>';
}
if ($countAssign > 0) {
print '<span class="badge badge-info"><i class="fas fa-link"></i> '.$countAssign.' '.$langs->trans('ToAssign').'</span>';
}
print '</div>';
print '<button type="submit" class="button button-primary">';
print '<i class="fas fa-check paddingright"></i>'.$langs->trans('ConfirmAndCreateProducts');
print '</button>';
print ' &nbsp; ';
print '<a href="'.$_SERVER['PHP_SELF'].'?action=edit&id='.$import->id.'" class="button">';
print '<i class="fas fa-times paddingright"></i>'.$langs->trans('Cancel');
print '</a>';
print '</div>';
print '</form>';
print '</div>';
// JavaScript for select all checkbox
print '<script>
$(document).ready(function() {
$("#checkall_datanorm").change(function() {
$("input[name=\'selected_lines[]\']").prop("checked", this.checked);
});
});
</script>';
}
// Action buttons
print '<div class="center" style="margin-top: 20px;">';
if ($allComplete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=createinvoice&id='.$import->id.'&token='.newToken().'" class="button button-primary">';
print '<i class="fas fa-file-invoice paddingright"></i>'.$langs->trans('CreateSupplierInvoice');
print '</a>';
print ' &nbsp; ';
}
// Finish import button - shown when pending status and all products assigned
if ($import->status == ZugferdImport::STATUS_PENDING && $allComplete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=finishimport&id='.$import->id.'&token='.newToken().'" class="button">';
print '<i class="fas fa-check paddingright"></i>'.$langs->trans('FinishImport');
print '</a>';
print ' &nbsp; ';
}
// 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 '<a href="'.$_SERVER['PHP_SELF'].'?action=createallfromdatanorm&id='.$import->id.'&token='.newToken().'" class="button">';
print '<i class="fas fa-database paddingright"></i>'.$langs->trans('AssignAllFromDatanorm');
print '</a>';
print ' &nbsp; ';
// "Datanorm Vorschau" - preview what will be created
print '<a href="'.$_SERVER['PHP_SELF'].'?action=previewdatanorm&id='.$import->id.'&token='.newToken().'" class="button">';
print '<i class="fas fa-search paddingright"></i>'.$langs->trans('PreviewDatanormMatches');
print '</a>';
print ' &nbsp; ';
}
print '<a href="'.dol_buildpath('/importzugferd/list.php', 1).'" class="button">'.$langs->trans('BackToList').'</a>';
// 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 ' &nbsp; ';
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&id='.$import->id.'&token='.newToken().'" class="button button-cancel">';
print '<i class="fas fa-trash paddingright"></i>'.$langs->trans('Delete');
print '</a>';
}
print '</div>';
print '</div>';
// Modal CSS and HTML for raw Datanorm data
print '<style>
.datanorm-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 9999;
}
.datanorm-modal {
position: relative;
background: white;
width: 90%;
max-width: 700px;
max-height: 85vh;
margin: 50px auto;
padding: 20px;
border-radius: 5px;
overflow-y: auto;
}
.datanorm-modal h3 {
margin-top: 0;
color: #333;
}
.datanorm-modal h4 {
margin-bottom: 5px;
color: #555;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
}
.datanorm-modal pre {
background: #f5f5f5;
padding: 10px;
font-size: 11px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.datanorm-modal .close-btn {
float: right;
cursor: pointer;
font-size: 20px;
color: #999;
}
.datanorm-modal .close-btn:hover {
color: #333;
}
</style>';
// Modal for raw data
print '<div id="datanormRawModal" class="datanorm-modal-overlay" onclick="closeRawDatanormModal(event)">';
print '<div class="datanorm-modal" onclick="event.stopPropagation()">';
print '<span class="close-btn" onclick="closeRawDatanormModal()">&times;</span>';
print '<h3><i class="fas fa-file-code"></i> Rohdaten: <span id="modalArticleNumber"></span></h3>';
print '<div id="modalContent">';
print '<p><em>Laden...</em></p>';
print '</div>';
print '</div>';
print '</div>';
print '<script>
function showRawDatanorm(articleNumber, fkSoc) {
document.getElementById("datanormRawModal").style.display = "block";
document.getElementById("modalArticleNumber").textContent = articleNumber;
document.getElementById("modalContent").innerHTML = "<p><em>Laden...</em></p>";
// AJAX request
var xhr = new XMLHttpRequest();
xhr.open("GET", "'.dol_escape_js($_SERVER['PHP_SELF']).'?action=get_raw_lines&article_number=" + encodeURIComponent(articleNumber) + "&fk_soc=" + fkSoc + "&token='.newToken().'", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
var html = "";
html += "<h4>DATANORM (A-Satz)</h4>";
if (data.datanorm_line) {
html += "<pre>" + escapeHtml(data.datanorm_line) + "</pre>";
// Parse and show fields
var parts = data.datanorm_line.split(";");
html += "<table class=\"noborder\" style=\"margin-top:10px;font-size:12px;\">";
html += "<tr><td><b>0: Satzart</b></td><td>" + escapeHtml(parts[0] || "") + "</td></tr>";
html += "<tr><td><b>1: Aktionscode</b></td><td>" + escapeHtml(parts[1] || "") + "</td></tr>";
html += "<tr><td><b>2: Artikelnummer</b></td><td>" + escapeHtml(parts[2] || "") + "</td></tr>";
html += "<tr><td><b>3: Textkennzeichen</b></td><td>" + escapeHtml(parts[3] || "") + "</td></tr>";
html += "<tr><td><b>4: Kurztext 1</b></td><td>" + escapeHtml(parts[4] || "") + "</td></tr>";
html += "<tr><td><b>5: Kurztext 2</b></td><td>" + escapeHtml(parts[5] || "") + "</td></tr>";
html += "<tr><td><b>6: Preiskennzeichen</b></td><td>" + escapeHtml(parts[6] || "") + " (1=Brutto, 2=Netto)</td></tr>";
html += "<tr><td><b>7: PE-Code</b></td><td>" + escapeHtml(parts[7] || "") + " (0=1, 1=10, 2=100, 3=1000)</td></tr>";
html += "<tr><td><b>8: Mengeneinheit</b></td><td>" + escapeHtml(parts[8] || "") + "</td></tr>";
html += "<tr><td><b>9: Preis (Cent)</b></td><td>" + escapeHtml(parts[9] || "") + "</td></tr>";
html += "<tr><td><b>10: Rabattgruppe</b></td><td>" + escapeHtml(parts[10] || "") + "</td></tr>";
html += "<tr><td><b>11: Warengruppe</b></td><td>" + escapeHtml(parts[11] || "") + "</td></tr>";
html += "</table>";
} else {
html += "<p class=\"opacitymedium\">Keine DATANORM-Zeile gefunden</p>";
}
html += "<br><h4>DATPREIS (P-Satz)</h4>";
if (data.datpreis_line) {
html += "<pre>" + escapeHtml(data.datpreis_line) + "</pre>";
// Parse P-Satz
var pparts = data.datpreis_line.split(";");
html += "<table class=\"noborder\" style=\"margin-top:10px;font-size:12px;\">";
html += "<tr><td><b>0: Satzart</b></td><td>" + escapeHtml(pparts[0] || "") + "</td></tr>";
html += "<tr><td><b>1: Aktionscode</b></td><td>" + escapeHtml(pparts[1] || "") + "</td></tr>";
// Find the article in the P-Satz (multiple articles per line)
for (var i = 2; i < pparts.length; i += 9) {
if (pparts[i] && pparts[i].trim() === articleNumber) {
html += "<tr><td colspan=\"2\"><b>--- Artikel " + escapeHtml(pparts[i]) + " ---</b></td></tr>";
html += "<tr><td><b>" + i + ": Artikelnummer</b></td><td>" + escapeHtml(pparts[i] || "") + "</td></tr>";
html += "<tr><td><b>" + (i+1) + ": Preiskennzeichen</b></td><td>" + escapeHtml(pparts[i+1] || "") + " (2=Netto)</td></tr>";
html += "<tr><td><b>" + (i+2) + ": Preis (Cent)</b></td><td>" + escapeHtml(pparts[i+2] || "") + " = " + (parseFloat(pparts[i+2] || 0) / 100).toFixed(2) + " EUR</td></tr>";
html += "<tr><td><b>" + (i+3) + ": Unbekannt</b></td><td>" + escapeHtml(pparts[i+3] || "") + "</td></tr>";
html += "<tr><td><b>" + (i+4) + ": Metallzuschlag (Cent)</b></td><td>" + escapeHtml(pparts[i+4] || "") + " = " + (parseFloat(pparts[i+4] || 0) / 100).toFixed(2) + " EUR</td></tr>";
break;
}
}
html += "</table>";
} else {
html += "<p class=\"opacitymedium\">Keine DATPREIS-Zeile gefunden</p>";
}
if (data.error) {
html += "<br><p class=\"error\">" + escapeHtml(data.error) + "</p>";
}
document.getElementById("modalContent").innerHTML = html;
} catch(e) {
document.getElementById("modalContent").innerHTML = "<p class=\"error\">Fehler beim Parsen: " + e.message + "</p><pre>" + escapeHtml(xhr.responseText) + "</pre>";
}
} else {
document.getElementById("modalContent").innerHTML = "<p class=\"error\">HTTP Fehler: " + xhr.status + "</p>";
}
}
};
xhr.send();
}
function closeRawDatanormModal(event) {
if (!event || event.target.id === "datanormRawModal") {
document.getElementById("datanormRawModal").style.display = "none";
}
}
function escapeHtml(text) {
var div = document.createElement("div");
div.appendChild(document.createTextNode(text || ""));
return div.innerHTML;
}
// Close modal with ESC key
document.addEventListener("keydown", function(e) {
if (e.key === "Escape") {
closeRawDatanormModal();
}
});
</script>';
}
llxFooter();
$db->close();