2206 lines
100 KiB
PHP
2206 lines
100 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
|
||
*/
|
||
|
||
// 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 ' ';
|
||
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 ' ';
|
||
}
|
||
|
||
// 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 ' ';
|
||
}
|
||
|
||
// 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 ' ';
|
||
|
||
// "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 ' ';
|
||
}
|
||
|
||
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 ' ';
|
||
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()">×</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();
|