760 lines
28 KiB
PHP
760 lines
28 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/zugferdimport.class.php
|
|
* \ingroup importzugferd
|
|
* \brief Class for ZUGFeRD import records
|
|
*/
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
|
|
|
/**
|
|
* Class ZugferdImport
|
|
* Manages imported ZUGFeRD invoices
|
|
*/
|
|
class ZugferdImport extends CommonObject
|
|
{
|
|
/**
|
|
* @var string ID to identify managed object
|
|
*/
|
|
public $element = 'zugferdimport';
|
|
|
|
/**
|
|
* @var string Name of table without prefix
|
|
*/
|
|
public $table_element = 'importzugferd_import';
|
|
|
|
/**
|
|
* @var int Does object support multicompany
|
|
*/
|
|
public $ismultientitymanaged = 1;
|
|
|
|
/**
|
|
* @var string Field with ID of parent key if object has a parent
|
|
*/
|
|
public $fk_element = 'fk_zugferdimport';
|
|
|
|
/**
|
|
* @var array Fields definition
|
|
*/
|
|
public $fields = array(
|
|
'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'position' => 1, 'notnull' => 1, 'visible' => 0, 'index' => 1),
|
|
'ref' => array('type' => 'varchar(128)', 'label' => 'Ref', 'enabled' => 1, 'position' => 10, 'notnull' => 1, 'visible' => 4, 'index' => 1, 'searchall' => 1),
|
|
'invoice_number' => array('type' => 'varchar(128)', 'label' => 'InvoiceNumber', 'enabled' => 1, 'position' => 20, 'notnull' => 1, 'visible' => 1, 'searchall' => 1),
|
|
'invoice_date' => array('type' => 'date', 'label' => 'InvoiceDate', 'enabled' => 1, 'position' => 30, 'notnull' => 1, 'visible' => 1),
|
|
'seller_name' => array('type' => 'varchar(255)', 'label' => 'SellerName', 'enabled' => 1, 'position' => 40, 'notnull' => 0, 'visible' => 1, 'searchall' => 1),
|
|
'seller_vat' => array('type' => 'varchar(50)', 'label' => 'SellerVAT', 'enabled' => 1, 'position' => 50, 'notnull' => 0, 'visible' => 1),
|
|
'buyer_reference' => array('type' => 'varchar(128)', 'label' => 'BuyerReference', 'enabled' => 1, 'position' => 60, 'notnull' => 0, 'visible' => 1),
|
|
'total_ht' => array('type' => 'price', 'label' => 'TotalHT', 'enabled' => 1, 'position' => 70, 'notnull' => 0, 'visible' => 1),
|
|
'total_ttc' => array('type' => 'price', 'label' => 'TotalTTC', 'enabled' => 1, 'position' => 80, 'notnull' => 0, 'visible' => 1),
|
|
'currency' => array('type' => 'varchar(3)', 'label' => 'Currency', 'enabled' => 1, 'position' => 90, 'notnull' => 0, 'visible' => 1, 'default' => 'EUR'),
|
|
'fk_soc' => array('type' => 'integer:Societe:societe/class/societe.class.php', 'label' => 'Supplier', 'enabled' => 1, 'position' => 100, 'notnull' => 0, 'visible' => 1),
|
|
'fk_facture_fourn' => array('type' => 'integer:FactureFournisseur:fourn/class/fournisseur.facture.class.php', 'label' => 'SupplierInvoice', 'enabled' => 1, 'position' => 110, 'notnull' => 0, 'visible' => 1),
|
|
'status' => array('type' => 'integer', 'label' => 'Status', 'enabled' => 1, 'position' => 500, 'notnull' => 1, 'visible' => 2, 'default' => 0, 'index' => 1, 'arrayofkeyval' => array(0 => 'Imported', 1 => 'Processed', 2 => 'Error')),
|
|
'error_message' => array('type' => 'text', 'label' => 'ErrorMessage', 'enabled' => 1, 'position' => 510, 'notnull' => 0, 'visible' => 0),
|
|
'file_hash' => array('type' => 'varchar(64)', 'label' => 'FileHash', 'enabled' => 1, 'position' => 520, 'notnull' => 0, 'visible' => 0),
|
|
'pdf_filename' => array('type' => 'varchar(255)', 'label' => 'PDFFilename', 'enabled' => 1, 'position' => 530, 'notnull' => 0, 'visible' => 1),
|
|
'date_creation' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'position' => 600, 'notnull' => 1, 'visible' => 2),
|
|
'date_import' => array('type' => 'datetime', 'label' => 'DateImport', 'enabled' => 1, 'position' => 610, 'notnull' => 0, 'visible' => 2),
|
|
'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'position' => 620, 'notnull' => 0, 'visible' => 0),
|
|
'fk_user_creat' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserCreator', 'enabled' => 1, 'position' => 700, 'notnull' => 0, 'visible' => 0),
|
|
'fk_user_modif' => array('type' => 'integer:User:User/class/user.class.php', 'label' => 'UserModifier', 'enabled' => 1, 'position' => 710, 'notnull' => 0, 'visible' => 0),
|
|
'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportKey', 'enabled' => 1, 'position' => 800, 'notnull' => 0, 'visible' => 0),
|
|
'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'position' => 900, 'notnull' => 1, 'visible' => 0, 'default' => 1, 'index' => 1),
|
|
);
|
|
|
|
/**
|
|
* @var string Ref
|
|
*/
|
|
public $ref;
|
|
|
|
/**
|
|
* @var string Invoice number from ZUGFeRD
|
|
*/
|
|
public $invoice_number;
|
|
|
|
/**
|
|
* @var string Invoice date
|
|
*/
|
|
public $invoice_date;
|
|
|
|
/**
|
|
* @var string Seller name
|
|
*/
|
|
public $seller_name;
|
|
|
|
/**
|
|
* @var string Seller VAT ID
|
|
*/
|
|
public $seller_vat;
|
|
|
|
/**
|
|
* @var string Buyer reference (our customer number at supplier)
|
|
*/
|
|
public $buyer_reference;
|
|
|
|
/**
|
|
* @var float Net total
|
|
*/
|
|
public $total_ht;
|
|
|
|
/**
|
|
* @var float Gross total
|
|
*/
|
|
public $total_ttc;
|
|
|
|
/**
|
|
* @var string Currency
|
|
*/
|
|
public $currency = 'EUR';
|
|
|
|
/**
|
|
* @var int Supplier ID
|
|
*/
|
|
public $fk_soc;
|
|
|
|
/**
|
|
* @var int Created supplier invoice ID
|
|
*/
|
|
public $fk_facture_fourn;
|
|
|
|
/**
|
|
* @var string XML content
|
|
*/
|
|
public $xml_content;
|
|
|
|
/**
|
|
* @var string PDF filename
|
|
*/
|
|
public $pdf_filename;
|
|
|
|
/**
|
|
* @var string File hash for duplicate detection
|
|
*/
|
|
public $file_hash;
|
|
|
|
/**
|
|
* @var int Status: 0=imported, 1=processed, 2=error
|
|
*/
|
|
public $status = 0;
|
|
|
|
/**
|
|
* @var string Error message
|
|
*/
|
|
public $error_message;
|
|
|
|
/**
|
|
* @var string Date creation
|
|
*/
|
|
public $date_creation;
|
|
|
|
/**
|
|
* @var string Date import
|
|
*/
|
|
public $date_import;
|
|
|
|
/**
|
|
* @var int User creator
|
|
*/
|
|
public $fk_user_creat;
|
|
|
|
/**
|
|
* @var int User modifier
|
|
*/
|
|
public $fk_user_modif;
|
|
|
|
/**
|
|
* @var string Import key
|
|
*/
|
|
public $import_key;
|
|
|
|
/**
|
|
* @var array Parsed line items
|
|
*/
|
|
public $lines = array();
|
|
|
|
/**
|
|
* Status constants
|
|
*/
|
|
const STATUS_IMPORTED = 0;
|
|
const STATUS_PROCESSED = 1;
|
|
const STATUS_ERROR = 2;
|
|
const STATUS_PENDING = 3; // Pending manual product assignment
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
$this->db = $db;
|
|
|
|
if (empty($conf->global->MAIN_SHOW_TECHNICAL_ID) && isset($this->fields['rowid'])) {
|
|
$this->fields['rowid']['visible'] = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create object into database
|
|
*
|
|
* @param User $user User that creates
|
|
* @param bool $notrigger false=launch triggers, true=disable triggers
|
|
* @return int <0 if KO, Id of created object if OK
|
|
*/
|
|
public function create($user, $notrigger = false)
|
|
{
|
|
global $conf;
|
|
|
|
$this->entity = $conf->entity;
|
|
|
|
if (empty($this->ref)) {
|
|
$this->ref = $this->getNextRef();
|
|
}
|
|
|
|
if (empty($this->date_creation)) {
|
|
$this->date_creation = dol_now();
|
|
}
|
|
|
|
$this->fk_user_creat = $user->id;
|
|
|
|
$sql = "INSERT INTO " . MAIN_DB_PREFIX . $this->table_element . " (";
|
|
$sql .= "ref, invoice_number, invoice_date, seller_name, seller_vat, buyer_reference,";
|
|
$sql .= "total_ht, total_ttc, currency, fk_soc, fk_facture_fourn,";
|
|
$sql .= "xml_content, pdf_filename, file_hash, status, error_message,";
|
|
$sql .= "date_creation, date_import, fk_user_creat, import_key, entity";
|
|
$sql .= ") VALUES (";
|
|
$sql .= "'" . $this->db->escape($this->ref) . "',";
|
|
$sql .= "'" . $this->db->escape($this->invoice_number) . "',";
|
|
$sql .= "'" . $this->db->escape($this->invoice_date) . "',";
|
|
$sql .= "'" . $this->db->escape($this->seller_name) . "',";
|
|
$sql .= "'" . $this->db->escape($this->seller_vat) . "',";
|
|
$sql .= "'" . $this->db->escape($this->buyer_reference) . "',";
|
|
$sql .= price2num($this->total_ht) . ",";
|
|
$sql .= price2num($this->total_ttc) . ",";
|
|
$sql .= "'" . $this->db->escape($this->currency) . "',";
|
|
$sql .= ($this->fk_soc > 0 ? $this->fk_soc : "null") . ",";
|
|
$sql .= ($this->fk_facture_fourn > 0 ? $this->fk_facture_fourn : "null") . ",";
|
|
// Normalize XML before storing (compact format without whitespace)
|
|
$normalizedXml = self::normalizeXml($this->xml_content);
|
|
$sql .= "'" . $this->db->escape($normalizedXml) . "',";
|
|
$sql .= "'" . $this->db->escape($this->pdf_filename) . "',";
|
|
$sql .= "'" . $this->db->escape($this->file_hash) . "',";
|
|
$sql .= (int) $this->status . ",";
|
|
$sql .= "'" . $this->db->escape($this->error_message) . "',";
|
|
$sql .= "'" . $this->db->escape($this->db->idate($this->date_creation)) . "',";
|
|
$sql .= ($this->date_import ? "'" . $this->db->escape($this->db->idate($this->date_import)) . "'" : "null") . ",";
|
|
$sql .= (int) $this->fk_user_creat . ",";
|
|
$sql .= "'" . $this->db->escape($this->import_key) . "',";
|
|
$sql .= (int) $this->entity;
|
|
$sql .= ")";
|
|
|
|
$this->db->begin();
|
|
|
|
dol_syslog(get_class($this) . "::create", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if (!$resql) {
|
|
$this->error = $this->db->lasterror();
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
|
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX . $this->table_element);
|
|
|
|
$this->db->commit();
|
|
return $this->id;
|
|
}
|
|
|
|
/**
|
|
* Load object in memory from database
|
|
*
|
|
* @param int $id Id object
|
|
* @param string $ref Ref
|
|
* @param string $file_hash File hash
|
|
* @return int <0 if KO, 0 if not found, >0 if OK
|
|
*/
|
|
public function fetch($id, $ref = null, $file_hash = null)
|
|
{
|
|
global $conf;
|
|
|
|
$sql = "SELECT rowid, ref, invoice_number, invoice_date, seller_name, seller_vat, buyer_reference,";
|
|
$sql .= " total_ht, total_ttc, currency, fk_soc, fk_facture_fourn,";
|
|
$sql .= " xml_content, pdf_filename, file_hash, status, error_message,";
|
|
$sql .= " date_creation, date_import, tms, fk_user_creat, fk_user_modif, import_key, entity";
|
|
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
|
$sql .= " WHERE entity IN (" . getEntity($this->table_element) . ")";
|
|
|
|
if ($id) {
|
|
$sql .= " AND rowid = " . (int) $id;
|
|
} elseif ($ref) {
|
|
$sql .= " AND ref = '" . $this->db->escape($ref) . "'";
|
|
} elseif ($file_hash) {
|
|
$sql .= " AND file_hash = '" . $this->db->escape($file_hash) . "'";
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
dol_syslog(get_class($this) . "::fetch", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql) {
|
|
if ($this->db->num_rows($resql)) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
|
|
$this->id = $obj->rowid;
|
|
$this->ref = $obj->ref;
|
|
$this->invoice_number = $obj->invoice_number;
|
|
$this->invoice_date = $this->db->jdate($obj->invoice_date);
|
|
$this->seller_name = $obj->seller_name;
|
|
$this->seller_vat = $obj->seller_vat;
|
|
$this->buyer_reference = $obj->buyer_reference;
|
|
$this->total_ht = $obj->total_ht;
|
|
$this->total_ttc = $obj->total_ttc;
|
|
$this->currency = $obj->currency;
|
|
$this->fk_soc = $obj->fk_soc;
|
|
$this->fk_facture_fourn = $obj->fk_facture_fourn;
|
|
$this->xml_content = $obj->xml_content;
|
|
$this->pdf_filename = $obj->pdf_filename;
|
|
$this->file_hash = $obj->file_hash;
|
|
$this->status = $obj->status;
|
|
$this->error_message = $obj->error_message;
|
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
|
$this->date_import = $this->db->jdate($obj->date_import);
|
|
$this->tms = $this->db->jdate($obj->tms);
|
|
$this->fk_user_creat = $obj->fk_user_creat;
|
|
$this->fk_user_modif = $obj->fk_user_modif;
|
|
$this->import_key = $obj->import_key;
|
|
$this->entity = $obj->entity;
|
|
|
|
$this->db->free($resql);
|
|
return 1;
|
|
} else {
|
|
$this->db->free($resql);
|
|
return 0;
|
|
}
|
|
} else {
|
|
$this->error = $this->db->lasterror();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update object in database
|
|
*
|
|
* @param User $user User that modifies
|
|
* @param bool $notrigger false=launch triggers, true=disable triggers
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function update($user, $notrigger = false)
|
|
{
|
|
$this->fk_user_modif = $user->id;
|
|
|
|
$sql = "UPDATE " . MAIN_DB_PREFIX . $this->table_element . " SET";
|
|
$sql .= " ref = '" . $this->db->escape($this->ref) . "',";
|
|
$sql .= " invoice_number = '" . $this->db->escape($this->invoice_number) . "',";
|
|
$sql .= " invoice_date = " . ($this->invoice_date ? "'" . $this->db->idate($this->invoice_date) . "'" : "null") . ",";
|
|
$sql .= " seller_name = '" . $this->db->escape($this->seller_name) . "',";
|
|
$sql .= " seller_vat = '" . $this->db->escape($this->seller_vat) . "',";
|
|
$sql .= " buyer_reference = '" . $this->db->escape($this->buyer_reference) . "',";
|
|
$sql .= " total_ht = " . price2num($this->total_ht) . ",";
|
|
$sql .= " total_ttc = " . price2num($this->total_ttc) . ",";
|
|
$sql .= " currency = '" . $this->db->escape($this->currency) . "',";
|
|
$sql .= " fk_soc = " . ($this->fk_soc > 0 ? (int) $this->fk_soc : "null") . ",";
|
|
$sql .= " fk_facture_fourn = " . ($this->fk_facture_fourn > 0 ? (int) $this->fk_facture_fourn : "null") . ",";
|
|
$sql .= " status = " . (int) $this->status . ",";
|
|
$sql .= " date_import = " . ($this->date_import ? "'" . $this->db->idate($this->date_import) . "'" : "null") . ",";
|
|
$sql .= " error_message = " . ($this->error_message ? "'" . $this->db->escape($this->error_message) . "'" : "null") . ",";
|
|
$sql .= " fk_user_modif = " . (int) $this->fk_user_modif;
|
|
$sql .= " WHERE rowid = " . (int) $this->id;
|
|
|
|
dol_syslog(get_class($this) . "::update sql=" . $sql, LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if (!$resql) {
|
|
$this->error = $this->db->lasterror();
|
|
dol_syslog(get_class($this) . "::update error=" . $this->error, LOG_ERR);
|
|
return -1;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Delete object from database
|
|
*
|
|
* @param User $user User that deletes
|
|
* @param bool $notrigger false=launch triggers, true=disable triggers
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function delete($user, $notrigger = false)
|
|
{
|
|
$sql = "DELETE FROM " . MAIN_DB_PREFIX . $this->table_element;
|
|
$sql .= " WHERE rowid = " . (int) $this->id;
|
|
|
|
dol_syslog(get_class($this) . "::delete", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if (!$resql) {
|
|
$this->error = $this->db->lasterror();
|
|
return -1;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Check if file already imported (duplicate detection)
|
|
*
|
|
* @param string $file_hash SHA256 hash of file
|
|
* @return bool true if already exists
|
|
*/
|
|
public function isDuplicate($file_hash)
|
|
{
|
|
global $conf;
|
|
|
|
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . $this->table_element;
|
|
$sql .= " WHERE file_hash = '" . $this->db->escape($file_hash) . "'";
|
|
$sql .= " AND entity = " . (int) $conf->entity;
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get next reference number
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getNextRef()
|
|
{
|
|
global $conf;
|
|
|
|
$sql = "SELECT MAX(CAST(SUBSTRING(ref, 4) AS UNSIGNED)) as maxref";
|
|
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
|
$sql .= " WHERE ref LIKE 'ZI-%'";
|
|
$sql .= " AND entity = " . (int) $conf->entity;
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
$num = $obj->maxref ? $obj->maxref + 1 : 1;
|
|
return 'ZI-' . str_pad($num, 6, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
return 'ZI-000001';
|
|
}
|
|
|
|
/**
|
|
* Normalize XML for database storage
|
|
* Removes whitespace between tags to store compact XML
|
|
*
|
|
* @param string $xml XML content
|
|
* @return string Normalized XML
|
|
*/
|
|
public static function normalizeXml($xml)
|
|
{
|
|
if (empty($xml)) {
|
|
return '';
|
|
}
|
|
|
|
$dom = new DOMDocument('1.0', 'UTF-8');
|
|
$dom->preserveWhiteSpace = false;
|
|
$dom->formatOutput = false;
|
|
|
|
// Try to load XML
|
|
if (@$dom->loadXML($xml)) {
|
|
// Return compact XML without declaration
|
|
$result = $dom->saveXML($dom->documentElement);
|
|
return $result ? $result : $xml;
|
|
}
|
|
|
|
// Fallback: just remove whitespace between tags
|
|
return preg_replace('/>\s+</', '><', trim($xml));
|
|
}
|
|
|
|
/**
|
|
* Format XML for display
|
|
* Takes compact XML and formats it with proper indentation
|
|
*
|
|
* @param string $xml Compact XML content
|
|
* @return string Formatted XML
|
|
*/
|
|
public static function formatXmlForDisplay($xml)
|
|
{
|
|
if (empty($xml)) {
|
|
return '';
|
|
}
|
|
|
|
// Clean up any escaped newlines from old data (literal \n strings)
|
|
$xml = str_replace('\n', '', $xml);
|
|
$xml = str_replace('\r', '', $xml);
|
|
$xml = str_replace('\t', '', $xml);
|
|
|
|
$dom = new DOMDocument('1.0', 'UTF-8');
|
|
$dom->preserveWhiteSpace = false;
|
|
$dom->formatOutput = true;
|
|
|
|
if (@$dom->loadXML($xml)) {
|
|
return $dom->saveXML();
|
|
}
|
|
|
|
// Fallback: return as-is
|
|
return $xml;
|
|
}
|
|
|
|
/**
|
|
* Import a ZUGFeRD invoice from PDF file
|
|
* This is the main entry point for batch/automated imports
|
|
*
|
|
* @param User $user User performing import
|
|
* @param string $file_path Path to PDF file
|
|
* @param bool $auto_create_invoice Whether to auto-create supplier invoice
|
|
* @return int >0 (import ID) if OK, -2 if duplicate, <0 if error
|
|
*/
|
|
public function importFromFile($user, $file_path, $auto_create_invoice = false)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
$langs->load('importzugferd@importzugferd');
|
|
|
|
dol_include_once('/importzugferd/class/zugferdparser.class.php');
|
|
dol_include_once('/importzugferd/class/productmapping.class.php');
|
|
dol_include_once('/importzugferd/class/importline.class.php');
|
|
dol_include_once('/importzugferd/class/importnotification.class.php');
|
|
|
|
// Parse PDF
|
|
$parser = new ZugferdParser($this->db);
|
|
$result = $parser->extractFromPdf($file_path);
|
|
if ($result < 0) {
|
|
$this->error = $parser->error;
|
|
return -1;
|
|
}
|
|
|
|
$result = $parser->parse();
|
|
if ($result < 0) {
|
|
$this->error = $parser->error;
|
|
return -1;
|
|
}
|
|
|
|
$invoice_data = $parser->getInvoiceData();
|
|
|
|
// Check for duplicates
|
|
$file_hash = $parser->getFileHash($file_path);
|
|
if ($this->isDuplicate($file_hash)) {
|
|
$this->error = $langs->trans('ErrorDuplicateInvoice');
|
|
return -2; // Duplicate
|
|
}
|
|
|
|
// Find supplier
|
|
$supplier_id = $this->findSupplier($invoice_data);
|
|
|
|
// Set import record data
|
|
$this->invoice_number = $invoice_data['invoice_number'];
|
|
$this->invoice_date = $invoice_data['invoice_date'];
|
|
$this->seller_name = $invoice_data['seller']['name'];
|
|
$this->seller_vat = $invoice_data['seller']['vat_id'];
|
|
$this->buyer_reference = $invoice_data['buyer']['reference'] ?: $invoice_data['buyer']['id'];
|
|
$this->total_ht = $invoice_data['totals']['net'];
|
|
$this->total_ttc = $invoice_data['totals']['gross'];
|
|
$this->currency = $invoice_data['totals']['currency'] ?: 'EUR';
|
|
$this->fk_soc = $supplier_id;
|
|
$this->xml_content = $parser->getXmlContent();
|
|
$this->pdf_filename = basename($file_path);
|
|
$this->file_hash = $file_hash;
|
|
$this->date_import = dol_now();
|
|
|
|
// Create import record
|
|
$import_id = $this->create($user);
|
|
if ($import_id < 0) {
|
|
return -3;
|
|
}
|
|
|
|
// Process and store line items
|
|
$mapping = new ProductMapping($this->db);
|
|
$unmatched_count = 0;
|
|
$matched_count = 0;
|
|
$total_lines = count($invoice_data['lines']);
|
|
|
|
foreach ($invoice_data['lines'] as $line_data) {
|
|
$line = new ImportLine($this->db);
|
|
$line->fk_import = $import_id;
|
|
$line->line_id = $line_data['line_id'];
|
|
$line->supplier_ref = $line_data['product']['seller_id'];
|
|
$line->product_name = $line_data['product']['name'];
|
|
$line->description = $line_data['product']['description'];
|
|
$line->quantity = $line_data['quantity'];
|
|
$line->unit_code = $line_data['unit_code'];
|
|
$line->unit_price = $line_data['unit_price'];
|
|
$line->unit_price_raw = isset($line_data['unit_price_raw']) ? $line_data['unit_price_raw'] : $line_data['unit_price'];
|
|
$line->basis_quantity = isset($line_data['basis_quantity']) ? $line_data['basis_quantity'] : 1;
|
|
$line->basis_quantity_unit = isset($line_data['basis_quantity_unit']) ? $line_data['basis_quantity_unit'] : '';
|
|
$line->line_total = $line_data['line_total'];
|
|
$line->tax_percent = $line_data['tax_percent'];
|
|
$line->ean = $line_data['product']['global_id'];
|
|
|
|
// Try to match product
|
|
$fk_product = 0;
|
|
$match_method = '';
|
|
|
|
if ($supplier_id > 0) {
|
|
$match = $mapping->findProduct($supplier_id, $line_data['product']);
|
|
if (!empty($match) && $match['fk_product'] > 0) {
|
|
$fk_product = $match['fk_product'];
|
|
$match_method = $match['method'];
|
|
}
|
|
}
|
|
|
|
$line->fk_product = $fk_product;
|
|
$line->match_method = $match_method;
|
|
|
|
if ($fk_product == 0) {
|
|
$unmatched_count++;
|
|
} else {
|
|
$matched_count++;
|
|
}
|
|
|
|
$line->create($user);
|
|
}
|
|
|
|
// Determine status based on matching results
|
|
// STATUS_IMPORTED only if: supplier found, has lines, and ALL lines have matched products
|
|
if ($supplier_id == 0 || $total_lines == 0 || $unmatched_count > 0 || $matched_count == 0) {
|
|
// Missing supplier, no lines, unmatched products, or no matches at all - needs manual intervention
|
|
$this->status = self::STATUS_PENDING;
|
|
} else {
|
|
// All lines matched
|
|
$this->status = self::STATUS_IMPORTED;
|
|
}
|
|
|
|
// Copy PDF to documents
|
|
$destdir = $conf->importzugferd->dir_output . '/imports';
|
|
if (!is_dir($destdir)) {
|
|
dol_mkdir($destdir);
|
|
}
|
|
$destfile = $destdir . '/' . $this->ref . '_' . basename($file_path);
|
|
copy($file_path, $destfile);
|
|
|
|
// Update status
|
|
$this->update($user);
|
|
|
|
// Send notification if manual intervention required
|
|
if ($this->status == self::STATUS_PENDING && class_exists('ImportNotification')) {
|
|
$notification = new ImportNotification($this->db);
|
|
$importLine = new ImportLine($this->db);
|
|
$storedLines = $importLine->fetchAllByImport($this->id);
|
|
$notification->sendManualInterventionNotification($this, $storedLines);
|
|
}
|
|
|
|
return $import_id;
|
|
}
|
|
|
|
/**
|
|
* Find supplier by buyer reference or VAT ID
|
|
*
|
|
* @param array $invoice_data Parsed invoice data
|
|
* @return int Supplier ID or 0
|
|
*/
|
|
protected 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;
|
|
}
|
|
|
|
/**
|
|
* Get status label
|
|
*
|
|
* @param int $mode 0=short, 1=long
|
|
* @return string
|
|
*/
|
|
public function getLibStatut($mode = 0)
|
|
{
|
|
return $this->LibStatut($this->status, $mode);
|
|
}
|
|
|
|
/**
|
|
* Return status label for a given status
|
|
*
|
|
* @param int $status Status
|
|
* @param int $mode 0=short, 1=long
|
|
* @return string
|
|
*/
|
|
public function LibStatut($status, $mode = 0)
|
|
{
|
|
global $langs;
|
|
|
|
$langs->load('importzugferd@importzugferd');
|
|
|
|
$statusLabels = array(
|
|
self::STATUS_IMPORTED => array('short' => 'Imported', 'long' => 'StatusImported', 'class' => 'status4'),
|
|
self::STATUS_PROCESSED => array('short' => 'Processed', 'long' => 'StatusProcessed', 'class' => 'status6'),
|
|
self::STATUS_ERROR => array('short' => 'Error', 'long' => 'StatusError', 'class' => 'status8'),
|
|
self::STATUS_PENDING => array('short' => 'Pending', 'long' => 'StatusPending', 'class' => 'status1'),
|
|
);
|
|
|
|
$statusType = isset($statusLabels[$status]) ? $statusLabels[$status] : $statusLabels[0];
|
|
$label = $mode == 0 ? $statusType['short'] : $statusType['long'];
|
|
|
|
return dolGetStatus($langs->trans($label), '', '', $statusType['class']);
|
|
}
|
|
}
|