importzugferd/import.php

2206 lines
100 KiB
PHP
Raw Permalink 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');
// Initialize objects
$form = new Form($db);
$formfile = new FormFile($db);
$actions = new ActionsImportZugferd($db);
$import = new ZugferdImport($db);
$importLine = new ImportLine($db);
$notification = new ImportNotification($db);
$error = 0;
$message = '';
/*
* Actions
*/
// AJAX: Get raw Datanorm lines for debugging
if ($action == 'get_raw_lines' && GETPOST('article_number', 'alphanohtml')) {
header('Content-Type: application/json');
$article_number = GETPOST('article_number', 'alphanohtml');
$ajax_fk_soc = GETPOSTINT('fk_soc');
$result = array(
'datanorm_line' => '',
'datpreis_line' => '',
'article_number' => $article_number
);
// Get the upload directory for this supplier
$upload_dir = $conf->importzugferd->dir_output.'/datanorm/'.$ajax_fk_soc;
if (is_dir($upload_dir)) {
$allFiles = glob($upload_dir . '/*');
// Search in DATANORM files
foreach ($allFiles as $file) {
$basename = strtoupper(basename($file));
if (preg_match('/^DATANORM\.\d{3}$/', $basename)) {
$handle = fopen($file, 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
// A-Satz starts with A; and contains the article number
if (preg_match('/^A;/', $line)) {
$parts = explode(';', $line);
if (isset($parts[2]) && trim($parts[2]) == $article_number) {
$result['datanorm_line'] = trim($line);
break;
}
}
}
fclose($handle);
}
if (!empty($result['datanorm_line'])) break;
}
}
// Search in DATPREIS files
foreach ($allFiles as $file) {
$basename = strtoupper(basename($file));
if (preg_match('/^DATPREIS\.\d{3}$/', $basename)) {
$handle = fopen($file, 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
// P-Satz contains article numbers at various positions
if (preg_match('/^P;/', $line) && strpos($line, $article_number) !== false) {
$result['datpreis_line'] = trim($line);
break;
}
}
fclose($handle);
}
if (!empty($result['datpreis_line'])) break;
}
}
$result['upload_dir'] = $upload_dir;
} else {
$result['error'] = 'Upload directory not found: ' . $upload_dir;
}
echo json_encode($result);
exit;
}
// Upload and parse PDF - creates import record immediately
if ($action == 'upload') {
if (!empty($_FILES['zugferd_file']['tmp_name'])) {
$upload_dir = $conf->importzugferd->dir_output.'/temp';
if (!is_dir($upload_dir)) {
dol_mkdir($upload_dir);
}
$filename = dol_sanitizeFileName($_FILES['zugferd_file']['name']);
$destfile = $upload_dir.'/'.$filename;
if (move_uploaded_file($_FILES['zugferd_file']['tmp_name'], $destfile)) {
$force_reimport = GETPOST('force_reimport', 'int');
// Check for duplicate
$file_hash = hash_file('sha256', $destfile);
$isDuplicate = $import->isDuplicate($file_hash);
if ($isDuplicate && !$force_reimport) {
$error++;
$message = $langs->trans('ErrorDuplicateInvoice');
@unlink($destfile);
} else {
// If force reimport, delete the old record first
if ($isDuplicate && $force_reimport) {
$oldImport = new ZugferdImport($db);
$oldImport->fetch(0, null, $file_hash);
if ($oldImport->id > 0) {
// Delete old lines
$oldLines = new ImportLine($db);
$oldLines->deleteAllByImport($oldImport->id);
// Delete old files
$old_dir = $conf->importzugferd->dir_output.'/imports/'.$oldImport->id;
if (is_dir($old_dir)) {
dol_delete_dir_recursive($old_dir);
}
// Delete old import record
$oldImport->delete($user);
}
}
// Parse the file
$parser = new ZugferdParser($db);
$res = $parser->extractFromPdf($destfile);
if ($res > 0) {
$res = $parser->parse();
if ($res > 0) {
$parsed_data = $parser->getInvoiceData();
// Create import record immediately
$import->invoice_number = $parsed_data['invoice_number'];
$import->invoice_date = $parsed_data['invoice_date'];
$import->seller_name = $parsed_data['seller']['name'];
$import->seller_vat = $parsed_data['seller']['vat_id'];
$import->buyer_reference = $parsed_data['buyer']['reference'] ?: $parsed_data['buyer']['id'];
$import->total_ht = $parsed_data['totals']['net'];
$import->total_ttc = $parsed_data['totals']['gross'];
$import->currency = $parsed_data['totals']['currency'];
$import->xml_content = $parser->getXmlContent();
$import->pdf_filename = $filename;
$import->file_hash = $file_hash;
// Find supplier
$supplier_id = $actions->findSupplier($parsed_data);
$import->fk_soc = $supplier_id;
// Process line items to find products
$processed_lines = $actions->processLineItems($parsed_data['lines'], $supplier_id);
// Check if all lines have products
$all_have_products = true;
$has_any_product = false;
$total_lines = count($processed_lines);
foreach ($processed_lines as $line) {
if ($line['fk_product'] <= 0) {
$all_have_products = false;
} else {
$has_any_product = true;
}
}
// Set status based on product matching
// STATUS_IMPORTED only if: supplier found, has lines, ALL lines have products
if ($all_have_products && $supplier_id > 0 && $total_lines > 0 && $has_any_product) {
$import->status = ZugferdImport::STATUS_IMPORTED;
} else {
$import->status = ZugferdImport::STATUS_PENDING;
}
$import->date_creation = dol_now();
$result = $import->create($user);
if ($result > 0) {
// Store line items in database
foreach ($processed_lines as $line) {
$importLineObj = new ImportLine($db);
$importLineObj->fk_import = $import->id;
$importLineObj->line_id = $line['line_id'];
$importLineObj->supplier_ref = $line['supplier_ref'];
$importLineObj->product_name = $line['name'];
$importLineObj->description = $line['description'];
$importLineObj->quantity = $line['quantity'];
$importLineObj->unit_code = $line['unit_code'];
$importLineObj->unit_price = $line['unit_price'];
$importLineObj->unit_price_raw = $line['unit_price_raw'];
$importLineObj->basis_quantity = $line['basis_quantity'];
$importLineObj->basis_quantity_unit = $line['basis_quantity_unit'];
$importLineObj->line_total = $line['line_total'];
$importLineObj->tax_percent = $line['tax_percent'];
$importLineObj->ean = $line['ean'];
$importLineObj->fk_product = $line['fk_product'];
$importLineObj->match_method = $line['match_method'];
$importLineObj->create($user);
}
// Move PDF to permanent storage
$final_dir = $conf->importzugferd->dir_output.'/imports/'.$import->id;
if (!is_dir($final_dir)) {
dol_mkdir($final_dir);
}
rename($destfile, $final_dir.'/'.$filename);
// Send notification if manual intervention required
if ($import->status == ZugferdImport::STATUS_PENDING) {
$storedLines = $importLine->fetchAllByImport($import->id);
$notification->sendManualInterventionNotification($import, $storedLines);
}
// Check for price differences
if ($import->status == ZugferdImport::STATUS_IMPORTED) {
$storedLines = $importLine->fetchAllByImport($import->id);
$notification->checkAndNotifyPriceDifferences($import, $storedLines);
}
// Redirect to edit page
$id = $import->id;
$action = 'edit';
setEventMessages($langs->trans('ImportRecordCreated'), null, 'mesgs');
} else {
$error++;
$message = $import->error;
@unlink($destfile);
// Send error notification
$notification->sendErrorNotification($import, $message, $filename);
}
} else {
$error++;
$message = $parser->error;
@unlink($destfile);
}
} else {
$error++;
$message = $parser->error;
@unlink($destfile);
}
}
} else {
$error++;
$message = $langs->trans('ErrorFileUploadFailed');
}
} else {
$error++;
$message = $langs->trans('ErrorNoFileUploaded');
}
}
// Load existing import for editing
if ($id > 0 && empty($action)) {
$action = 'edit';
}
if ($action == 'edit' && $id > 0) {
$result = $import->fetch($id);
if ($result <= 0) {
$error++;
$message = $langs->trans('ErrorRecordNotFound');
$action = '';
}
}
// Assign product to line
if ($action == 'assignproduct' && $line_id > 0 && $product_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
$lineObj->setProduct($product_id, $langs->trans('ManualAssignment'), $user);
setEventMessages($langs->trans('ProductAssigned'), null, 'mesgs');
// Get import ID to reload
$id = $lineObj->fk_import;
// Check if all lines now have products
$allHaveProducts = $importLine->allLinesHaveProducts($id);
if ($allHaveProducts) {
// Update import status
$import->fetch($id);
if ($import->status == ZugferdImport::STATUS_PENDING) {
$import->status = ZugferdImport::STATUS_IMPORTED;
$import->update($user);
// Check for price differences now that all products are assigned
$storedLines = $importLine->fetchAllByImport($id);
$notification->checkAndNotifyPriceDifferences($import, $storedLines);
}
}
}
$action = 'edit';
$import->fetch($id);
}
// Remove product assignment from line
if ($action == 'removeproduct' && $line_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
$id = $lineObj->fk_import;
$lineObj->setProduct(0, '', $user);
setEventMessages($langs->trans('ProductRemoved'), null, 'mesgs');
// Update import status to pending
$import->fetch($id);
if ($import->status == ZugferdImport::STATUS_IMPORTED) {
$import->status = ZugferdImport::STATUS_PENDING;
$import->update($user);
}
}
$action = 'edit';
$import->fetch($id);
}
// Update supplier
if ($action == 'setsupplier' && $id > 0) {
$import->fetch($id);
$import->fk_soc = $supplier_id;
$import->update($user);
setEventMessages($langs->trans('SupplierUpdated'), null, 'mesgs');
$action = 'edit';
}
// Duplicate product from template
if ($action == 'duplicateproduct' && $template_product_id > 0 && $line_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
// Load template product
$template = new Product($db);
if ($template->fetch($template_product_id) > 0) {
// Create new product as copy
$newproduct = new Product($db);
// Copy basic properties from template
$newproduct->type = $template->type;
$newproduct->status = $template->status;
$newproduct->status_buy = $template->status_buy;
$newproduct->status_batch = $template->status_batch;
$newproduct->fk_product_type = $template->fk_product_type;
$newproduct->price = $lineObj->unit_price;
$newproduct->price_base_type = 'HT';
$newproduct->tva_tx = $lineObj->tax_percent ?: $template->tva_tx;
$newproduct->weight = $template->weight;
$newproduct->weight_units = $template->weight_units;
$newproduct->fk_unit = $template->fk_unit;
// Set label from ZUGFeRD
$newproduct->label = $lineObj->product_name;
// Generate unique ref
$newproduct->ref = 'NEW-'.dol_print_date(dol_now(), '%Y%m%d%H%M%S');
// Build description with ZUGFeRD data
$zugferd_info = '';
if (!empty($lineObj->supplier_ref)) {
$zugferd_info .= $langs->trans('SupplierRef').': '.$lineObj->supplier_ref."\n";
}
if (!empty($lineObj->unit_code)) {
$zugferd_info .= $langs->trans('Unit').': '.zugferdGetUnitLabel($lineObj->unit_code)."\n";
}
if (!empty($lineObj->ean)) {
$zugferd_info .= 'EAN: '.$lineObj->ean."\n";
}
$zugferd_info .= "---\n";
$newproduct->description = $zugferd_info . ($template->description ?: '');
// Create the product
$result = $newproduct->create($user);
if ($result > 0) {
setEventMessages($langs->trans('ProductCreated'), null, 'mesgs');
// Redirect to product card for editing
header('Location: '.DOL_URL_ROOT.'/product/card.php?id='.$result);
exit;
} else {
setEventMessages($newproduct->error, $newproduct->errors, 'errors');
}
}
$id = $lineObj->fk_import;
}
$action = 'edit';
$import->fetch($id);
}
// Create product from Datanorm
if ($action == 'createfromdatanorm' && $line_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
$id = $lineObj->fk_import;
$import->fetch($id);
// Get Datanorm settings
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
// Search in Datanorm database
$datanorm = new Datanorm($db);
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
if (empty($results)) {
// Try with EAN if available
if (!empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
}
}
if (!empty($results)) {
$datanormArticle = $results[0];
$datanorm->fetch($datanormArticle['id']);
// Load supplier for ref prefix
$supplier = new Societe($db);
$supplier->fetch($import->fk_soc);
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
// Create new product
$newproduct = new Product($db);
$newproduct->type = 0; // Product
$newproduct->status = 1; // On sale
$newproduct->status_buy = 1; // On purchase
// Generate reference
$newproduct->ref = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
// Set default accounting codes from module settings
$newproduct->accountancy_code_sell = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL', '');
$newproduct->accountancy_code_sell_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA', '');
$newproduct->accountancy_code_sell_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT', '');
$newproduct->accountancy_code_buy = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY', '');
$newproduct->accountancy_code_buy_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA', '');
$newproduct->accountancy_code_buy_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT', '');
// Label from Datanorm
$newproduct->label = $datanorm->short_text1;
if (!empty($datanorm->short_text2) && strlen($newproduct->label) < 100) {
$newproduct->label .= ' '.$datanorm->short_text2;
}
// Description
$newproduct->description = $datanorm->getFullDescription();
// Prices
$purchasePrice = $datanorm->price;
if ($datanorm->price_unit > 1) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
// Get copper surcharge for selling price calculation
// Priority: 1. Datanorm, 2. ZUGFeRD
$copperSurchargeForPrice = 0;
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$copperSurchargeForPrice = $datanorm->metal_surcharge;
// Normalize to per-unit if basis quantity differs
if ($datanorm->price_unit > 1) {
$copperSurchargeForPrice = $datanorm->metal_surcharge / $datanorm->price_unit;
}
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
$copperSurchargeForPrice = $lineObj->copper_surcharge;
// Normalize to per-unit if basis quantity differs
if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) {
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty;
} elseif (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->basis_quantity;
}
}
// Selling price with markup: (purchase price + copper surcharge) × (1 + markup%)
$sellingPrice = ($purchasePrice + $copperSurchargeForPrice) * (1 + $markup / 100);
$newproduct->price = $sellingPrice;
$newproduct->price_base_type = 'HT';
$newproduct->tva_tx = $lineObj->tax_percent ?: 19;
// Weight if available
if (!empty($datanorm->weight)) {
$newproduct->weight = $datanorm->weight;
$newproduct->weight_units = 0; // kg
}
// Let Dolibarr auto-generate barcode if configured
// Setting barcode to '-1' triggers automatic generation
if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
$newproduct->barcode = '-1';
}
// Create the product
$result = $newproduct->create($user);
if ($result > 0) {
// Add supplier price
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
$prodfourn = new ProductFournisseur($db);
$prodfourn->id = $newproduct->id;
$prodfourn->fourn_ref = $datanorm->article_number;
// Determine EAN for supplier price
$supplierEan = '';
$supplierEanType = 0;
if (!empty($datanorm->ean)) {
$supplierEan = $datanorm->ean;
$supplierEanType = 2; // EAN13
} elseif (!empty($lineObj->ean)) {
$supplierEan = $lineObj->ean;
$supplierEanType = 2; // EAN13
}
// Prepare extrafields for supplier price
$supplierPriceExtrafields = array();
// Kupferzuschlag (metal surcharge) - händlerspezifisch
// Priorität: 1. Datanorm, 2. ZUGFeRD Import Line
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$supplierPriceExtrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge;
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
// Kupferzuschlag aus ZUGFeRD-Rechnung
$supplierPriceExtrafields['options_kupferzuschlag'] = $lineObj->copper_surcharge;
}
// Preiseinheit - händlerspezifisch
// Priorität: 1. Datanorm, 2. ZUGFeRD basis_quantity
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
$supplierPriceExtrafields['options_preiseinheit'] = $datanorm->price_unit;
} elseif (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
// Preiseinheit aus ZUGFeRD-Rechnung (z.B. 100 für "pro 100 Meter")
$supplierPriceExtrafields['options_preiseinheit'] = $lineObj->basis_quantity;
}
// Warengruppe aus Datanorm
if (!empty($datanorm->product_group)) {
$supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group;
}
// Add supplier price entry with EAN and extrafields
$res = $prodfourn->update_buyprice(
1, // Quantity
$purchasePrice, // Price
$user,
'HT', // Price base
$supplier, // Supplier
0, // Availability
$datanorm->article_number, // Supplier ref
$lineObj->tax_percent ?: 19, // VAT
0, // Charges
0, // Remise
0, // Remise percentage
0, // No price minimum
0, // Delivery delay
0, // Reputation
array(), // Localtaxes array
'', // Default VAT code
0, // Multicurrency price
'HT', // Multicurrency price base type
1, // Multicurrency tx
'', // Multicurrency code
trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : '')), // Description from Datanorm
$supplierEan, // Barcode/EAN in supplier price
$supplierEanType, // Barcode type (EAN13)
$supplierPriceExtrafields // Extra fields (kupferzuschlag, preiseinheit)
);
// Manually ensure extrafields record exists for supplier price
// (Dolibarr update_buyprice doesn't always create it properly)
$sqlGetPrice = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlGetPrice .= " WHERE fk_product = ".(int)$newproduct->id;
$sqlGetPrice .= " AND fk_soc = ".(int)$supplier->id;
$sqlGetPrice .= " ORDER BY rowid DESC LIMIT 1";
$resGetPrice = $db->query($sqlGetPrice);
if ($resGetPrice && $db->num_rows($resGetPrice) > 0) {
$objPrice = $db->fetch_object($resGetPrice);
$priceId = $objPrice->rowid;
// Check if extrafields record exists
$sqlCheckExtra = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sqlCheckExtra .= " WHERE fk_object = ".(int)$priceId;
$resCheckExtra = $db->query($sqlCheckExtra);
if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) {
// Insert extrafields record
$kupferzuschlag = !empty($supplierPriceExtrafields['options_kupferzuschlag']) ? (float)$supplierPriceExtrafields['options_kupferzuschlag'] : 'NULL';
$preiseinheit = !empty($supplierPriceExtrafields['options_preiseinheit']) ? (int)$supplierPriceExtrafields['options_preiseinheit'] : 1;
$warengruppe = !empty($supplierPriceExtrafields['options_warengruppe']) ? "'".$db->escape($supplierPriceExtrafields['options_warengruppe'])."'" : 'NULL';
$sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sqlInsertExtra .= " (fk_object, kupferzuschlag, preiseinheit, warengruppe) VALUES (";
$sqlInsertExtra .= (int)$priceId.", ";
$sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
$sqlInsertExtra .= $preiseinheit.", ";
$sqlInsertExtra .= $warengruppe.")";
$db->query($sqlInsertExtra);
}
}
// Create product mapping for future imports
$mapping = new ProductMapping($db);
$mapping->fk_soc = $import->fk_soc;
$mapping->supplier_ref = $datanorm->article_number;
$mapping->fk_product = $newproduct->id;
$mapping->ean = $datanorm->ean;
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
$mapping->description = $datanorm->short_text1;
$mapping->create($user);
// Assign to import line
$lineObj->setProduct($newproduct->id, 'datanorm', $user);
setEventMessages($langs->trans('ProductCreatedFromDatanorm', $newproduct->ref), null, 'mesgs');
// Check if all lines now have products
$allHaveProducts = $importLine->allLinesHaveProducts($id);
if ($allHaveProducts) {
$import->status = ZugferdImport::STATUS_IMPORTED;
$import->update($user);
}
} else {
setEventMessages($newproduct->error, $newproduct->errors, 'errors');
}
} else {
setEventMessages($langs->trans('DatanormArticleNotFound', $lineObj->supplier_ref), null, 'errors');
}
}
$action = 'edit';
$import->fetch($id);
}
// "Alle zuordnen" - Assign Datanorm matches to import lines
if ($action == 'assignallfromdatanorm' && $id > 0) {
$import->fetch($id);
if ($import->fk_soc > 0) {
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
// Get all lines without product
$lines = $importLine->fetchAllByImport($import->id);
$datanorm = new Datanorm($db);
$mapping = new ProductMapping($db);
$assignedCount = 0;
$datanormFoundCount = 0;
foreach ($lines as $lineObj) {
// Skip lines that already have a product
if ($lineObj->fk_product > 0) {
continue;
}
// Skip lines without supplier_ref
if (empty($lineObj->supplier_ref)) {
continue;
}
// Search in Datanorm database
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
if (empty($results) && !empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
}
if (!empty($results)) {
$datanormFoundCount++;
$datanormMatch = $results[0];
// Get Datanorm ID and article number (array access)
$datanormId = isset($datanormMatch['id']) ? $datanormMatch['id'] : (isset($datanormMatch['rowid']) ? $datanormMatch['rowid'] : 0);
$articleNumber = isset($datanormMatch['article_number']) ? $datanormMatch['article_number'] : '';
// Check if product already exists for this supplier ref
$existingProductId = $mapping->findProductBySupplierRef($import->fk_soc, $articleNumber);
if ($existingProductId > 0) {
// Product exists - assign both product and Datanorm to the line
$lineObj->fk_product = $existingProductId;
$lineObj->fk_datanorm = $datanormId;
$lineObj->match_method = 'datanorm_assign';
$lineObj->update($user);
$assignedCount++;
} else {
// No product yet - save Datanorm reference for later product creation
$lineObj->fk_datanorm = $datanormId;
$lineObj->match_method = 'datanorm_pending';
$lineObj->update($user);
}
}
}
if ($assignedCount > 0) {
setEventMessages($langs->trans('ProductsAssignedFromDatanorm', $assignedCount), null, 'mesgs');
}
if ($datanormFoundCount > $assignedCount) {
$pendingCount = $datanormFoundCount - $assignedCount;
setEventMessages($langs->trans('DatanormMatchesFoundNotAssigned', $pendingCount), null, 'mesgs');
}
if ($datanormFoundCount == 0) {
setEventMessages($langs->trans('DatanormBatchNoMatches'), null, 'warnings');
}
}
header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&id='.$id.'&token='.newToken());
exit;
}
// Create ALL products from Datanorm (batch)
if ($action == 'createallfromdatanorm' && $id > 0) {
$import->fetch($id);
if ($import->fk_soc > 0) {
// Get Datanorm settings
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
// Load supplier
$supplier = new Societe($db);
$supplier->fetch($import->fk_soc);
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
// Get all lines without product
$lines = $importLine->fetchAllByImport($import->id);
$datanorm = new Datanorm($db);
$createdCount = 0;
$assignedCount = 0;
$errorCount = 0;
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
foreach ($lines as $lineObj) {
// Skip lines that already have a product
if ($lineObj->fk_product > 0) {
continue;
}
// Skip lines without supplier_ref
if (empty($lineObj->supplier_ref)) {
continue;
}
// Search in Datanorm database
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
if (empty($results) && !empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
}
if (!empty($results)) {
$datanormArticle = $results[0];
$datanorm->fetch($datanormArticle['id']);
$purchasePrice = $datanorm->price;
if ($datanorm->price_unit > 1) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
// Get copper surcharge for selling price calculation
// Priority: 1. Datanorm, 2. ZUGFeRD
$copperSurchargeForPrice = 0;
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$copperSurchargeForPrice = $datanorm->metal_surcharge;
// Normalize to per-unit if price_unit > 1
if ($datanorm->price_unit > 1) {
$copperSurchargeForPrice = $datanorm->metal_surcharge / $datanorm->price_unit;
}
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
$copperSurchargeForPrice = $lineObj->copper_surcharge;
// Normalize based on copper_surcharge_basis_qty
if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) {
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty;
}
}
// Check if product already exists in Dolibarr
$existingProduct = new Product($db);
$productExists = false;
$existingProductId = 0;
// 1. Check by supplier reference (ProductFournisseur)
$sqlCheck = "SELECT DISTINCT pf.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pf";
$sqlCheck .= " WHERE pf.fk_soc = ".(int)$import->fk_soc;
$sqlCheck .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'";
$sqlCheck .= " AND pf.entity IN (".getEntity('product').")";
$resqlCheck = $db->query($sqlCheck);
if ($resqlCheck && $db->num_rows($resqlCheck) > 0) {
$objCheck = $db->fetch_object($resqlCheck);
$existingProductId = $objCheck->fk_product;
$productExists = true;
}
// 2. Check by product reference pattern
if (!$productExists) {
$expectedRef = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
$fetchResult = $existingProduct->fetch(0, $expectedRef);
if ($fetchResult > 0) {
$existingProductId = $existingProduct->id;
$productExists = true;
}
}
// 3. Check by EAN if available
if (!$productExists && !empty($datanorm->ean)) {
$sqlEan = "SELECT rowid FROM ".MAIN_DB_PREFIX."product";
$sqlEan .= " WHERE barcode = '".$db->escape($datanorm->ean)."'";
$sqlEan .= " AND entity IN (".getEntity('product').")";
$resqlEan = $db->query($sqlEan);
if ($resqlEan && $db->num_rows($resqlEan) > 0) {
$objEan = $db->fetch_object($resqlEan);
$existingProductId = $objEan->rowid;
$productExists = true;
}
}
if ($productExists && $existingProductId > 0) {
// Product exists - just assign it to the line
$lineObj->setProduct($existingProductId, 'datanorm', $user);
$assignedCount++;
} else {
// Create new product
$newproduct = new Product($db);
$newproduct->type = 0;
$newproduct->status = 1;
$newproduct->status_buy = 1;
$newproduct->ref = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
$newproduct->label = $datanorm->short_text1;
if (!empty($datanorm->short_text2) && strlen($newproduct->label) < 100) {
$newproduct->label .= ' '.$datanorm->short_text2;
}
$newproduct->description = $datanorm->getFullDescription();
// Set default accounting codes from module settings
$newproduct->accountancy_code_sell = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL', '');
$newproduct->accountancy_code_sell_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA', '');
$newproduct->accountancy_code_sell_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT', '');
$newproduct->accountancy_code_buy = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY', '');
$newproduct->accountancy_code_buy_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA', '');
$newproduct->accountancy_code_buy_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT', '');
// Selling price: (purchase price + copper surcharge) × (1 + markup%)
$sellingPrice = ($purchasePrice + $copperSurchargeForPrice) * (1 + $markup / 100);
$newproduct->price = $sellingPrice;
$newproduct->price_base_type = 'HT';
$newproduct->tva_tx = $lineObj->tax_percent ?: 19;
if (!empty($datanorm->weight)) {
$newproduct->weight = $datanorm->weight;
$newproduct->weight_units = 0;
}
if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
$newproduct->barcode = '-1';
}
$result = $newproduct->create($user);
if ($result > 0) {
// Add supplier price
$prodfourn = new ProductFournisseur($db);
$prodfourn->id = $newproduct->id;
$prodfourn->fourn_ref = $datanorm->article_number;
$supplierEan = '';
$supplierEanType = 0;
if (!empty($datanorm->ean)) {
$supplierEan = $datanorm->ean;
$supplierEanType = 2;
} elseif (!empty($lineObj->ean)) {
$supplierEan = $lineObj->ean;
$supplierEanType = 2;
}
// Prepare extrafields for supplier price
$supplierPriceExtrafields = array();
// Kupferzuschlag - Priorität: 1. Datanorm, 2. ZUGFeRD
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$supplierPriceExtrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge;
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
$supplierPriceExtrafields['options_kupferzuschlag'] = $lineObj->copper_surcharge;
}
// Preiseinheit - Priorität: 1. Datanorm, 2. ZUGFeRD basis_quantity
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
$supplierPriceExtrafields['options_preiseinheit'] = $datanorm->price_unit;
} elseif (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
$supplierPriceExtrafields['options_preiseinheit'] = $lineObj->basis_quantity;
}
// Warengruppe aus Datanorm
if (!empty($datanorm->product_group)) {
$supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group;
}
$prodfourn->update_buyprice(
1, // Quantity
$purchasePrice, // Price
$user,
'HT', // Price base
$supplier, // Supplier
0, // Availability
$datanorm->article_number, // Supplier ref
$lineObj->tax_percent ?: 19, // VAT
0, // Charges
0, // Remise
0, // Remise percentage
0, // No price minimum
0, // Delivery delay
0, // Reputation
array(), // Localtaxes array
'', // Default VAT code
0, // Multicurrency price
'HT', // Multicurrency price base type
1, // Multicurrency tx
'', // Multicurrency code
trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : '')), // Description from Datanorm
$supplierEan, // Barcode/EAN
$supplierEanType, // Barcode type
$supplierPriceExtrafields // Extra fields
);
// Manually ensure extrafields record exists for supplier price
// (Dolibarr update_buyprice doesn't always create it properly)
$sqlGetPrice = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlGetPrice .= " WHERE fk_product = ".(int)$newproduct->id;
$sqlGetPrice .= " AND fk_soc = ".(int)$supplier->id;
$sqlGetPrice .= " ORDER BY rowid DESC LIMIT 1";
$resGetPrice = $db->query($sqlGetPrice);
if ($resGetPrice && $db->num_rows($resGetPrice) > 0) {
$objPrice = $db->fetch_object($resGetPrice);
$priceId = $objPrice->rowid;
// Check if extrafields record exists
$sqlCheckExtra = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sqlCheckExtra .= " WHERE fk_object = ".(int)$priceId;
$resCheckExtra = $db->query($sqlCheckExtra);
if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) {
// Insert extrafields record
$kupferzuschlag = !empty($supplierPriceExtrafields['options_kupferzuschlag']) ? (float)$supplierPriceExtrafields['options_kupferzuschlag'] : 'NULL';
$preiseinheit = !empty($supplierPriceExtrafields['options_preiseinheit']) ? (int)$supplierPriceExtrafields['options_preiseinheit'] : 1;
$warengruppe = !empty($supplierPriceExtrafields['options_warengruppe']) ? "'".$db->escape($supplierPriceExtrafields['options_warengruppe'])."'" : 'NULL';
$sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sqlInsertExtra .= " (fk_object, kupferzuschlag, preiseinheit, warengruppe) VALUES (";
$sqlInsertExtra .= (int)$priceId.", ";
$sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
$sqlInsertExtra .= $preiseinheit.", ";
$sqlInsertExtra .= $warengruppe.")";
$db->query($sqlInsertExtra);
}
}
// Create product mapping
$mapping = new ProductMapping($db);
$mapping->fk_soc = $import->fk_soc;
$mapping->supplier_ref = $datanorm->article_number;
$mapping->fk_product = $newproduct->id;
$mapping->ean = $datanorm->ean;
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
$mapping->description = $datanorm->short_text1;
$mapping->create($user);
// Assign to import line
$lineObj->setProduct($newproduct->id, 'datanorm', $user);
$createdCount++;
} else {
$errorCount++;
}
}
}
}
if ($createdCount > 0) {
setEventMessages($langs->trans('DatanormBatchCreated', $createdCount), null, 'mesgs');
}
if ($assignedCount > 0) {
setEventMessages($langs->trans('DatanormBatchAssigned', $assignedCount), null, 'mesgs');
}
if ($errorCount > 0) {
setEventMessages($langs->trans('DatanormBatchErrors', $errorCount), null, 'warnings');
}
if ($createdCount == 0 && $assignedCount == 0 && $errorCount == 0) {
setEventMessages($langs->trans('DatanormBatchNoMatches'), null, 'warnings');
}
// Check if all lines now have products
$allHaveProducts = $importLine->allLinesHaveProducts($id);
if ($allHaveProducts) {
$import->status = ZugferdImport::STATUS_IMPORTED;
$import->update($user);
}
}
$action = 'edit';
$import->fetch($id);
}
// Preview Datanorm matches (step 1 - show what will be created)
$datanormPreviewMatches = array();
if ($action == 'previewdatanorm' && $id > 0) {
$import->fetch($id);
if ($import->fk_soc > 0) {
// Get Datanorm settings
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
// Load supplier
$supplier = new Societe($db);
$supplier->fetch($import->fk_soc);
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
// Get all lines without product
$lines = $importLine->fetchAllByImport($import->id);
$datanorm = new Datanorm($db);
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
foreach ($lines as $lineObj) {
// Skip lines that already have a product
if ($lineObj->fk_product > 0) {
continue;
}
// Skip lines without supplier_ref
if (empty($lineObj->supplier_ref)) {
continue;
}
// Search in Datanorm database
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
if (empty($results) && !empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
}
if (!empty($results)) {
$datanormArticle = $results[0];
$datanorm->fetch($datanormArticle['id']);
$purchasePrice = $datanorm->price;
if ($datanorm->price_unit > 1) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
// Get copper surcharge for selling price calculation
$copperSurchargeForPrice = 0;
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$copperSurchargeForPrice = $datanorm->metal_surcharge;
if ($datanorm->price_unit > 1) {
$copperSurchargeForPrice = $datanorm->metal_surcharge / $datanorm->price_unit;
}
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
$copperSurchargeForPrice = $lineObj->copper_surcharge;
if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) {
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty;
}
}
// Calculate selling price
$sellingPrice = ($purchasePrice + $copperSurchargeForPrice) * (1 + $markup / 100);
// Check if product already exists in Dolibarr
$existingProductId = 0;
$productAction = 'create'; // 'create' or 'assign'
// 1. Check by supplier reference (ProductFournisseur)
$sqlCheck = "SELECT DISTINCT pf.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pf";
$sqlCheck .= " WHERE pf.fk_soc = ".(int)$import->fk_soc;
$sqlCheck .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'";
$sqlCheck .= " AND pf.entity IN (".getEntity('product').")";
$resqlCheck = $db->query($sqlCheck);
if ($resqlCheck && $db->num_rows($resqlCheck) > 0) {
$objCheck = $db->fetch_object($resqlCheck);
$existingProductId = $objCheck->fk_product;
$productAction = 'assign';
}
// 2. Check by product reference pattern
if ($existingProductId <= 0) {
$expectedRef = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
$existingProduct = new Product($db);
$fetchResult = $existingProduct->fetch(0, $expectedRef);
if ($fetchResult > 0) {
$existingProductId = $existingProduct->id;
$productAction = 'assign';
}
}
// 3. Check by EAN if available
if ($existingProductId <= 0 && !empty($datanorm->ean)) {
$sqlEan = "SELECT rowid FROM ".MAIN_DB_PREFIX."product";
$sqlEan .= " WHERE barcode = '".$db->escape($datanorm->ean)."'";
$sqlEan .= " AND entity IN (".getEntity('product').")";
$resqlEan = $db->query($sqlEan);
if ($resqlEan && $db->num_rows($resqlEan) > 0) {
$objEan = $db->fetch_object($resqlEan);
$existingProductId = $objEan->rowid;
$productAction = 'assign';
}
}
// Store match info for preview
$datanormPreviewMatches[] = array(
'line_id' => $lineObj->id,
'line_supplier_ref' => $lineObj->supplier_ref,
'line_product_name' => $lineObj->product_name,
'line_quantity' => $lineObj->quantity,
'line_unit_price' => $lineObj->unit_price,
'datanorm_id' => $datanorm->id,
'datanorm_article_number' => $datanorm->article_number,
'datanorm_short_text1' => $datanorm->short_text1,
'datanorm_short_text2' => $datanorm->short_text2,
'datanorm_price' => $datanorm->price,
'datanorm_price_unit' => $datanorm->price_unit,
'datanorm_ean' => $datanorm->ean,
'purchase_price' => $purchasePrice,
'selling_price' => $sellingPrice,
'copper_surcharge' => $copperSurchargeForPrice,
'existing_product_id' => $existingProductId,
'action' => $productAction,
'new_ref' => 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number
);
}
}
}
$action = 'edit';
}
// Create supplier invoice
if ($action == 'createinvoice' && $id > 0) {
$import->fetch($id);
// Check prerequisites
if ($import->fk_soc <= 0) {
$error++;
setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors');
} else {
// Check all lines have products
$lines = $importLine->fetchAllByImport($id);
$allHaveProducts = true;
foreach ($lines as $line) {
if ($line->fk_product <= 0) {
$allHaveProducts = false;
break;
}
}
if (!$allHaveProducts) {
$error++;
setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors');
} else {
// Load supplier to get default values
$supplier = new Societe($db);
$supplier->fetch($import->fk_soc);
// Create invoice
$invoice = new FactureFournisseur($db);
$invoice->socid = $import->fk_soc;
$invoice->ref_supplier = $import->invoice_number;
$invoice->date = $import->invoice_date;
$invoice->note_private = $langs->trans('ImportedFromZugferd').' ('.$import->ref.')';
// Use supplier default values for payment
$invoice->cond_reglement_id = $supplier->cond_reglement_supplier_id ?: 1;
$invoice->mode_reglement_id = $supplier->mode_reglement_supplier_id ?: 0;
$invoice->fk_account = $supplier->fk_account ?: 0;
$db->begin();
$result = $invoice->create($user);
if ($result > 0) {
// Add lines
foreach ($lines as $line) {
$res = $invoice->addline(
$line->product_name,
$line->unit_price,
$line->tax_percent,
0, 0,
$line->quantity,
$line->fk_product,
0, '', '',
0, 0,
'HT', // price_base_type - Netto-Preise aus ZUGFeRD
0 // type (0=product)
);
if ($res < 0) {
$error++;
setEventMessages($invoice->error, $invoice->errors, 'errors');
break;
}
// Update EAN on product if not set
if (!empty($line->ean) && $line->fk_product > 0) {
$product = new Product($db);
$product->fetch($line->fk_product);
if (empty($product->barcode)) {
$product->barcode = $line->ean;
$product->barcode_type = 2; // EAN13
$product->update($product->id, $user);
}
}
}
if (!$error) {
// Invoice stays as draft - user can validate manually
// Copy PDF to invoice
$source_pdf = $conf->importzugferd->dir_output.'/imports/'.$import->id.'/'.$import->pdf_filename;
if (file_exists($source_pdf)) {
$dest_dir = $conf->fournisseur->facture->dir_output.'/'.get_exdir($invoice->id, 2, 0, 0, $invoice, 'invoice_supplier').$invoice->ref;
if (!is_dir($dest_dir)) {
dol_mkdir($dest_dir);
}
copy($source_pdf, $dest_dir.'/'.$import->pdf_filename);
}
// Update import record
$import->fk_facture_fourn = $invoice->id;
$import->status = ZugferdImport::STATUS_PROCESSED;
$import->date_import = dol_now();
$import->update($user);
$db->commit();
setEventMessages($langs->trans('InvoiceCreatedSuccessfully'), null, 'mesgs');
// Redirect to invoice
header('Location: '.DOL_URL_ROOT.'/fourn/facture/card.php?facid='.$invoice->id);
exit;
} else {
$db->rollback();
}
} else {
$error++;
setEventMessages($invoice->error, $invoice->errors, 'errors');
$db->rollback();
}
}
}
$action = 'edit';
}
// Finish import - check for existing invoice and update status
if ($action == 'finishimport' && $id > 0) {
$import->fetch($id);
// Check all lines have products
$lines = $importLine->fetchAllByImport($id);
$allHaveProducts = true;
foreach ($lines as $line) {
if ($line->fk_product <= 0) {
$allHaveProducts = false;
break;
}
}
if (!$allHaveProducts) {
$error++;
setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors');
} elseif ($import->fk_soc <= 0) {
$error++;
setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors');
} else {
// Search for existing supplier invoice with this ref_supplier
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_fourn";
$sql .= " WHERE fk_soc = ".((int) $import->fk_soc);
$sql .= " AND ref_supplier = '".$db->escape($import->invoice_number)."'";
$sql .= " LIMIT 1";
$resql = $db->query($sql);
if ($resql && $db->num_rows($resql) > 0) {
$obj = $db->fetch_object($resql);
// Found existing invoice - link it
$import->fk_facture_fourn = $obj->rowid;
$import->status = ZugferdImport::STATUS_PROCESSED;
$import->date_import = dol_now();
$result = $import->update($user);
if ($result > 0) {
$invoiceLink = '<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) {
$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);
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: '.$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> '.$line->ean.'</span>';
}
print ' <i class="fas fa-check-circle" style="color: green;"></i>';
} 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>';
} 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', '', 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>';
// 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', '', 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>';
// 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>';
}
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", "'.$_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();