importzugferd/import.php

1439 lines
60 KiB
PHP

<?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
*/
// Upload and parse PDF - creates import record immediately
if ($action == 'upload') {
if (!empty($_FILES['zugferd_file']['tmp_name'])) {
$upload_dir = $conf->importzugferd->dir_output.'/temp';
if (!is_dir($upload_dir)) {
dol_mkdir($upload_dir);
}
$filename = dol_sanitizeFileName($_FILES['zugferd_file']['name']);
$destfile = $upload_dir.'/'.$filename;
if (move_uploaded_file($_FILES['zugferd_file']['tmp_name'], $destfile)) {
$force_reimport = GETPOST('force_reimport', 'int');
// Check for duplicate
$file_hash = hash_file('sha256', $destfile);
$isDuplicate = $import->isDuplicate($file_hash);
if ($isDuplicate && !$force_reimport) {
$error++;
$message = $langs->trans('ErrorDuplicateInvoice');
@unlink($destfile);
} else {
// If force reimport, delete the old record first
if ($isDuplicate && $force_reimport) {
$oldImport = new ZugferdImport($db);
$oldImport->fetch(0, null, $file_hash);
if ($oldImport->id > 0) {
// Delete old lines
$oldLines = new ImportLine($db);
$oldLines->deleteAllByImport($oldImport->id);
// Delete old files
$old_dir = $conf->importzugferd->dir_output.'/imports/'.$oldImport->id;
if (is_dir($old_dir)) {
dol_delete_dir_recursive($old_dir);
}
// Delete old import record
$oldImport->delete($user);
}
}
// Parse the file
$parser = new ZugferdParser($db);
$res = $parser->extractFromPdf($destfile);
if ($res > 0) {
$res = $parser->parse();
if ($res > 0) {
$parsed_data = $parser->getInvoiceData();
// Create import record immediately
$import->invoice_number = $parsed_data['invoice_number'];
$import->invoice_date = $parsed_data['invoice_date'];
$import->seller_name = $parsed_data['seller']['name'];
$import->seller_vat = $parsed_data['seller']['vat_id'];
$import->buyer_reference = $parsed_data['buyer']['reference'] ?: $parsed_data['buyer']['id'];
$import->total_ht = $parsed_data['totals']['net'];
$import->total_ttc = $parsed_data['totals']['gross'];
$import->currency = $parsed_data['totals']['currency'];
$import->xml_content = $parser->getXmlContent();
$import->pdf_filename = $filename;
$import->file_hash = $file_hash;
// Find supplier
$supplier_id = $actions->findSupplier($parsed_data);
$import->fk_soc = $supplier_id;
// Process line items to find products
$processed_lines = $actions->processLineItems($parsed_data['lines'], $supplier_id);
// Check if all lines have products
$all_have_products = true;
$has_any_product = false;
$total_lines = count($processed_lines);
foreach ($processed_lines as $line) {
if ($line['fk_product'] <= 0) {
$all_have_products = false;
} else {
$has_any_product = true;
}
}
// Set status based on product matching
// STATUS_IMPORTED only if: supplier found, has lines, ALL lines have products
if ($all_have_products && $supplier_id > 0 && $total_lines > 0 && $has_any_product) {
$import->status = ZugferdImport::STATUS_IMPORTED;
} else {
$import->status = ZugferdImport::STATUS_PENDING;
}
$import->date_creation = dol_now();
$result = $import->create($user);
if ($result > 0) {
// Store line items in database
foreach ($processed_lines as $line) {
$importLineObj = new ImportLine($db);
$importLineObj->fk_import = $import->id;
$importLineObj->line_id = $line['line_id'];
$importLineObj->supplier_ref = $line['supplier_ref'];
$importLineObj->product_name = $line['name'];
$importLineObj->description = $line['description'];
$importLineObj->quantity = $line['quantity'];
$importLineObj->unit_code = $line['unit_code'];
$importLineObj->unit_price = $line['unit_price'];
$importLineObj->unit_price_raw = $line['unit_price_raw'];
$importLineObj->basis_quantity = $line['basis_quantity'];
$importLineObj->basis_quantity_unit = $line['basis_quantity_unit'];
$importLineObj->line_total = $line['line_total'];
$importLineObj->tax_percent = $line['tax_percent'];
$importLineObj->ean = $line['ean'];
$importLineObj->fk_product = $line['fk_product'];
$importLineObj->match_method = $line['match_method'];
$importLineObj->create($user);
}
// Move PDF to permanent storage
$final_dir = $conf->importzugferd->dir_output.'/imports/'.$import->id;
if (!is_dir($final_dir)) {
dol_mkdir($final_dir);
}
rename($destfile, $final_dir.'/'.$filename);
// Send notification if manual intervention required
if ($import->status == ZugferdImport::STATUS_PENDING) {
$storedLines = $importLine->fetchAllByImport($import->id);
$notification->sendManualInterventionNotification($import, $storedLines);
}
// Check for price differences
if ($import->status == ZugferdImport::STATUS_IMPORTED) {
$storedLines = $importLine->fetchAllByImport($import->id);
$notification->checkAndNotifyPriceDifferences($import, $storedLines);
}
// Redirect to edit page
$id = $import->id;
$action = 'edit';
setEventMessages($langs->trans('ImportRecordCreated'), null, 'mesgs');
} else {
$error++;
$message = $import->error;
@unlink($destfile);
// Send error notification
$notification->sendErrorNotification($import, $message, $filename);
}
} else {
$error++;
$message = $parser->error;
@unlink($destfile);
}
} else {
$error++;
$message = $parser->error;
@unlink($destfile);
}
}
} else {
$error++;
$message = $langs->trans('ErrorFileUploadFailed');
}
} else {
$error++;
$message = $langs->trans('ErrorNoFileUploaded');
}
}
// Load existing import for editing
if ($id > 0 && empty($action)) {
$action = 'edit';
}
if ($action == 'edit' && $id > 0) {
$result = $import->fetch($id);
if ($result <= 0) {
$error++;
$message = $langs->trans('ErrorRecordNotFound');
$action = '';
}
}
// Assign product to line
if ($action == 'assignproduct' && $line_id > 0 && $product_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
$lineObj->setProduct($product_id, $langs->trans('ManualAssignment'), $user);
setEventMessages($langs->trans('ProductAssigned'), null, 'mesgs');
// Get import ID to reload
$id = $lineObj->fk_import;
// Check if all lines now have products
$allHaveProducts = $importLine->allLinesHaveProducts($id);
if ($allHaveProducts) {
// Update import status
$import->fetch($id);
if ($import->status == ZugferdImport::STATUS_PENDING) {
$import->status = ZugferdImport::STATUS_IMPORTED;
$import->update($user);
// Check for price differences now that all products are assigned
$storedLines = $importLine->fetchAllByImport($id);
$notification->checkAndNotifyPriceDifferences($import, $storedLines);
}
}
}
$action = 'edit';
$import->fetch($id);
}
// Remove product assignment from line
if ($action == 'removeproduct' && $line_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
$id = $lineObj->fk_import;
$lineObj->setProduct(0, '', $user);
setEventMessages($langs->trans('ProductRemoved'), null, 'mesgs');
// Update import status to pending
$import->fetch($id);
if ($import->status == ZugferdImport::STATUS_IMPORTED) {
$import->status = ZugferdImport::STATUS_PENDING;
$import->update($user);
}
}
$action = 'edit';
$import->fetch($id);
}
// Update supplier
if ($action == 'setsupplier' && $id > 0) {
$import->fetch($id);
$import->fk_soc = $supplier_id;
$import->update($user);
setEventMessages($langs->trans('SupplierUpdated'), null, 'mesgs');
$action = 'edit';
}
// Duplicate product from template
if ($action == 'duplicateproduct' && $template_product_id > 0 && $line_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
// Load template product
$template = new Product($db);
if ($template->fetch($template_product_id) > 0) {
// Create new product as copy
$newproduct = new Product($db);
// Copy basic properties from template
$newproduct->type = $template->type;
$newproduct->status = $template->status;
$newproduct->status_buy = $template->status_buy;
$newproduct->status_batch = $template->status_batch;
$newproduct->fk_product_type = $template->fk_product_type;
$newproduct->price = $lineObj->unit_price;
$newproduct->price_base_type = 'HT';
$newproduct->tva_tx = $lineObj->tax_percent ?: $template->tva_tx;
$newproduct->weight = $template->weight;
$newproduct->weight_units = $template->weight_units;
$newproduct->fk_unit = $template->fk_unit;
// Set label from ZUGFeRD
$newproduct->label = $lineObj->product_name;
// Generate unique ref
$newproduct->ref = 'NEW-'.dol_print_date(dol_now(), '%Y%m%d%H%M%S');
// Build description with ZUGFeRD data
$zugferd_info = '';
if (!empty($lineObj->supplier_ref)) {
$zugferd_info .= $langs->trans('SupplierRef').': '.$lineObj->supplier_ref."\n";
}
if (!empty($lineObj->unit_code)) {
$zugferd_info .= $langs->trans('Unit').': '.zugferdGetUnitLabel($lineObj->unit_code)."\n";
}
if (!empty($lineObj->ean)) {
$zugferd_info .= 'EAN: '.$lineObj->ean."\n";
}
$zugferd_info .= "---\n";
$newproduct->description = $zugferd_info . ($template->description ?: '');
// Create the product
$result = $newproduct->create($user);
if ($result > 0) {
setEventMessages($langs->trans('ProductCreated'), null, 'mesgs');
// Redirect to product card for editing
header('Location: '.DOL_URL_ROOT.'/product/card.php?id='.$result);
exit;
} else {
setEventMessages($newproduct->error, $newproduct->errors, 'errors');
}
}
$id = $lineObj->fk_import;
}
$action = 'edit';
$import->fetch($id);
}
// Create product from Datanorm
if ($action == 'createfromdatanorm' && $line_id > 0) {
$lineObj = new ImportLine($db);
$result = $lineObj->fetch($line_id);
if ($result > 0) {
$id = $lineObj->fk_import;
$import->fetch($id);
// Get Datanorm settings
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
// Search in Datanorm database
$datanorm = new Datanorm($db);
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
if (empty($results)) {
// Try with EAN if available
if (!empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
}
}
if (!empty($results)) {
$datanormArticle = $results[0];
$datanorm->fetch($datanormArticle['id']);
// Load supplier for ref prefix
$supplier = new Societe($db);
$supplier->fetch($import->fk_soc);
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
// Create new product
$newproduct = new Product($db);
$newproduct->type = 0; // Product
$newproduct->status = 1; // On sale
$newproduct->status_buy = 1; // On purchase
// Generate reference
$newproduct->ref = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
// Label from Datanorm
$newproduct->label = $datanorm->short_text1;
if (!empty($datanorm->short_text2) && strlen($newproduct->label) < 100) {
$newproduct->label .= ' '.$datanorm->short_text2;
}
// Description
$newproduct->description = $datanorm->getFullDescription();
// Prices
$purchasePrice = $datanorm->price;
if ($datanorm->price_unit > 1) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
// Selling price with markup
$sellingPrice = $purchasePrice * (1 + $markup / 100);
$newproduct->price = $sellingPrice;
$newproduct->price_base_type = 'HT';
$newproduct->tva_tx = $lineObj->tax_percent ?: 19;
// Weight if available
if (!empty($datanorm->weight)) {
$newproduct->weight = $datanorm->weight;
$newproduct->weight_units = 0; // kg
}
// Let Dolibarr auto-generate barcode if configured
// Setting barcode to '-1' triggers automatic generation
if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
$newproduct->barcode = '-1';
}
// Create the product
$result = $newproduct->create($user);
if ($result > 0) {
// Add supplier price
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
$prodfourn = new ProductFournisseur($db);
$prodfourn->id = $newproduct->id;
$prodfourn->fourn_ref = $datanorm->article_number;
// Determine EAN for supplier price
$supplierEan = '';
$supplierEanType = 0;
if (!empty($datanorm->ean)) {
$supplierEan = $datanorm->ean;
$supplierEanType = 2; // EAN13
} elseif (!empty($lineObj->ean)) {
$supplierEan = $lineObj->ean;
$supplierEanType = 2; // EAN13
}
// Add supplier price entry with EAN
$res = $prodfourn->update_buyprice(
1, // Quantity
$purchasePrice, // Price
$user,
'HT', // Price base
$supplier, // Supplier
0, // Availability
$datanorm->article_number, // Supplier ref
$lineObj->tax_percent ?: 19, // VAT
0, // Charges
0, // Remise
0, // Remise percentage
0, // No price minimum
0, // Delivery delay
0, // Reputation
array(), // Extra fields
0, // Charges array
$supplierEan, // Barcode/EAN in supplier price
$supplierEanType // Barcode type (EAN13)
);
// Create product mapping for future imports
$mapping = new ProductMapping($db);
$mapping->fk_soc = $import->fk_soc;
$mapping->supplier_ref = $datanorm->article_number;
$mapping->fk_product = $newproduct->id;
$mapping->ean = $datanorm->ean;
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
$mapping->description = $datanorm->short_text1;
$mapping->create($user);
// Assign to import line
$lineObj->setProduct($newproduct->id, 'datanorm', $user);
setEventMessages($langs->trans('ProductCreatedFromDatanorm', $newproduct->ref), null, 'mesgs');
// Check if all lines now have products
$allHaveProducts = $importLine->allLinesHaveProducts($id);
if ($allHaveProducts) {
$import->status = ZugferdImport::STATUS_IMPORTED;
$import->update($user);
}
} else {
setEventMessages($newproduct->error, $newproduct->errors, 'errors');
}
} else {
setEventMessages($langs->trans('DatanormArticleNotFound', $lineObj->supplier_ref), null, 'errors');
}
}
$action = 'edit';
$import->fetch($id);
}
// Create ALL products from Datanorm (batch)
if ($action == 'createallfromdatanorm' && $id > 0) {
$import->fetch($id);
if ($import->fk_soc > 0) {
// Get Datanorm settings
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
// Load supplier
$supplier = new Societe($db);
$supplier->fetch($import->fk_soc);
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
// Get all lines without product
$lines = $importLine->fetchAllByImport($import->id);
$datanorm = new Datanorm($db);
$createdCount = 0;
$assignedCount = 0;
$errorCount = 0;
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
foreach ($lines as $lineObj) {
// Skip lines that already have a product
if ($lineObj->fk_product > 0) {
continue;
}
// Skip lines without supplier_ref
if (empty($lineObj->supplier_ref)) {
continue;
}
// Search in Datanorm database
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
if (empty($results) && !empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
}
if (!empty($results)) {
$datanormArticle = $results[0];
$datanorm->fetch($datanormArticle['id']);
$purchasePrice = $datanorm->price;
if ($datanorm->price_unit > 1) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
// Check if product already exists in Dolibarr
$existingProduct = new Product($db);
$productExists = false;
$existingProductId = 0;
// 1. Check by supplier reference (ProductFournisseur)
$sqlCheck = "SELECT DISTINCT pf.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pf";
$sqlCheck .= " WHERE pf.fk_soc = ".(int)$import->fk_soc;
$sqlCheck .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'";
$sqlCheck .= " AND pf.entity IN (".getEntity('product').")";
$resqlCheck = $db->query($sqlCheck);
if ($resqlCheck && $db->num_rows($resqlCheck) > 0) {
$objCheck = $db->fetch_object($resqlCheck);
$existingProductId = $objCheck->fk_product;
$productExists = true;
}
// 2. Check by product reference pattern
if (!$productExists) {
$expectedRef = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
$fetchResult = $existingProduct->fetch(0, $expectedRef);
if ($fetchResult > 0) {
$existingProductId = $existingProduct->id;
$productExists = true;
}
}
// 3. Check by EAN if available
if (!$productExists && !empty($datanorm->ean)) {
$sqlEan = "SELECT rowid FROM ".MAIN_DB_PREFIX."product";
$sqlEan .= " WHERE barcode = '".$db->escape($datanorm->ean)."'";
$sqlEan .= " AND entity IN (".getEntity('product').")";
$resqlEan = $db->query($sqlEan);
if ($resqlEan && $db->num_rows($resqlEan) > 0) {
$objEan = $db->fetch_object($resqlEan);
$existingProductId = $objEan->rowid;
$productExists = true;
}
}
if ($productExists && $existingProductId > 0) {
// Product exists - just assign it to the line
$lineObj->setProduct($existingProductId, 'datanorm', $user);
$assignedCount++;
} else {
// Create new product
$newproduct = new Product($db);
$newproduct->type = 0;
$newproduct->status = 1;
$newproduct->status_buy = 1;
$newproduct->ref = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
$newproduct->label = $datanorm->short_text1;
if (!empty($datanorm->short_text2) && strlen($newproduct->label) < 100) {
$newproduct->label .= ' '.$datanorm->short_text2;
}
$newproduct->description = $datanorm->getFullDescription();
$sellingPrice = $purchasePrice * (1 + $markup / 100);
$newproduct->price = $sellingPrice;
$newproduct->price_base_type = 'HT';
$newproduct->tva_tx = $lineObj->tax_percent ?: 19;
if (!empty($datanorm->weight)) {
$newproduct->weight = $datanorm->weight;
$newproduct->weight_units = 0;
}
if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
$newproduct->barcode = '-1';
}
$result = $newproduct->create($user);
if ($result > 0) {
// Add supplier price
$prodfourn = new ProductFournisseur($db);
$prodfourn->id = $newproduct->id;
$prodfourn->fourn_ref = $datanorm->article_number;
$supplierEan = '';
$supplierEanType = 0;
if (!empty($datanorm->ean)) {
$supplierEan = $datanorm->ean;
$supplierEanType = 2;
} elseif (!empty($lineObj->ean)) {
$supplierEan = $lineObj->ean;
$supplierEanType = 2;
}
$prodfourn->update_buyprice(
1, $purchasePrice, $user, 'HT', $supplier, 0,
$datanorm->article_number, $lineObj->tax_percent ?: 19,
0, 0, 0, 0, 0, 0, array(), 0, $supplierEan, $supplierEanType
);
// Create product mapping
$mapping = new ProductMapping($db);
$mapping->fk_soc = $import->fk_soc;
$mapping->supplier_ref = $datanorm->article_number;
$mapping->fk_product = $newproduct->id;
$mapping->ean = $datanorm->ean;
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
$mapping->description = $datanorm->short_text1;
$mapping->create($user);
// Assign to import line
$lineObj->setProduct($newproduct->id, 'datanorm', $user);
$createdCount++;
} else {
$errorCount++;
}
}
}
}
if ($createdCount > 0) {
setEventMessages($langs->trans('DatanormBatchCreated', $createdCount), null, 'mesgs');
}
if ($assignedCount > 0) {
setEventMessages($langs->trans('DatanormBatchAssigned', $assignedCount), null, 'mesgs');
}
if ($errorCount > 0) {
setEventMessages($langs->trans('DatanormBatchErrors', $errorCount), null, 'warnings');
}
if ($createdCount == 0 && $assignedCount == 0 && $errorCount == 0) {
setEventMessages($langs->trans('DatanormBatchNoMatches'), null, 'warnings');
}
// Check if all lines now have products
$allHaveProducts = $importLine->allLinesHaveProducts($id);
if ($allHaveProducts) {
$import->status = ZugferdImport::STATUS_IMPORTED;
$import->update($user);
}
}
$action = 'edit';
$import->fetch($id);
}
// Create supplier invoice
if ($action == 'createinvoice' && $id > 0) {
$import->fetch($id);
// Check prerequisites
if ($import->fk_soc <= 0) {
$error++;
setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors');
} else {
// Check all lines have products
$lines = $importLine->fetchAllByImport($id);
$allHaveProducts = true;
foreach ($lines as $line) {
if ($line->fk_product <= 0) {
$allHaveProducts = false;
break;
}
}
if (!$allHaveProducts) {
$error++;
setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors');
} else {
// Create invoice
$invoice = new FactureFournisseur($db);
$invoice->socid = $import->fk_soc;
$invoice->ref_supplier = $import->invoice_number;
$invoice->date = $import->invoice_date;
$invoice->note_private = $langs->trans('ImportedFromZugferd').' ('.$import->ref.')';
$invoice->cond_reglement_id = 1;
$db->begin();
$result = $invoice->create($user);
if ($result > 0) {
// Add lines
foreach ($lines as $line) {
$res = $invoice->addline(
$line->product_name,
$line->unit_price,
$line->tax_percent,
0, 0,
$line->quantity,
$line->fk_product,
0, '', '',
0, 0,
'HT', // price_base_type - Netto-Preise aus ZUGFeRD
0 // type (0=product)
);
if ($res < 0) {
$error++;
setEventMessages($invoice->error, $invoice->errors, 'errors');
break;
}
// Update EAN on product if not set
if (!empty($line->ean) && $line->fk_product > 0) {
$product = new Product($db);
$product->fetch($line->fk_product);
if (empty($product->barcode)) {
$product->barcode = $line->ean;
$product->barcode_type = 2; // EAN13
$product->update($product->id, $user);
}
}
}
if (!$error) {
// Invoice stays as draft - user can validate manually
// Copy PDF to invoice
$source_pdf = $conf->importzugferd->dir_output.'/imports/'.$import->id.'/'.$import->pdf_filename;
if (file_exists($source_pdf)) {
$dest_dir = $conf->fournisseur->facture->dir_output.'/'.get_exdir($invoice->id, 2, 0, 0, $invoice, 'invoice_supplier').$invoice->ref;
if (!is_dir($dest_dir)) {
dol_mkdir($dest_dir);
}
copy($source_pdf, $dest_dir.'/'.$import->pdf_filename);
}
// Update import record
$import->fk_facture_fourn = $invoice->id;
$import->status = ZugferdImport::STATUS_PROCESSED;
$import->date_import = dol_now();
$import->update($user);
$db->commit();
setEventMessages($langs->trans('InvoiceCreatedSuccessfully'), null, 'mesgs');
// Redirect to invoice
header('Location: '.DOL_URL_ROOT.'/fourn/facture/card.php?facid='.$invoice->id);
exit;
} else {
$db->rollback();
}
} else {
$error++;
setEventMessages($invoice->error, $invoice->errors, 'errors');
$db->rollback();
}
}
}
$action = 'edit';
}
// Finish import - check for existing invoice and update status
if ($action == 'finishimport' && $id > 0) {
$import->fetch($id);
// Check all lines have products
$lines = $importLine->fetchAllByImport($id);
$allHaveProducts = true;
foreach ($lines as $line) {
if ($line->fk_product <= 0) {
$allHaveProducts = false;
break;
}
}
if (!$allHaveProducts) {
$error++;
setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors');
} elseif ($import->fk_soc <= 0) {
$error++;
setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors');
} else {
// Search for existing supplier invoice with this ref_supplier
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_fourn";
$sql .= " WHERE fk_soc = ".((int) $import->fk_soc);
$sql .= " AND ref_supplier = '".$db->escape($import->invoice_number)."'";
$sql .= " LIMIT 1";
$resql = $db->query($sql);
if ($resql && $db->num_rows($resql) > 0) {
$obj = $db->fetch_object($resql);
// Found existing invoice - link it
$import->fk_facture_fourn = $obj->rowid;
$import->status = ZugferdImport::STATUS_PROCESSED;
$import->date_import = dol_now();
$result = $import->update($user);
if ($result > 0) {
$invoiceLink = '<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);
$rowClass = $hasProduct ? 'oddeven opacitymedium' : 'oddeven';
print '<tr class="'.$rowClass.'">';
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 class="opacitymedium">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 class="opacitymedium">('.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>';
print '<br><span class="opacitymedium small">';
print dol_trunc($datanormArticle['short_text1'], 40);
print ' - '.price($datanormArticle['price']);
print '</span>';
}
}
}
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>';
// 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; ';
}
// Button to create all products from Datanorm
if ($missingProducts > 0 && $import->fk_soc > 0) {
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('CreateAllFromDatanorm');
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>';
}
llxFooter();
$db->close();