importzugferd/class/actions_importzugferd.class.php

956 lines
34 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 class/actions_importzugferd.class.php
* \ingroup importzugferd
* \brief Actions class for ZUGFeRD import operations
*/
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
dol_include_once('/importzugferd/class/zugferdparser.class.php');
dol_include_once('/importzugferd/class/zugferdimport.class.php');
dol_include_once('/importzugferd/class/productmapping.class.php');
/**
* Class ActionsImportZugferd
* Handles the import process of ZUGFeRD invoices
*/
class ActionsImportZugferd
{
/**
* @var DoliDB Database handler
*/
public $db;
/**
* @var string Error message
*/
public $error = '';
/**
* @var array Error messages
*/
public $errors = array();
/**
* @var array Warning messages
*/
public $warnings = array();
/**
* @var ZugferdParser Parser instance
*/
public $parser;
/**
* @var ZugferdImport Import record
*/
public $import;
/**
* @var ProductMapping Mapping helper
*/
public $mapping;
/**
* @var array Import result data
*/
public $result = array();
/**
* @var array Results for hooks
*/
public $results = array();
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
$this->parser = new ZugferdParser($db);
$this->import = new ZugferdImport($db);
$this->mapping = new ProductMapping($db);
}
/**
* Process a ZUGFeRD PDF file
*
* @param string $pdf_path Path to PDF file
* @param User $user Current user
* @param bool $create_invoice Whether to create supplier invoice
* @param bool $force_reimport Whether to bypass duplicate check
* @return int <0 if KO, >0 if OK (import record ID)
*/
public function processPdf($pdf_path, $user, $create_invoice = false, $force_reimport = false)
{
global $conf;
$this->result = array(
'import_id' => 0,
'invoice_id' => 0,
'supplier_id' => 0,
'supplier_found' => false,
'is_duplicate' => false,
'lines' => array(),
'warnings' => array(),
);
// Extract XML from PDF
$res = $this->parser->extractFromPdf($pdf_path);
if ($res < 0) {
$this->error = $this->parser->error;
return -1;
}
// Parse XML
$res = $this->parser->parse();
if ($res < 0) {
$this->error = $this->parser->error;
return -2;
}
$invoice_data = $this->parser->getInvoiceData();
// Check for duplicates
$file_hash = $this->parser->getFileHash($pdf_path);
if ($this->import->isDuplicate($file_hash)) {
if ($force_reimport) {
// Delete existing import record to allow reimport
$this->deleteExistingImport($file_hash, $user);
} else {
global $langs;
$langs->load('importzugferd@importzugferd');
$this->result['is_duplicate'] = true;
$this->error = $langs->trans('ErrorDuplicateInvoice');
return -3;
}
}
// Find supplier
$supplier_id = $this->findSupplier($invoice_data);
$this->result['supplier_id'] = $supplier_id;
$this->result['supplier_found'] = ($supplier_id > 0);
// Create import record
$this->import->invoice_number = $invoice_data['invoice_number'];
$this->import->invoice_date = $invoice_data['invoice_date'];
$this->import->seller_name = $invoice_data['seller']['name'];
$this->import->seller_vat = $invoice_data['seller']['vat_id'];
$this->import->buyer_reference = $invoice_data['buyer']['reference'] ?: $invoice_data['buyer']['id'];
$this->import->total_ht = $invoice_data['totals']['net'];
$this->import->total_ttc = $invoice_data['totals']['gross'];
$this->import->currency = $invoice_data['totals']['currency'] ?: 'EUR';
$this->import->fk_soc = $supplier_id;
$this->import->xml_content = $this->parser->getXmlContent();
$this->import->pdf_filename = basename($pdf_path);
$this->import->file_hash = $file_hash;
$this->import->status = ZugferdImport::STATUS_IMPORTED;
$this->import->date_import = dol_now();
$import_id = $this->import->create($user);
if ($import_id < 0) {
$this->error = $this->import->error;
return -4;
}
$this->result['import_id'] = $import_id;
// Process line items
$this->result['lines'] = $this->processLineItems($invoice_data['lines'], $supplier_id);
// Copy PDF to documents folder
$this->copyToDocuments($pdf_path, $import_id);
// Create supplier invoice if requested
if ($create_invoice && $supplier_id > 0) {
$invoice_id = $this->createSupplierInvoice($invoice_data, $supplier_id, $user, $pdf_path);
if ($invoice_id > 0) {
$this->result['invoice_id'] = $invoice_id;
$this->import->fk_facture_fourn = $invoice_id;
// Check validation result - status may have been set to ERROR in validateTotals()
if ($this->import->status != ZugferdImport::STATUS_ERROR) {
$this->import->status = ZugferdImport::STATUS_PROCESSED;
}
$this->import->update($user);
// Add validation warning if there was a sum mismatch
if (!empty($this->result['validation']) && !$this->result['validation']['valid']) {
$this->result['warnings'][] = $this->result['validation']['message'];
}
} else {
$this->result['warnings'][] = 'Could not create supplier invoice: ' . $this->error;
}
}
return $import_id;
}
/**
* Find supplier by buyer reference (customer number)
*
* @param array $invoice_data Parsed invoice data
* @return int Supplier ID or 0
*/
public function findSupplier($invoice_data)
{
global $conf;
$buyer_ref = $invoice_data['buyer']['reference'] ?: $invoice_data['buyer']['id'];
$seller_vat = $invoice_data['seller']['vat_id'];
$seller_name = $invoice_data['seller']['name'];
// 1. Search by buyer reference in extrafield
if (!empty($buyer_ref)) {
$sql = "SELECT fk_object FROM " . MAIN_DB_PREFIX . "societe_extrafields";
$sql .= " WHERE supplier_customer_number = '" . $this->db->escape($buyer_ref) . "'";
$resql = $this->db->query($sql);
if ($resql && $this->db->num_rows($resql) > 0) {
$obj = $this->db->fetch_object($resql);
return (int) $obj->fk_object;
}
}
// 2. Search by VAT ID
if (!empty($seller_vat)) {
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "societe";
$sql .= " WHERE tva_intra = '" . $this->db->escape($seller_vat) . "'";
$sql .= " AND fournisseur = 1";
$sql .= " AND entity IN (" . getEntity('societe') . ")";
$resql = $this->db->query($sql);
if ($resql && $this->db->num_rows($resql) > 0) {
$obj = $this->db->fetch_object($resql);
return (int) $obj->rowid;
}
}
// 3. Search by name (fuzzy)
if (!empty($seller_name)) {
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "societe";
$sql .= " WHERE (nom LIKE '" . $this->db->escape($seller_name) . "%'";
$sql .= " OR nom LIKE '%" . $this->db->escape(substr($seller_name, 0, 20)) . "%')";
$sql .= " AND fournisseur = 1";
$sql .= " AND entity IN (" . getEntity('societe') . ")";
$sql .= " LIMIT 1";
$resql = $this->db->query($sql);
if ($resql && $this->db->num_rows($resql) > 0) {
$obj = $this->db->fetch_object($resql);
return (int) $obj->rowid;
}
}
return 0;
}
/**
* Process line items and find matching products
*
* @param array $lines Line items from invoice
* @param int $supplier_id Supplier ID
* @return array Processed lines with product info
*/
public function processLineItems($lines, $supplier_id)
{
$processed = array();
$last_product_index = -1;
foreach ($lines as $idx => $line) {
// Check if this is a metal surcharge line
$is_surcharge = $this->isMetalSurchargeLine($line);
// Get copper surcharge directly from parsed line data (if available)
$copper_surcharge_per_unit = isset($line['copper_surcharge_per_unit']) ? $line['copper_surcharge_per_unit'] : null;
$processed_line = array(
'line_id' => $line['line_id'],
'supplier_ref' => $line['product']['seller_id'],
'ean' => $line['product']['global_id'],
'name' => $line['product']['name'],
'description' => $line['product']['description'],
'quantity' => $line['quantity'],
'unit_code' => $line['unit_code'],
'unit_price' => $line['unit_price'],
'unit_price_raw' => isset($line['unit_price_raw']) ? $line['unit_price_raw'] : $line['unit_price'],
'basis_quantity' => isset($line['basis_quantity']) ? $line['basis_quantity'] : 1,
'basis_quantity_unit' => isset($line['basis_quantity_unit']) ? $line['basis_quantity_unit'] : '',
'line_total' => $line['line_total'],
'tax_percent' => $line['tax_percent'],
'fk_product' => 0,
'product_ref' => '',
'product_label' => '',
'match_method' => '',
'needs_creation' => false,
'is_metal_surcharge' => $is_surcharge,
'metal_surcharge' => $copper_surcharge_per_unit ?: 0, // From parsed XML or will be filled from surcharge lines
'copper_surcharge_raw' => isset($line['copper_surcharge']) ? $line['copper_surcharge'] : null,
'copper_surcharge_basis_qty' => isset($line['copper_surcharge_basis_qty']) ? $line['copper_surcharge_basis_qty'] : null,
);
// Try to find product
if ($supplier_id > 0 && !$is_surcharge) {
$match = $this->mapping->findProduct($supplier_id, $line['product']);
if ($match['fk_product'] > 0) {
$processed_line['fk_product'] = $match['fk_product'];
$processed_line['match_method'] = $match['method'];
// Get product info
$product = new Product($this->db);
if ($product->fetch($match['fk_product']) > 0) {
$processed_line['product_ref'] = $product->ref;
$processed_line['product_label'] = $product->label;
}
} else {
$processed_line['needs_creation'] = true;
}
} elseif (!$is_surcharge) {
$processed_line['needs_creation'] = true;
}
$processed[] = $processed_line;
$current_index = count($processed) - 1;
// If this is a metal surcharge line, associate it with the previous product
// Only use this fallback if the product line doesn't already have copper_surcharge from XML
if ($is_surcharge && $last_product_index >= 0) {
// Only apply if the previous product doesn't already have a copper surcharge from XML
if (empty($processed[$last_product_index]['metal_surcharge'])) {
// Calculate surcharge per unit based on the product's quantity
$product_qty = $processed[$last_product_index]['quantity'];
if ($product_qty > 0) {
$surcharge_per_unit = $line['line_total'] / $product_qty;
$processed[$last_product_index]['metal_surcharge'] = $surcharge_per_unit;
dol_syslog("Metal surcharge from separate line: " . $line['line_total'] . " EUR for " . $product_qty . " units = " . $surcharge_per_unit . " EUR/unit", LOG_INFO);
}
}
// Copy product info to surcharge line for reference
$processed_line['fk_product'] = $processed[$last_product_index]['fk_product'];
$processed_line['associated_product_ref'] = $processed[$last_product_index]['product_ref'];
$processed[$current_index] = $processed_line;
}
// Log if copper surcharge was extracted from XML
if ($copper_surcharge_per_unit > 0) {
dol_syslog("Copper surcharge from XML: " . $copper_surcharge_per_unit . " EUR/unit for " . $line['product']['name'], LOG_INFO);
}
// Remember the last non-surcharge product line
if (!$is_surcharge && $processed_line['fk_product'] > 0) {
$last_product_index = $current_index;
}
}
return $processed;
}
/**
* Create supplier invoice from parsed data
*
* @param array $invoice_data Parsed invoice data
* @param int $supplier_id Supplier ID
* @param User $user Current user
* @param string $pdf_path Path to source PDF file (optional)
* @return int Invoice ID or <0 if error
*/
public function createSupplierInvoice($invoice_data, $supplier_id, $user, $pdf_path = '')
{
global $conf, $langs;
$invoice = new FactureFournisseur($this->db);
$invoice->socid = $supplier_id;
$invoice->ref_supplier = $invoice_data['invoice_number'];
$invoice->date = strtotime($invoice_data['invoice_date']);
$invoice->date_echeance = !empty($invoice_data['due_date']) ? strtotime($invoice_data['due_date']) : null;
$invoice->note_private = $langs->trans('ImportedFromZugferd') . ' - ' . $this->import->ref;
$invoice->multicurrency_code = $invoice_data['totals']['currency'] ?: 'EUR';
$this->db->begin();
$invoice_id = $invoice->create($user);
if ($invoice_id < 0) {
$this->error = $invoice->error;
$this->db->rollback();
return -1;
}
// Add lines
foreach ($this->result['lines'] as $line) {
$result = $this->addInvoiceLine($invoice, $line, $user);
if ($result < 0) {
$this->db->rollback();
return -2;
}
}
$this->db->commit();
// Validate totals - re-fetch invoice to get calculated totals
$invoice->fetch($invoice_id);
$validation_result = $this->validateTotals($invoice_data, $invoice);
$this->result['validation'] = $validation_result;
// Attach PDF to supplier invoice
if (!empty($pdf_path) && file_exists($pdf_path)) {
$this->attachPdfToInvoice($invoice, $pdf_path);
}
return $invoice_id;
}
/**
* Attach PDF file to supplier invoice
*
* @param FactureFournisseur $invoice Invoice object
* @param string $pdf_path Source PDF path
* @return bool Success
*/
public function attachPdfToInvoice($invoice, $pdf_path)
{
global $conf;
// Get supplier for folder name
$supplier = new Societe($this->db);
$supplier->fetch($invoice->socid);
// Build destination directory path for supplier invoice
// Format: DOL_DATA_ROOT/fournisseur/facture/[thirdparty_name]/[invoice_ref]/
$destdir = $conf->fournisseur->facture->dir_output;
$destdir .= '/' . dol_sanitizeFileName($supplier->nom);
$destdir .= '/' . dol_sanitizeFileName($invoice->ref);
// Create directory if it doesn't exist
if (!is_dir($destdir)) {
dol_mkdir($destdir);
}
// Build descriptive filename
// Format: YYYY-MM-DD - Lieferant - Rechnungsnummer - Material - Preis EUR.pdf
$newFilename = $this->buildInvoiceFilename($invoice, $supplier);
$destfile = $destdir . '/' . $newFilename;
if (copy($pdf_path, $destfile)) {
dol_syslog("Attached PDF as " . $newFilename . " to supplier invoice " . $invoice->ref, LOG_INFO);
return true;
}
return false;
}
/**
* Build descriptive filename for invoice PDF
* Format: YYYY-MM-DD - Lieferant - Rechnungsnummer - Material - Preis EUR.pdf
*
* @param FactureFournisseur $invoice Invoice object
* @param Societe $supplier Supplier object
* @return string Filename
*/
private function buildInvoiceFilename($invoice, $supplier)
{
// Date: YYYY-MM-DD
$date = dol_print_date($invoice->date, '%Y-%m-%d');
// Supplier name (shortened if too long)
$supplierName = dol_sanitizeFileName($supplier->nom);
if (strlen($supplierName) > 30) {
$supplierName = substr($supplierName, 0, 30);
}
// Invoice number from supplier
$invoiceNumber = dol_sanitizeFileName($invoice->ref_supplier);
if (empty($invoiceNumber)) {
$invoiceNumber = $invoice->ref;
}
// Get material description from first line item or use generic term
$material = 'Material';
if (!empty($this->result['lines'])) {
// Try to get a meaningful description from line items
$firstLine = reset($this->result['lines']);
if (!empty($firstLine['name'])) {
// Use first product name, shortened
$material = dol_sanitizeFileName($firstLine['name']);
if (strlen($material) > 25) {
$material = substr($material, 0, 25);
}
}
// If multiple lines, indicate it
if (count($this->result['lines']) > 1) {
$material .= ' ua'; // "und andere" / "and others"
}
}
// Price rounded
$price = round($invoice->total_ttc);
// Build filename
$filename = sprintf(
'%s - %s - %s - %s - %d EUR.pdf',
$date,
$supplierName,
$invoiceNumber,
$material,
$price
);
// Clean up any double spaces or invalid characters
$filename = preg_replace('/\s+/', ' ', $filename);
$filename = str_replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], '-', $filename);
return $filename;
}
/**
* Validate that ZUGFeRD totals match Dolibarr calculated totals
*
* @param array $invoice_data Parsed ZUGFeRD invoice data
* @param FactureFournisseur $invoice Created Dolibarr invoice
* @return array Validation result with status and message
*/
public function validateTotals($invoice_data, $invoice)
{
global $langs;
$langs->load('importzugferd@importzugferd');
$result = array(
'valid' => true,
'zugferd_ht' => (float) $invoice_data['totals']['net'],
'zugferd_ttc' => (float) $invoice_data['totals']['gross'],
'dolibarr_ht' => (float) $invoice->total_ht,
'dolibarr_ttc' => (float) $invoice->total_ttc,
'diff_ht' => 0,
'diff_ttc' => 0,
'message' => '',
);
$result['diff_ht'] = abs($result['zugferd_ht'] - $result['dolibarr_ht']);
$result['diff_ttc'] = abs($result['zugferd_ttc'] - $result['dolibarr_ttc']);
// Allow small deviations (max 0.05€ per total)
$tolerance = 0.05;
if ($result['diff_ht'] > $tolerance || $result['diff_ttc'] > $tolerance) {
$result['valid'] = false;
$result['message'] = $langs->trans(
'SumValidationError',
price($result['zugferd_ttc']),
price($result['dolibarr_ttc']),
price($result['diff_ttc'])
);
// Update import record with error
$this->import->status = ZugferdImport::STATUS_ERROR;
$this->import->error_message = $result['message'];
} else {
$result['message'] = $langs->trans('SumValidationOk');
// Keep status as PROCESSED (already set)
}
return $result;
}
/**
* Add a line to supplier invoice
*
* @param FactureFournisseur $invoice Invoice object
* @param array $line Line data
* @param User $user Current user
* @return int >0 if OK, <0 if error
*/
private function addInvoiceLine($invoice, $line, $user)
{
$desc = $line['name'];
if (!empty($line['description']) && $line['description'] != $line['name']) {
$desc .= "\n" . $line['description'];
}
// Add supplier reference to description if no product found
if ($line['fk_product'] == 0 && !empty($line['supplier_ref'])) {
$desc .= "\n[" . $line['supplier_ref'] . "]";
}
// Determine VAT rate
$tva_tx = $line['tax_percent'] ?: 19;
// Add line
$result = $invoice->addline(
$desc, // description
$line['unit_price'], // pu_ht
$tva_tx, // tva_tx
0, // localtax1_tx
0, // localtax2_tx
$line['quantity'], // qty
$line['fk_product'] ?: 0, // fk_product
0, // remise_percent
'', // date_start
'', // date_end
0, // ventil
0, // info_bits
'HT', // price_base_type
0, // type (0=product, 1=service)
-1, // rang
0, // notrigger
array(), // array_options
'', // fk_unit
0, // origin_id
0, // pu_ht_devise
$line['supplier_ref'] ?: '' // ref_supplier
);
if ($result < 0) {
$this->error = $invoice->error;
return -1;
}
// Update supplier price with EAN if product was matched and EAN is available
if ($line['fk_product'] > 0 && !empty($line['ean'])) {
$this->updateSupplierPriceBarcode($invoice->socid, $line['fk_product'], $line['ean'], $line['supplier_ref']);
}
// Check if this line has a metal surcharge associated and update extrafield
if ($line['fk_product'] > 0 && !empty($line['metal_surcharge']) && $line['metal_surcharge'] > 0) {
$this->updateSupplierPriceMetalSurcharge(
$invoice->socid,
$line['fk_product'],
$line['metal_surcharge'],
$line['supplier_ref']
);
}
return 1;
}
/**
* Check if a line is a metal surcharge line
*
* @param array $line Line data from invoice
* @return bool
*/
public function isMetalSurchargeLine($line)
{
$name = strtolower($line['product']['name'] ?? '');
$description = strtolower($line['product']['description'] ?? '');
$text = $name . ' ' . $description;
// Keywords that indicate metal surcharge
$keywords = array(
'metallzuschlag',
'kupferzuschlag',
'cu-zuschlag',
'cuzuschlag',
'metallnotierung',
'kupfernotierung',
'metal surcharge',
'copper surcharge',
'metallaufschlag',
'kupferaufschlag',
'mez ', // MEZ = Metallzuschlag (with space to avoid false positives)
' mez',
);
foreach ($keywords as $keyword) {
if (strpos($text, $keyword) !== false) {
return true;
}
}
return false;
}
/**
* Update metal surcharge extrafield on supplier price
*
* @param int $supplier_id Supplier ID
* @param int $product_id Product ID
* @param float $surcharge Metal surcharge amount per unit
* @param string $ref_fourn Supplier reference
* @return int >0 if updated, 0 if no update, <0 if error
*/
public function updateSupplierPriceMetalSurcharge($supplier_id, $product_id, $surcharge, $ref_fourn = '')
{
global $conf;
if ($surcharge <= 0) {
return 0;
}
// Find supplier price record
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
$sql .= " WHERE fk_soc = " . (int) $supplier_id;
$sql .= " AND fk_product = " . (int) $product_id;
$sql .= " AND entity IN (" . getEntity('product') . ")";
if (!empty($ref_fourn)) {
$sql .= " AND ref_fourn = '" . $this->db->escape($ref_fourn) . "'";
}
$sql .= " ORDER BY rowid DESC LIMIT 1";
$resql = $this->db->query($sql);
if ($resql && $this->db->num_rows($resql) > 0) {
$obj = $this->db->fetch_object($resql);
$price_id = $obj->rowid;
// Check if extrafield table exists
$sql_check = "SHOW TABLES LIKE '" . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields'";
$res_check = $this->db->query($sql_check);
if (!$res_check || $this->db->num_rows($res_check) == 0) {
dol_syslog("Extrafield table does not exist, skipping metal surcharge update", LOG_WARNING);
return 0;
}
// Check if record exists in extrafields
$sql_exists = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
$sql_exists .= " WHERE fk_object = " . (int) $price_id;
$res_exists = $this->db->query($sql_exists);
if ($res_exists && $this->db->num_rows($res_exists) > 0) {
// Update existing record
$sql_update = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
$sql_update .= " SET kupferzuschlag = " . (float) $surcharge;
$sql_update .= " WHERE fk_object = " . (int) $price_id;
} else {
// Insert new record
$sql_update = "INSERT INTO " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
$sql_update .= " (fk_object, kupferzuschlag) VALUES (" . (int) $price_id . ", " . (float) $surcharge . ")";
}
$res = $this->db->query($sql_update);
if ($res) {
dol_syslog("Updated metal surcharge for product " . $product_id . " supplier " . $supplier_id . " to " . $surcharge, LOG_INFO);
return 1;
} else {
dol_syslog("Error updating metal surcharge: " . $this->db->lasterror(), LOG_ERR);
return -1;
}
}
return 0; // No supplier price record found
}
/**
* Update barcode in supplier price record
*
* @param int $supplier_id Supplier ID
* @param int $product_id Product ID
* @param string $barcode EAN/GTIN barcode
* @param string $ref_fourn Supplier reference (optional, to identify correct price record)
* @return int >0 if updated, 0 if no update needed, <0 if error
*/
public function updateSupplierPriceBarcode($supplier_id, $product_id, $barcode, $ref_fourn = '')
{
global $conf;
// Check if barcode column exists in product_fournisseur_price table
if (!$this->checkSupplierPriceBarcodeColumn()) {
return 0; // Column doesn't exist, skip update
}
// Find supplier price record
$sql = "SELECT rowid, barcode FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
$sql .= " WHERE fk_soc = " . (int) $supplier_id;
$sql .= " AND fk_product = " . (int) $product_id;
$sql .= " AND entity IN (" . getEntity('product') . ")";
if (!empty($ref_fourn)) {
$sql .= " AND ref_fourn = '" . $this->db->escape($ref_fourn) . "'";
}
$sql .= " ORDER BY rowid DESC LIMIT 1";
$resql = $this->db->query($sql);
if ($resql && $this->db->num_rows($resql) > 0) {
$obj = $this->db->fetch_object($resql);
// Only update if barcode is empty or different
if (empty($obj->barcode) || $obj->barcode != $barcode) {
$sql_update = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
$sql_update .= " SET barcode = '" . $this->db->escape($barcode) . "'";
$sql_update .= " WHERE rowid = " . (int) $obj->rowid;
$res = $this->db->query($sql_update);
if ($res) {
dol_syslog("Updated supplier price barcode for product " . $product_id . " supplier " . $supplier_id . " to " . $barcode, LOG_DEBUG);
return 1;
} else {
return -1;
}
}
return 0; // No update needed
}
return 0; // No supplier price record found
}
/**
* Check if barcode column exists in product_fournisseur_price table
*
* @return bool
*/
private function checkSupplierPriceBarcodeColumn()
{
static $has_barcode_column = null;
if ($has_barcode_column === null) {
$sql = "SHOW COLUMNS FROM " . MAIN_DB_PREFIX . "product_fournisseur_price LIKE 'barcode'";
$resql = $this->db->query($sql);
$has_barcode_column = ($resql && $this->db->num_rows($resql) > 0);
}
return $has_barcode_column;
}
/**
* Delete existing import record by file hash (for reimport)
*
* @param string $file_hash File hash
* @param User $user Current user
* @return int >0 if deleted, 0 if not found, <0 if error
*/
public function deleteExistingImport($file_hash, $user)
{
global $conf;
// Find existing import by hash
$existingImport = new ZugferdImport($this->db);
$result = $existingImport->fetch(0, null, $file_hash);
if ($result > 0) {
// Delete the existing import record
$deleteResult = $existingImport->delete($user);
if ($deleteResult > 0) {
dol_syslog("Deleted existing import record " . $existingImport->ref . " for reimport", LOG_INFO);
return 1;
} else {
$this->error = $existingImport->error;
return -1;
}
}
return 0; // Not found
}
/**
* Copy PDF to documents folder
*
* @param string $pdf_path Source PDF path
* @param int $import_id Import record ID
* @return bool
*/
public function copyToDocuments($pdf_path, $import_id)
{
global $conf;
$destdir = $conf->importzugferd->dir_output . '/imports';
if (!is_dir($destdir)) {
dol_mkdir($destdir);
}
$destfile = $destdir . '/' . $this->import->ref . '_' . basename($pdf_path);
return copy($pdf_path, $destfile);
}
/**
* Get import result
*
* @return array
*/
public function getResult()
{
return $this->result;
}
/**
* Get parsed invoice data
*
* @return array
*/
public function getInvoiceData()
{
return $this->parser->getInvoiceData();
}
/**
* Hook to add dashboard line for new products
*
* @param array $parameters Parameters
* @param object $object Object
* @param string $action Action
* @param HookManager $hookmanager Hook manager
* @return int 0 = OK, >0 = number of errors
*/
public function addOpenElementsDashboardLine($parameters, &$object, &$action, $hookmanager)
{
global $langs, $user;
if (!$user->hasRight('produit', 'lire')) {
return 0;
}
require_once DOL_DOCUMENT_ROOT.'/core/class/workboardresponse.class.php';
$langs->load('importzugferd@importzugferd');
$sql = "SELECT COUNT(*) as total FROM " . MAIN_DB_PREFIX . "product";
$sql .= " WHERE entity IN (" . getEntity('product') . ")";
$sql .= " AND ref LIKE 'New%'";
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
$count = (int) $obj->total;
if ($count > 0) {
$response = new WorkboardResponse();
$response->warning_delay = 0;
$response->label = $langs->trans("NewProductsToReview");
$response->labelShort = $langs->trans("NewProductsToReview");
$response->url = dol_buildpath('/importzugferd/new_products.php', 1);
$response->img = 'product';
$response->nbtodo = $count;
$response->nbtodolate = 0;
$this->results['importzugferd_newproducts'] = $response;
}
}
return 0;
}
/**
* Hook to add dashboard group for new products
*
* @param array $parameters Parameters
* @param object $object Object
* @param string $action Action
* @param HookManager $hookmanager Hook manager
* @return int 0 = OK, >0 = number of errors
*/
public function addOpenElementsDashboardGroup($parameters, &$object, &$action, $hookmanager)
{
global $langs;
$langs->load('importzugferd@importzugferd');
$this->results['importzugferd_newproducts'] = array(
'groupName' => $langs->trans("NewProductsToReview"),
'stats' => array('importzugferd_newproducts'),
);
return 0;
}
}