1438 lines
60 KiB
PHP
1438 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'
|
|
);
|
|
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 ' ';
|
|
}
|
|
|
|
// 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 ' ';
|
|
}
|
|
|
|
// 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 ' ';
|
|
}
|
|
|
|
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>';
|
|
}
|
|
|
|
llxFooter();
|
|
$db->close();
|