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->db->escape($this->invoice_date) . "',"; $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 ? $this->fk_soc : "null") . ","; $sql .= " fk_facture_fourn = " . ($this->fk_facture_fourn > 0 ? $this->fk_facture_fourn : "null") . ","; $sql .= " status = " . (int) $this->status . ","; $sql .= " error_message = '" . $this->db->escape($this->error_message) . "',"; $sql .= " fk_user_modif = " . (int) $this->fk_user_modif; $sql .= " WHERE rowid = " . (int) $this->id; dol_syslog(get_class($this) . "::update", LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); 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; } /** * 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']); } }