Sucht jetzt auch im alten Format /imports/{ref}_{filename}
falls die Datei nicht im neuen Pfad /imports/{id}/{filename} liegt.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3428 lines
165 KiB
PHP
Executable file
3428 lines
165 KiB
PHP
Executable file
<?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');
|
||
// Zeilenspezifische Produkt-IDs (wegen eindeutiger select2-IDs pro Zeile)
|
||
if (empty($product_id) && $line_id > 0) {
|
||
$product_id = GETPOST('product_id_'.$line_id, 'int');
|
||
}
|
||
if (empty($template_product_id) && $line_id > 0) {
|
||
$template_product_id = GETPOST('template_product_id_'.$line_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 = '';
|
||
|
||
/*
|
||
* Helper-Funktionen (DRY)
|
||
*/
|
||
|
||
/**
|
||
* Parse Aderanzahl und Querschnitt aus Kabelbezeichnung
|
||
* Erkennt Formate wie: NYM-J 3x2,5 / NYM-J 5x1.5 / H07V-K 1x4 / J-Y(ST)Y 2x2x0,8 etc.
|
||
*
|
||
* @param string $text Kabelbezeichnung (z.B. "NYM-J 3x2,5 Eca Ri100")
|
||
* @return array|null Array mit 'aderanzahl', 'querschnitt' oder null wenn kein Kabel
|
||
*/
|
||
function parseCableSpecsFromText($text)
|
||
{
|
||
// Spezialfall: Fernmeldekabel wie J-Y(ST)Y 2x2x0,8 (Paare x Adern pro Paar x Querschnitt)
|
||
// Pattern: Zahl x Zahl x Zahl (z.B. 2x2x0,8 = 4 Adern mit 0,8mm²)
|
||
if (preg_match('/(\d+)\s*[xX]\s*(\d+)\s*[xX]\s*(\d+(?:[,\.]\d+)?)/', $text, $matches)) {
|
||
$paare = (int) $matches[1];
|
||
$adernProPaar = (int) $matches[2];
|
||
$querschnitt = (float) str_replace(',', '.', $matches[3]);
|
||
$aderanzahl = $paare * $adernProPaar;
|
||
|
||
// Plausibilitätsprüfung
|
||
if ($aderanzahl >= 1 && $aderanzahl <= 200 && $querschnitt >= 0.14 && $querschnitt <= 400) {
|
||
return array(
|
||
'aderanzahl' => $aderanzahl,
|
||
'querschnitt' => $querschnitt
|
||
);
|
||
}
|
||
}
|
||
|
||
// Standard: NYM-J 3x2,5 (Adern x Querschnitt)
|
||
// Pattern: Zahl x Zahl (mit Komma oder Punkt als Dezimaltrenner)
|
||
if (preg_match('/(\d+)\s*[xX]\s*(\d+(?:[,\.]\d+)?)/', $text, $matches)) {
|
||
$aderanzahl = (int) $matches[1];
|
||
$querschnitt = (float) str_replace(',', '.', $matches[2]);
|
||
|
||
// Plausibilitätsprüfung
|
||
if ($aderanzahl >= 1 && $aderanzahl <= 100 && $querschnitt >= 0.5 && $querschnitt <= 400) {
|
||
return array(
|
||
'aderanzahl' => $aderanzahl,
|
||
'querschnitt' => $querschnitt
|
||
);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Berechne Kupfergehalt aus Aderanzahl und Querschnitt
|
||
* Formel: Aderanzahl × Querschnitt × 8.9 (Dichte Kupfer) = kg/km
|
||
*
|
||
* @param int $aderanzahl Anzahl der Adern
|
||
* @param float $querschnitt Querschnitt in mm²
|
||
* @return float Kupfergehalt in kg/km
|
||
*/
|
||
function calculateKupfergehalt($aderanzahl, $querschnitt)
|
||
{
|
||
// Kupferdichte: 8.9 g/cm³ = 8.9 kg/dm³
|
||
// 1 mm² × 1 km = 1 mm² × 1000m = 1000 mm³ = 1 cm³
|
||
// Also: 1 mm² Querschnitt × 1 km Länge = 1000 cm³ = 1 dm³ = 8.9 kg
|
||
return $aderanzahl * $querschnitt * 8.9;
|
||
}
|
||
|
||
/**
|
||
* Hole aktuellen Kupferpreis aus Metallzuschlag-Modul
|
||
*
|
||
* @param DoliDB $db Datenbank
|
||
* @param int $supplierId Lieferanten-ID (optional, für lieferantenspezifischen Preis)
|
||
* @return float CU-Notiz in EUR/100kg oder 0 wenn nicht verfügbar
|
||
*/
|
||
function getCurrentCopperPrice($db, $supplierId = 0)
|
||
{
|
||
// Erst prüfen ob Metallzuschlag-Modul aktiv ist
|
||
if (!isModEnabled('metallzuschlag')) {
|
||
return 0;
|
||
}
|
||
|
||
// Lieferanten-spezifischer CU-Wert (aus societe_extrafields)
|
||
if ($supplierId > 0) {
|
||
$sql = "SELECT metallzuschlag_cu FROM ".MAIN_DB_PREFIX."societe_extrafields";
|
||
$sql .= " WHERE fk_object = ".(int)$supplierId;
|
||
$resql = $db->query($sql);
|
||
if ($resql && $db->num_rows($resql) > 0) {
|
||
$obj = $db->fetch_object($resql);
|
||
if (!empty($obj->metallzuschlag_cu) && (float)$obj->metallzuschlag_cu > 0) {
|
||
return (float)$obj->metallzuschlag_cu;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallback: Aktuellster CU-Wert aus History
|
||
$sql = "SELECT value FROM ".MAIN_DB_PREFIX."metallzuschlag_history";
|
||
$sql .= " WHERE metal = 'CU' ORDER BY date_notiz DESC LIMIT 1";
|
||
$resql = $db->query($sql);
|
||
if ($resql && $db->num_rows($resql) > 0) {
|
||
$obj = $db->fetch_object($resql);
|
||
return (float)$obj->value;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Berechne Kupferzuschlag für eine bestimmte Menge
|
||
* Formel: Kupfergehalt (kg/km) × CU (EUR/100kg) / 100000 × Menge
|
||
*
|
||
* @param float $kupfergehalt Kupfergehalt in kg/km
|
||
* @param float $cuPrice CU-Notiz in EUR/100kg
|
||
* @param float $quantity Menge (z.B. 100 für 100m)
|
||
* @return float Kupferzuschlag in EUR
|
||
*/
|
||
function calculateKupferzuschlag($kupfergehalt, $cuPrice, $quantity = 1)
|
||
{
|
||
if ($kupfergehalt <= 0 || $cuPrice <= 0) {
|
||
return 0;
|
||
}
|
||
// kg/km × EUR/100kg / 100000 × m = EUR
|
||
return round($kupfergehalt * $cuPrice / 100000 * $quantity, 2);
|
||
}
|
||
|
||
/**
|
||
* Prüft ob ein Produkt ein Kabel ist (basierend auf Warengruppe oder Bezeichnung)
|
||
*
|
||
* @param Datanorm $datanorm Datanorm-Objekt
|
||
* @return bool True wenn Kabel
|
||
*/
|
||
function isCableProduct($datanorm)
|
||
{
|
||
// Warengruppen die typisch für Kabel sind
|
||
$cableGroups = array('KAB', 'KABEL', 'LEI', 'LEIT', 'LEITUNG');
|
||
|
||
if (!empty($datanorm->product_group)) {
|
||
$group = strtoupper(substr($datanorm->product_group, 0, 5));
|
||
foreach ($cableGroups as $cg) {
|
||
if (strpos($group, $cg) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Typische Kabelbezeichnungen
|
||
$cablePatterns = array(
|
||
'/NYM[-\s]?[JYOA]/i',
|
||
'/NYY[-\s]?[JO]/i',
|
||
'/H0[357]V[-\s]?[KUR]/i',
|
||
'/H0[357]RN[-\s]?F/i',
|
||
'/NHXH/i',
|
||
'/J[-\s]?Y\(ST\)Y/i',
|
||
'/LiYCY/i',
|
||
'/ÖLFLEX/i',
|
||
);
|
||
|
||
$text = $datanorm->short_text1 . ' ' . $datanorm->short_text2;
|
||
foreach ($cablePatterns as $pattern) {
|
||
if (preg_match($pattern, $text)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Ringgröße aus Kabel-Bezeichnung extrahieren
|
||
* Erkennt Muster wie: Ri100, Ri.50, Ri 100, Ring100, Tr500, Fol.25m, "Ring 100m", "Trommel 500m"
|
||
*
|
||
* WICHTIG: Nur verwenden wenn price_unit = 1!
|
||
* Bei price_unit > 1 ist das bereits die korrekte Preiseinheit (z.B. 100 für 100m)
|
||
*
|
||
* @param string $text Produktbezeichnung
|
||
* @return int Ringgröße in Metern (0 wenn nicht gefunden)
|
||
*/
|
||
function extractCableRingSize($text)
|
||
{
|
||
// Muster für Ringgröße: Ri100, Ri.50, Ri 100, Ring100, Ring 50
|
||
if (preg_match('/Ri(?:ng)?[.\s]?(\d+)/i', $text, $matches)) {
|
||
return (int)$matches[1];
|
||
}
|
||
// Muster für "Ring 100m", "Ring 50 m"
|
||
if (preg_match('/Ring\s+(\d+)\s*m/i', $text, $matches)) {
|
||
return (int)$matches[1];
|
||
}
|
||
// Muster für Trommel: Tr500, Tr.500, Trommel500, "Trommel 500m"
|
||
if (preg_match('/Tr(?:ommel)?[.\s]?(\d+)/i', $text, $matches)) {
|
||
return (int)$matches[1];
|
||
}
|
||
if (preg_match('/Trommel\s+(\d+)\s*m/i', $text, $matches)) {
|
||
return (int)$matches[1];
|
||
}
|
||
// Muster für Folie/Rolle: Fol.25m, Fol25, Rol.50m
|
||
if (preg_match('/(?:Fol|Rol)[.\s]?(\d+)/i', $text, $matches)) {
|
||
return (int)$matches[1];
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Berechne Kabelpreis unter Berücksichtigung unterschiedlicher Lieferanten-Formate
|
||
*
|
||
* Logik:
|
||
* - Kluxen/Witte/eltric: price_unit > 1 (z.B. 100) → Preis ist für 100m
|
||
* - Sonepar: price_unit = 1 → Preis ist für kompletten Ring (Größe aus Name)
|
||
*
|
||
* @param Datanorm $datanorm Datanorm-Objekt
|
||
* @param float $minQty Mindestbestellmenge (default 1)
|
||
* @return array Array mit 'unitPrice', 'totalPrice', 'priceUnit'
|
||
*/
|
||
function calculateCablePricing($datanorm, $minQty = 1)
|
||
{
|
||
$priceUnit = $datanorm->price_unit > 0 ? $datanorm->price_unit : 1;
|
||
$cableText = $datanorm->short_text1 . ' ' . $datanorm->short_text2;
|
||
|
||
if ($priceUnit > 1) {
|
||
// Kluxen/Witte-Format: price_unit gibt die Preiseinheit an (z.B. 100m)
|
||
$unitPrice = $datanorm->price / $priceUnit;
|
||
$effectivePriceUnit = $priceUnit;
|
||
} else {
|
||
// Sonepar-Format: price_unit = 1, aber Preis ist für kompletten Ring
|
||
$ringSize = extractCableRingSize($cableText);
|
||
if ($ringSize > 0) {
|
||
$unitPrice = $datanorm->price / $ringSize;
|
||
$effectivePriceUnit = $ringSize;
|
||
} else {
|
||
// Einzelstück
|
||
$unitPrice = $datanorm->price;
|
||
$effectivePriceUnit = 1;
|
||
}
|
||
}
|
||
|
||
// Schutz gegen Division durch Null
|
||
$effectivePriceUnit = max(1, $effectivePriceUnit);
|
||
|
||
return array(
|
||
'unitPrice' => $unitPrice,
|
||
'totalPrice' => $unitPrice * $minQty,
|
||
'priceUnit' => $effectivePriceUnit
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Extrafields fuer Lieferantenpreis aus Datanorm-Daten zusammenstellen
|
||
*
|
||
* @param Datanorm $datanorm Datanorm-Objekt
|
||
* @param ImportLine|null $lineObj Import-Zeile (optional, fuer ZUGFeRD-Daten)
|
||
* @return array Extrafields-Array
|
||
*/
|
||
function datanormBuildSupplierPriceExtrafields($datanorm, $lineObj = null)
|
||
{
|
||
$extrafields = array();
|
||
|
||
// Produktpreis (reiner Materialpreis ohne Kupferzuschlag) - nur bei Kabeln mit Metallzuschlag
|
||
// Der Preis ist bereits auf Mindestmenge (price_unit) bezogen
|
||
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0 && !empty($datanorm->price)) {
|
||
$extrafields['options_produktpreis'] = $datanorm->price;
|
||
}
|
||
|
||
// Preiseinheit
|
||
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
|
||
$extrafields['options_preiseinheit'] = $datanorm->price_unit;
|
||
} elseif ($lineObj && !empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
|
||
$extrafields['options_preiseinheit'] = $lineObj->basis_quantity;
|
||
}
|
||
// Warengruppe
|
||
if (!empty($datanorm->product_group)) {
|
||
$extrafields['options_warengruppe'] = $datanorm->product_group;
|
||
}
|
||
return $extrafields;
|
||
}
|
||
|
||
/**
|
||
* Lieferantenpreis aus Datanorm hinzufuegen
|
||
*
|
||
* @param DoliDB $db Datenbank
|
||
* @param int $productId Produkt-ID
|
||
* @param Datanorm $datanorm Datanorm-Objekt
|
||
* @param Societe $supplier Lieferant-Objekt
|
||
* @param User $user Benutzer
|
||
* @param float $purchasePrice Einkaufspreis
|
||
* @param float $taxPercent MwSt-Satz
|
||
* @param array $extrafields Extrafields
|
||
* @return int >0 bei Erfolg, <0 bei Fehler
|
||
*/
|
||
function datanormAddSupplierPrice($db, $productId, $datanorm, $supplier, $user, $purchasePrice, $taxPercent = 19, $extrafields = array())
|
||
{
|
||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
|
||
|
||
$prodfourn = new ProductFournisseur($db);
|
||
$prodfourn->id = $productId;
|
||
|
||
$supplierEan = !empty($datanorm->ean) ? $datanorm->ean : '';
|
||
$supplierEanType = !empty($datanorm->ean) ? 2 : 0;
|
||
$description = trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : ''));
|
||
|
||
// Mindestbestellmenge und Verpackungseinheit vom bestehenden Lieferantenpreis übernehmen
|
||
// (gleiches Produkt = gleiche Mengen, nur anderer Lieferant)
|
||
$minQty = 1;
|
||
$packaging = null;
|
||
|
||
$sqlExisting = "SELECT quantity, packaging FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||
$sqlExisting .= " WHERE fk_product = " . (int)$productId;
|
||
$sqlExisting .= " AND quantity > 0";
|
||
$sqlExisting .= " ORDER BY rowid ASC LIMIT 1";
|
||
$resExisting = $db->query($sqlExisting);
|
||
if ($resExisting && $db->num_rows($resExisting) > 0) {
|
||
$objExisting = $db->fetch_object($resExisting);
|
||
if ($objExisting->quantity > 0) {
|
||
$minQty = $objExisting->quantity;
|
||
}
|
||
if (!empty($objExisting->packaging)) {
|
||
$packaging = $objExisting->packaging;
|
||
}
|
||
}
|
||
|
||
// Preis berechnen mit zentraler Funktion
|
||
$pricing = calculateCablePricing($datanorm, $minQty);
|
||
$totalPrice = $pricing['totalPrice'];
|
||
|
||
$result = $prodfourn->update_buyprice(
|
||
$minQty, $totalPrice, $user, 'HT', $supplier, 0,
|
||
$datanorm->article_number, $taxPercent,
|
||
0, 0, 0, 0, 0, 0, array(), '',
|
||
0, 'HT', 1, '',
|
||
$description, $supplierEan, $supplierEanType,
|
||
$extrafields
|
||
);
|
||
|
||
// Verpackungseinheit nachträglich setzen (nicht in update_buyprice verfügbar)
|
||
if ($result > 0 && !empty($packaging)) {
|
||
$sqlPkg = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||
$sqlPkg .= " SET packaging = " . (float)$packaging;
|
||
$sqlPkg .= " WHERE rowid = " . (int)$result;
|
||
$db->query($sqlPkg);
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Extrafields in product_fournisseur_price_extrafields einfuegen
|
||
*
|
||
* @param DoliDB $db Datenbank
|
||
* @param int $priceId ID des Lieferantenpreises
|
||
* @param array $extrafields Extrafields-Array
|
||
*/
|
||
function datanormInsertPriceExtrafields($db, $priceId, $extrafields)
|
||
{
|
||
if (empty($priceId) || empty($extrafields)) {
|
||
return;
|
||
}
|
||
// Note: kupferzuschlag is NOT set here - it's calculated by a separate module
|
||
// based on copper content (kupfergehalt) and current copper price
|
||
$produktpreis = !empty($extrafields['options_produktpreis']) ? (float)$extrafields['options_produktpreis'] : 'NULL';
|
||
$preiseinheit = !empty($extrafields['options_preiseinheit']) ? (int)$extrafields['options_preiseinheit'] : 1;
|
||
$warengruppe = !empty($extrafields['options_warengruppe']) ? "'".$db->escape($extrafields['options_warengruppe'])."'" : 'NULL';
|
||
|
||
// Pruefen ob bereits vorhanden
|
||
$sqlCheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields WHERE fk_object = ".(int)$priceId;
|
||
$resCheck = $db->query($sqlCheck);
|
||
if ($resCheck && $db->num_rows($resCheck) > 0) {
|
||
// Update statt Insert wenn bereits vorhanden
|
||
$sql = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields SET ";
|
||
$sql .= "produktpreis = ".($produktpreis === 'NULL' ? "NULL" : $produktpreis).", ";
|
||
$sql .= "preiseinheit = ".$preiseinheit.", ";
|
||
$sql .= "warengruppe = ".$warengruppe." ";
|
||
$sql .= "WHERE fk_object = ".(int)$priceId;
|
||
if (!$db->query($sql)) {
|
||
dol_syslog('ImportZugferd: Fehler beim Update der Extrafields: '.$db->lasterror(), LOG_ERR);
|
||
}
|
||
return;
|
||
}
|
||
|
||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
|
||
$sql .= " (fk_object, produktpreis, preiseinheit, warengruppe) VALUES (";
|
||
$sql .= (int)$priceId.", ";
|
||
$sql .= ($produktpreis === 'NULL' ? "NULL" : $produktpreis).", ";
|
||
$sql .= $preiseinheit.", ";
|
||
$sql .= $warengruppe.")";
|
||
if (!$db->query($sql)) {
|
||
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Actions
|
||
*/
|
||
|
||
// AJAX: Get raw Datanorm lines for debugging
|
||
if ($action == 'get_raw_lines' && GETPOST('article_number', 'alphanohtml')) {
|
||
header('Content-Type: application/json');
|
||
$article_number = GETPOST('article_number', 'alphanohtml');
|
||
$ajax_fk_soc = GETPOSTINT('fk_soc');
|
||
|
||
$result = array(
|
||
'datanorm_line' => '',
|
||
'datpreis_line' => '',
|
||
'article_number' => $article_number
|
||
);
|
||
|
||
// Get the upload directory for this supplier
|
||
$upload_dir = $conf->importzugferd->dir_output.'/datanorm/'.$ajax_fk_soc;
|
||
|
||
if (is_dir($upload_dir)) {
|
||
$allFiles = glob($upload_dir . '/*');
|
||
|
||
// Search in DATANORM files
|
||
foreach ($allFiles as $file) {
|
||
$basename = strtoupper(basename($file));
|
||
if (preg_match('/^DATANORM\.\d{3}$/', $basename)) {
|
||
$handle = fopen($file, 'r');
|
||
if ($handle) {
|
||
while (($line = fgets($handle)) !== false) {
|
||
// A-Satz starts with A; and contains the article number
|
||
if (preg_match('/^A;/', $line)) {
|
||
$parts = explode(';', $line);
|
||
if (isset($parts[2]) && trim($parts[2]) == $article_number) {
|
||
$result['datanorm_line'] = trim($line);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
fclose($handle);
|
||
}
|
||
if (!empty($result['datanorm_line'])) break;
|
||
}
|
||
}
|
||
|
||
// Search in DATPREIS files
|
||
foreach ($allFiles as $file) {
|
||
$basename = strtoupper(basename($file));
|
||
if (preg_match('/^DATPREIS\.\d{3}$/', $basename)) {
|
||
$handle = fopen($file, 'r');
|
||
if ($handle) {
|
||
while (($line = fgets($handle)) !== false) {
|
||
// P-Satz contains article numbers at various positions
|
||
if (preg_match('/^P;/', $line) && strpos($line, $article_number) !== false) {
|
||
$result['datpreis_line'] = trim($line);
|
||
break;
|
||
}
|
||
}
|
||
fclose($handle);
|
||
}
|
||
if (!empty($result['datpreis_line'])) break;
|
||
}
|
||
}
|
||
|
||
$result['upload_dir'] = $upload_dir;
|
||
} else {
|
||
$result['error'] = 'Upload directory not found: ' . $upload_dir;
|
||
}
|
||
|
||
echo json_encode($result);
|
||
exit;
|
||
}
|
||
|
||
// Upload and parse PDF - creates import record immediately
|
||
if ($action == 'upload') {
|
||
if (!empty($_FILES['zugferd_file']['tmp_name'])) {
|
||
$upload_dir = $conf->importzugferd->dir_output.'/temp';
|
||
if (!is_dir($upload_dir)) {
|
||
dol_mkdir($upload_dir);
|
||
}
|
||
|
||
$filename = dol_sanitizeFileName($_FILES['zugferd_file']['name']);
|
||
$destfile = $upload_dir.'/'.$filename;
|
||
|
||
if (move_uploaded_file($_FILES['zugferd_file']['tmp_name'], $destfile)) {
|
||
$force_reimport = GETPOST('force_reimport', 'int');
|
||
|
||
// Check for duplicate
|
||
$file_hash = hash_file('sha256', $destfile);
|
||
$isDuplicate = $import->isDuplicate($file_hash);
|
||
|
||
if ($isDuplicate && !$force_reimport) {
|
||
$error++;
|
||
$message = $langs->trans('ErrorDuplicateInvoice');
|
||
@unlink($destfile);
|
||
} else {
|
||
// If force reimport, delete the old record first
|
||
if ($isDuplicate && $force_reimport) {
|
||
$oldImport = new ZugferdImport($db);
|
||
$oldImport->fetch(0, null, $file_hash);
|
||
if ($oldImport->id > 0) {
|
||
$db->begin();
|
||
// Alten Import-Datensatz komplett loeschen (Transaktion)
|
||
$oldLines = new ImportLine($db);
|
||
$oldLines->deleteAllByImport($oldImport->id);
|
||
$old_dir = $conf->importzugferd->dir_output.'/imports/'.$oldImport->id;
|
||
if (is_dir($old_dir)) {
|
||
dol_delete_dir_recursive($old_dir);
|
||
}
|
||
$oldImport->delete($user);
|
||
$db->commit();
|
||
}
|
||
}
|
||
// 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);
|
||
}
|
||
if (!@rename($destfile, $final_dir.'/'.$filename)) {
|
||
// Fallback: copy + delete (z.B. bei verschiedenen Dateisystemen)
|
||
if (@copy($destfile, $final_dir.'/'.$filename)) {
|
||
@unlink($destfile);
|
||
} else {
|
||
dol_syslog('ImportZugferd: Fehler beim Verschieben der PDF nach '.$final_dir, LOG_ERR);
|
||
}
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
// Fehlende Lieferantenpreise aus anderen Katalogen hinzufuegen
|
||
if ($action == 'addmissingprices' && $id > 0) {
|
||
$import->fetch($id);
|
||
$selectedPrices = GETPOST('add_prices', 'array');
|
||
|
||
if (!empty($selectedPrices)) {
|
||
$addedCount = 0;
|
||
$errorCount = 0;
|
||
$processedKeys = array();
|
||
|
||
foreach ($selectedPrices as $entry) {
|
||
// Duplikate ueberspringen
|
||
if (isset($processedKeys[$entry])) {
|
||
continue;
|
||
}
|
||
$processedKeys[$entry] = true;
|
||
|
||
$parts = explode(',', $entry);
|
||
if (count($parts) !== 3) {
|
||
continue;
|
||
}
|
||
$productId = (int) $parts[0];
|
||
$socId = (int) $parts[1];
|
||
$datanormId = (int) $parts[2];
|
||
|
||
if ($productId <= 0 || $socId <= 0 || $datanormId <= 0) {
|
||
continue;
|
||
}
|
||
|
||
$datanorm = new Datanorm($db);
|
||
if ($datanorm->fetch($datanormId) > 0) {
|
||
$altSupplier = new Societe($db);
|
||
$altSupplier->fetch($socId);
|
||
|
||
$priceExtrafields = datanormBuildSupplierPriceExtrafields($datanorm);
|
||
$result = datanormAddSupplierPrice($db, $productId, $datanorm, $altSupplier, $user, 0, 19, $priceExtrafields);
|
||
|
||
if ($result > 0) {
|
||
datanormInsertPriceExtrafields($db, $result, $priceExtrafields);
|
||
|
||
$mapping = new ProductMapping($db);
|
||
$mapping->fk_soc = $socId;
|
||
$mapping->supplier_ref = $datanorm->article_number;
|
||
$mapping->fk_product = $productId;
|
||
$mapping->ean = $datanorm->ean;
|
||
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
|
||
$mapping->description = $datanorm->short_text1;
|
||
$mapping->create($user);
|
||
|
||
$addedCount++;
|
||
} else {
|
||
$errorCount++;
|
||
dol_syslog('ImportZugferd addmissingprices: Fehler bei Lieferantenpreis product='.$productId.' supplier='.$socId, LOG_ERR);
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($addedCount > 0) {
|
||
setEventMessages($langs->trans('SupplierPricesAdded', $addedCount), null, 'mesgs');
|
||
}
|
||
if ($errorCount > 0) {
|
||
setEventMessages($addedCount.' hinzugefuegt, '.$errorCount.' Fehler', null, 'warnings');
|
||
}
|
||
} else {
|
||
setEventMessages('Keine Preise ausgewählt', null, 'warnings');
|
||
}
|
||
$action = 'edit';
|
||
}
|
||
|
||
// Update supplier
|
||
if ($action == 'setsupplier' && $id > 0) {
|
||
$import->fetch($id);
|
||
$import->fk_soc = $supplier_id;
|
||
$import->update($user);
|
||
setEventMessages($langs->trans('SupplierUpdated'), null, 'mesgs');
|
||
$action = 'edit';
|
||
}
|
||
|
||
// Duplicate product from template
|
||
if ($action == 'duplicateproduct' && $template_product_id > 0 && $line_id > 0) {
|
||
$lineObj = new ImportLine($db);
|
||
$result = $lineObj->fetch($line_id);
|
||
|
||
if ($result > 0) {
|
||
// Load template product
|
||
$template = new Product($db);
|
||
if ($template->fetch($template_product_id) > 0) {
|
||
// Create new product as copy
|
||
$newproduct = new Product($db);
|
||
|
||
// Copy basic properties from template
|
||
$newproduct->type = $template->type;
|
||
$newproduct->status = $template->status;
|
||
$newproduct->status_buy = $template->status_buy;
|
||
$newproduct->status_batch = $template->status_batch;
|
||
$newproduct->fk_product_type = $template->fk_product_type;
|
||
$newproduct->price = $lineObj->unit_price;
|
||
$newproduct->price_base_type = 'HT';
|
||
$newproduct->tva_tx = $lineObj->tax_percent ?: $template->tva_tx;
|
||
$newproduct->weight = $template->weight;
|
||
$newproduct->weight_units = $template->weight_units;
|
||
$newproduct->fk_unit = $template->fk_unit;
|
||
|
||
// Set label from ZUGFeRD
|
||
$newproduct->label = $lineObj->product_name;
|
||
|
||
// Generate unique ref
|
||
$newproduct->ref = 'NEW-'.dol_print_date(dol_now(), '%Y%m%d%H%M%S');
|
||
|
||
// Build description with ZUGFeRD data
|
||
$zugferd_info = '';
|
||
if (!empty($lineObj->supplier_ref)) {
|
||
$zugferd_info .= $langs->trans('SupplierRef').': '.$lineObj->supplier_ref."\n";
|
||
}
|
||
if (!empty($lineObj->unit_code)) {
|
||
$zugferd_info .= $langs->trans('Unit').': '.zugferdGetUnitLabel($lineObj->unit_code)."\n";
|
||
}
|
||
if (!empty($lineObj->ean)) {
|
||
$zugferd_info .= 'EAN: '.$lineObj->ean."\n";
|
||
}
|
||
$zugferd_info .= "---\n";
|
||
$newproduct->description = $zugferd_info . ($template->description ?: '');
|
||
|
||
// Create the product
|
||
$result = $newproduct->create($user);
|
||
if ($result > 0) {
|
||
setEventMessages($langs->trans('ProductCreated'), null, 'mesgs');
|
||
// Redirect to product card for editing
|
||
header('Location: '.DOL_URL_ROOT.'/product/card.php?id='.$result);
|
||
exit;
|
||
} else {
|
||
setEventMessages($newproduct->error, $newproduct->errors, 'errors');
|
||
}
|
||
}
|
||
$id = $lineObj->fk_import;
|
||
}
|
||
$action = 'edit';
|
||
$import->fetch($id);
|
||
}
|
||
|
||
// Create product from Datanorm
|
||
if ($action == 'createfromdatanorm' && $line_id > 0) {
|
||
$lineObj = new ImportLine($db);
|
||
$result = $lineObj->fetch($line_id);
|
||
|
||
if ($result > 0) {
|
||
$id = $lineObj->fk_import;
|
||
$import->fetch($id);
|
||
|
||
// Get Datanorm settings
|
||
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
|
||
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
|
||
|
||
// Search in Datanorm database
|
||
$datanorm = new Datanorm($db);
|
||
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
|
||
|
||
if (empty($results)) {
|
||
// Try with EAN if available
|
||
if (!empty($lineObj->ean)) {
|
||
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
|
||
}
|
||
}
|
||
|
||
if (!empty($results)) {
|
||
$datanormArticle = $results[0];
|
||
$datanorm->fetch($datanormArticle['id']);
|
||
|
||
// Load supplier for ref prefix
|
||
$supplier = new Societe($db);
|
||
$supplier->fetch($import->fk_soc);
|
||
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
|
||
|
||
// Create new product
|
||
$newproduct = new Product($db);
|
||
$newproduct->type = 0; // Product
|
||
$newproduct->status = 1; // On sale
|
||
$newproduct->status_buy = 1; // On purchase
|
||
|
||
// Generate reference
|
||
$newproduct->ref = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
|
||
|
||
// Set default accounting codes from module settings
|
||
$newproduct->accountancy_code_sell = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL', '');
|
||
$newproduct->accountancy_code_sell_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA', '');
|
||
$newproduct->accountancy_code_sell_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT', '');
|
||
$newproduct->accountancy_code_buy = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY', '');
|
||
$newproduct->accountancy_code_buy_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA', '');
|
||
$newproduct->accountancy_code_buy_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT', '');
|
||
|
||
// Label from Datanorm
|
||
$newproduct->label = $datanorm->short_text1;
|
||
if (!empty($datanorm->short_text2) && strlen($newproduct->label) < 100) {
|
||
$newproduct->label .= ' '.$datanorm->short_text2;
|
||
}
|
||
|
||
// Description
|
||
$newproduct->description = $datanorm->getFullDescription();
|
||
|
||
// Preise und Kupferzuschlag
|
||
// Datanorm liefert den reinen Materialpreis (ohne Kupferzuschlag)
|
||
// WICHTIG: Bei Kabeln ist der Preis bereits für die Ringgröße (z.B. 49,20€ für 100m Ring)
|
||
$materialPrice = $datanorm->price;
|
||
$priceUnit = $datanorm->price_unit > 0 ? $datanorm->price_unit : 1;
|
||
|
||
// Prüfen ob es ein Kabel ist
|
||
$isCable = isCableProduct($datanorm);
|
||
|
||
// Preiseinheit bestimmen - unterschiedliche Logik je nach Lieferant-Datenformat:
|
||
// - Kluxen/Witte/eltric: price_unit > 1 (z.B. 100) → Preis ist für 100m
|
||
// - Sonepar: price_unit = 1, aber Preis ist für kompletten Ring → Größe aus Name extrahieren
|
||
$cableText = $datanorm->short_text1 . ' ' . $datanorm->short_text2;
|
||
if ($priceUnit == 1) {
|
||
// Sonepar-Format: Ringgröße aus Bezeichnung extrahieren
|
||
$ringSize = extractCableRingSize($cableText);
|
||
if ($ringSize > 0) {
|
||
$priceUnit = $ringSize; // z.B. 100 für Ri100
|
||
}
|
||
}
|
||
// Bei price_unit > 1 (Kluxen/Witte) bleibt priceUnit unverändert
|
||
$cableSpecs = null;
|
||
$kupfergehalt = 0;
|
||
$kupferzuschlag = 0;
|
||
$cuPrice = 0;
|
||
|
||
if ($isCable) {
|
||
// Parse Aderanzahl und Querschnitt aus Bezeichnung
|
||
$cableSpecs = parseCableSpecsFromText($datanorm->short_text1 . ' ' . $datanorm->short_text2);
|
||
|
||
if ($cableSpecs) {
|
||
// Kupfergehalt berechnen
|
||
$kupfergehalt = calculateKupfergehalt($cableSpecs['aderanzahl'], $cableSpecs['querschnitt']);
|
||
|
||
// Aktuellen Kupferpreis holen
|
||
$cuPrice = getCurrentCopperPrice($db, $import->fk_soc);
|
||
|
||
if ($cuPrice > 0 && $kupfergehalt > 0) {
|
||
// Kupferzuschlag für die Preiseinheit berechnen (z.B. 100m)
|
||
$kupferzuschlag = calculateKupferzuschlag($kupfergehalt, $cuPrice, $priceUnit);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Einkaufspreis = Materialpreis + Kupferzuschlag (für die Preiseinheit)
|
||
$totalPurchasePrice = $materialPrice + $kupferzuschlag;
|
||
|
||
// Stückpreis (pro 1 Einheit, z.B. pro Meter)
|
||
$purchasePricePerUnit = $totalPurchasePrice / $priceUnit;
|
||
|
||
// Verkaufspreis mit Aufschlag
|
||
$sellingPrice = $purchasePricePerUnit * (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) {
|
||
// Bei Kabeln: Produkt-Extrafields für Aderanzahl, Querschnitt und Kupfergehalt setzen
|
||
if ($isCable && $cableSpecs && $kupfergehalt > 0) {
|
||
$sqlProdExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_extrafields";
|
||
$sqlProdExtra .= " (fk_object, aderanzahl, querschnitt, kupfergehalt)";
|
||
$sqlProdExtra .= " VALUES (".(int)$newproduct->id.", ";
|
||
$sqlProdExtra .= (int)$cableSpecs['aderanzahl'].", ";
|
||
$sqlProdExtra .= (float)$cableSpecs['querschnitt'].", ";
|
||
$sqlProdExtra .= (float)$kupfergehalt.")";
|
||
$sqlProdExtra .= " ON DUPLICATE KEY UPDATE";
|
||
$sqlProdExtra .= " aderanzahl = ".(int)$cableSpecs['aderanzahl'].",";
|
||
$sqlProdExtra .= " querschnitt = ".(float)$cableSpecs['querschnitt'].",";
|
||
$sqlProdExtra .= " kupfergehalt = ".(float)$kupfergehalt;
|
||
if (!$db->query($sqlProdExtra)) {
|
||
dol_syslog("ImportZugferd: Fehler beim Setzen der Kabel-Extrafields: ".$db->lasterror(), LOG_WARNING);
|
||
} else {
|
||
dol_syslog("ImportZugferd: Kabel-Extrafields gesetzt - Adern: ".$cableSpecs['aderanzahl'].", Querschnitt: ".$cableSpecs['querschnitt'].", Kupfergehalt: ".$kupfergehalt." kg/km", LOG_INFO);
|
||
}
|
||
}
|
||
|
||
// Add supplier price
|
||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
|
||
$prodfourn = new ProductFournisseur($db);
|
||
$prodfourn->id = $newproduct->id;
|
||
$prodfourn->fourn_ref = $datanorm->article_number;
|
||
|
||
// Determine EAN for supplier price
|
||
$supplierEan = '';
|
||
$supplierEanType = 0;
|
||
if (!empty($datanorm->ean)) {
|
||
$supplierEan = $datanorm->ean;
|
||
$supplierEanType = 2; // EAN13
|
||
} elseif (!empty($lineObj->ean)) {
|
||
$supplierEan = $lineObj->ean;
|
||
$supplierEanType = 2; // EAN13
|
||
}
|
||
|
||
// Prepare extrafields for supplier price
|
||
$supplierPriceExtrafields = array();
|
||
// Produktpreis (reiner Materialpreis ohne Kupferzuschlag) - nur bei Kabeln
|
||
if ($isCable && $materialPrice > 0) {
|
||
$supplierPriceExtrafields['options_produktpreis'] = $materialPrice;
|
||
}
|
||
// Preiseinheit
|
||
if ($priceUnit > 1) {
|
||
$supplierPriceExtrafields['options_preiseinheit'] = $priceUnit;
|
||
} elseif (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
|
||
$supplierPriceExtrafields['options_preiseinheit'] = $lineObj->basis_quantity;
|
||
}
|
||
// Warengruppe aus Datanorm
|
||
if (!empty($datanorm->product_group)) {
|
||
$supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group;
|
||
}
|
||
|
||
// Lieferantenpreis speichern (Gesamtpreis inkl. Kupferzuschlag für die Preiseinheit)
|
||
$res = $prodfourn->update_buyprice(
|
||
$priceUnit, // Quantity (Mindestmenge, z.B. 100 für 100m)
|
||
$totalPurchasePrice, // Price (Gesamtpreis für die Mindestmenge inkl. Kupfer)
|
||
$user,
|
||
'HT', // Price base
|
||
$supplier, // Supplier
|
||
0, // Availability
|
||
$datanorm->article_number, // Supplier ref
|
||
$lineObj->tax_percent ?: 19, // VAT
|
||
0, // Charges
|
||
0, // Remise
|
||
0, // Remise percentage
|
||
0, // No price minimum
|
||
0, // Delivery delay
|
||
0, // Reputation
|
||
array(), // Localtaxes array
|
||
'', // Default VAT code
|
||
0, // Multicurrency price
|
||
'HT', // Multicurrency price base type
|
||
1, // Multicurrency tx
|
||
'', // Multicurrency code
|
||
trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : '')), // Description from Datanorm
|
||
$supplierEan, // Barcode/EAN in supplier price
|
||
$supplierEanType, // Barcode type (EAN13)
|
||
$supplierPriceExtrafields // Extra fields
|
||
);
|
||
|
||
dol_syslog("ImportZugferd: Lieferantenpreis - Material: ".$materialPrice.", Kupfer: ".$kupferzuschlag.", Gesamt: ".$totalPurchasePrice." (für ".$priceUnit." Einheiten)", LOG_INFO);
|
||
|
||
// Manually ensure extrafields record exists for supplier price
|
||
// (Dolibarr update_buyprice doesn't always create it properly)
|
||
$sqlGetPrice = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
||
$sqlGetPrice .= " WHERE fk_product = ".(int)$newproduct->id;
|
||
$sqlGetPrice .= " AND fk_soc = ".(int)$supplier->id;
|
||
$sqlGetPrice .= " ORDER BY rowid DESC LIMIT 1";
|
||
$resGetPrice = $db->query($sqlGetPrice);
|
||
if ($resGetPrice && $db->num_rows($resGetPrice) > 0) {
|
||
$objPrice = $db->fetch_object($resGetPrice);
|
||
$priceId = $objPrice->rowid;
|
||
|
||
// Check if extrafields record exists
|
||
$sqlCheckExtra = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
|
||
$sqlCheckExtra .= " WHERE fk_object = ".(int)$priceId;
|
||
$resCheckExtra = $db->query($sqlCheckExtra);
|
||
|
||
// Werte für Extrafields vorbereiten
|
||
$produktpreisVal = $isCable && $materialPrice > 0 ? (float)$materialPrice : 'NULL';
|
||
$kupferzuschlagVal = $kupferzuschlag > 0 ? (float)$kupferzuschlag : 'NULL';
|
||
$preiseinheitVal = $priceUnit > 1 ? (int)$priceUnit : 1;
|
||
$warengruppeVal = !empty($datanorm->product_group) ? "'".$db->escape($datanorm->product_group)."'" : 'NULL';
|
||
|
||
if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) {
|
||
// Insert extrafields record
|
||
$sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
|
||
$sqlInsertExtra .= " (fk_object, produktpreis, kupferzuschlag, preiseinheit, warengruppe) VALUES (";
|
||
$sqlInsertExtra .= (int)$priceId.", ";
|
||
$sqlInsertExtra .= ($produktpreisVal === 'NULL' ? "NULL" : $produktpreisVal).", ";
|
||
$sqlInsertExtra .= ($kupferzuschlagVal === 'NULL' ? "NULL" : $kupferzuschlagVal).", ";
|
||
$sqlInsertExtra .= $preiseinheitVal.", ";
|
||
$sqlInsertExtra .= $warengruppeVal.")";
|
||
if (!$db->query($sqlInsertExtra)) {
|
||
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
|
||
}
|
||
} else {
|
||
// Update extrafields record
|
||
$sqlUpdateExtra = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields SET ";
|
||
$sqlUpdateExtra .= "produktpreis = ".($produktpreisVal === 'NULL' ? "NULL" : $produktpreisVal).", ";
|
||
$sqlUpdateExtra .= "kupferzuschlag = ".($kupferzuschlagVal === 'NULL' ? "NULL" : $kupferzuschlagVal).", ";
|
||
$sqlUpdateExtra .= "preiseinheit = ".$preiseinheitVal.", ";
|
||
$sqlUpdateExtra .= "warengruppe = ".$warengruppeVal." ";
|
||
$sqlUpdateExtra .= "WHERE fk_object = ".(int)$priceId;
|
||
if (!$db->query($sqlUpdateExtra)) {
|
||
dol_syslog('ImportZugferd: Fehler beim Update der Extrafields: '.$db->lasterror(), LOG_ERR);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Create product mapping for future imports
|
||
$mapping = new ProductMapping($db);
|
||
$mapping->fk_soc = $import->fk_soc;
|
||
$mapping->supplier_ref = $datanorm->article_number;
|
||
$mapping->fk_product = $newproduct->id;
|
||
$mapping->ean = $datanorm->ean;
|
||
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
|
||
$mapping->description = $datanorm->short_text1;
|
||
$mapping->create($user);
|
||
|
||
// Assign to import line
|
||
$lineObj->setProduct($newproduct->id, 'datanorm', $user);
|
||
|
||
setEventMessages($langs->trans('ProductCreatedFromDatanorm', $newproduct->ref), null, 'mesgs');
|
||
|
||
// Check if all lines now have products
|
||
$allHaveProducts = $importLine->allLinesHaveProducts($id);
|
||
if ($allHaveProducts) {
|
||
$import->status = ZugferdImport::STATUS_IMPORTED;
|
||
$import->update($user);
|
||
}
|
||
} else {
|
||
setEventMessages($newproduct->error, $newproduct->errors, 'errors');
|
||
}
|
||
} else {
|
||
setEventMessages($langs->trans('DatanormArticleNotFound', $lineObj->supplier_ref), null, 'errors');
|
||
}
|
||
}
|
||
$action = 'edit';
|
||
$import->fetch($id);
|
||
}
|
||
|
||
// "Alle zuordnen" - Assign Datanorm matches to import lines
|
||
if ($action == 'assignallfromdatanorm' && $id > 0) {
|
||
$import->fetch($id);
|
||
|
||
if ($import->fk_soc > 0) {
|
||
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
|
||
|
||
// Get all lines without product
|
||
$lines = $importLine->fetchAllByImport($import->id);
|
||
$datanorm = new Datanorm($db);
|
||
$mapping = new ProductMapping($db);
|
||
$assignedCount = 0;
|
||
$datanormFoundCount = 0;
|
||
|
||
foreach ($lines as $lineObj) {
|
||
// Skip lines that already have a product
|
||
if ($lineObj->fk_product > 0) {
|
||
continue;
|
||
}
|
||
|
||
// Skip lines without supplier_ref
|
||
if (empty($lineObj->supplier_ref)) {
|
||
continue;
|
||
}
|
||
|
||
// Search in Datanorm database
|
||
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
|
||
|
||
if (empty($results) && !empty($lineObj->ean)) {
|
||
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
|
||
}
|
||
|
||
if (!empty($results)) {
|
||
$datanormFoundCount++;
|
||
$datanormMatch = $results[0];
|
||
// Get Datanorm ID and article number (array access)
|
||
$datanormId = isset($datanormMatch['id']) ? $datanormMatch['id'] : (isset($datanormMatch['rowid']) ? $datanormMatch['rowid'] : 0);
|
||
$articleNumber = isset($datanormMatch['article_number']) ? $datanormMatch['article_number'] : '';
|
||
|
||
// Check if product already exists for this supplier ref
|
||
$existingProductId = $mapping->findProductBySupplierRef($import->fk_soc, $articleNumber);
|
||
|
||
if ($existingProductId > 0) {
|
||
// Product exists - assign both product and Datanorm to the line
|
||
$lineObj->fk_product = $existingProductId;
|
||
$lineObj->fk_datanorm = $datanormId;
|
||
$lineObj->match_method = 'datanorm_assign';
|
||
$lineObj->update($user);
|
||
$assignedCount++;
|
||
} else {
|
||
// No product yet - save Datanorm reference for later product creation
|
||
$lineObj->fk_datanorm = $datanormId;
|
||
$lineObj->match_method = 'datanorm_pending';
|
||
$lineObj->update($user);
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($assignedCount > 0) {
|
||
setEventMessages($langs->trans('ProductsAssignedFromDatanorm', $assignedCount), null, 'mesgs');
|
||
}
|
||
if ($datanormFoundCount > $assignedCount) {
|
||
$pendingCount = $datanormFoundCount - $assignedCount;
|
||
setEventMessages($langs->trans('DatanormMatchesFoundNotAssigned', $pendingCount), null, 'mesgs');
|
||
}
|
||
if ($datanormFoundCount == 0) {
|
||
setEventMessages($langs->trans('DatanormBatchNoMatches'), null, 'warnings');
|
||
}
|
||
}
|
||
|
||
header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&id='.$id.'&token='.newToken());
|
||
exit;
|
||
}
|
||
|
||
// Create ALL products from Datanorm (batch)
|
||
if ($action == 'createallfromdatanorm' && $id > 0) {
|
||
$import->fetch($id);
|
||
|
||
if ($import->fk_soc > 0) {
|
||
// Get Datanorm settings
|
||
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
|
||
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
|
||
|
||
// Load supplier
|
||
$supplier = new Societe($db);
|
||
$supplier->fetch($import->fk_soc);
|
||
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
|
||
|
||
// Get all lines without product
|
||
$lines = $importLine->fetchAllByImport($import->id);
|
||
$datanorm = new Datanorm($db);
|
||
$createdCount = 0;
|
||
$assignedCount = 0;
|
||
$errorCount = 0;
|
||
|
||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
|
||
|
||
foreach ($lines as $lineObj) {
|
||
// Skip lines that already have a product
|
||
if ($lineObj->fk_product > 0) {
|
||
continue;
|
||
}
|
||
|
||
// Skip lines without supplier_ref
|
||
if (empty($lineObj->supplier_ref)) {
|
||
continue;
|
||
}
|
||
|
||
// Search in Datanorm database
|
||
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1);
|
||
|
||
if (empty($results) && !empty($lineObj->ean)) {
|
||
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1);
|
||
}
|
||
|
||
if (!empty($results)) {
|
||
$datanormArticle = $results[0];
|
||
$datanorm->fetch($datanormArticle['id']);
|
||
|
||
$purchasePrice = $datanorm->price;
|
||
if ($datanorm->price_unit > 1) {
|
||
$purchasePrice = $datanorm->price / $datanorm->price_unit;
|
||
}
|
||
|
||
// Get copper surcharge for selling price calculation
|
||
// Priority: 1. Datanorm, 2. ZUGFeRD
|
||
$copperSurchargeForPrice = 0;
|
||
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
|
||
$copperSurchargeForPrice = $datanorm->metal_surcharge;
|
||
// Normalize to per-unit if price_unit > 1
|
||
if ($datanorm->price_unit > 1) {
|
||
$copperSurchargeForPrice = $datanorm->metal_surcharge / $datanorm->price_unit;
|
||
}
|
||
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
|
||
$copperSurchargeForPrice = $lineObj->copper_surcharge;
|
||
// Normalize based on copper_surcharge_basis_qty
|
||
if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) {
|
||
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty;
|
||
}
|
||
}
|
||
|
||
// Check if product already exists in Dolibarr
|
||
$existingProduct = new Product($db);
|
||
$productExists = false;
|
||
$existingProductId = 0;
|
||
|
||
// 1. Check by supplier reference (ProductFournisseur)
|
||
$sqlCheck = "SELECT DISTINCT pf.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pf";
|
||
$sqlCheck .= " WHERE pf.fk_soc = ".(int)$import->fk_soc;
|
||
$sqlCheck .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'";
|
||
$sqlCheck .= " AND pf.entity IN (".getEntity('product').")";
|
||
$resqlCheck = $db->query($sqlCheck);
|
||
if ($resqlCheck && $db->num_rows($resqlCheck) > 0) {
|
||
$objCheck = $db->fetch_object($resqlCheck);
|
||
$existingProductId = $objCheck->fk_product;
|
||
$productExists = true;
|
||
}
|
||
|
||
// 2. Check by product reference pattern
|
||
if (!$productExists) {
|
||
$expectedRef = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
|
||
$fetchResult = $existingProduct->fetch(0, $expectedRef);
|
||
if ($fetchResult > 0) {
|
||
$existingProductId = $existingProduct->id;
|
||
$productExists = true;
|
||
}
|
||
}
|
||
|
||
// 3. Check by EAN if available
|
||
if (!$productExists && !empty($datanorm->ean)) {
|
||
$sqlEan = "SELECT rowid FROM ".MAIN_DB_PREFIX."product";
|
||
$sqlEan .= " WHERE barcode = '".$db->escape($datanorm->ean)."'";
|
||
$sqlEan .= " AND entity IN (".getEntity('product').")";
|
||
$resqlEan = $db->query($sqlEan);
|
||
if ($resqlEan && $db->num_rows($resqlEan) > 0) {
|
||
$objEan = $db->fetch_object($resqlEan);
|
||
$existingProductId = $objEan->rowid;
|
||
$productExists = true;
|
||
}
|
||
}
|
||
|
||
if ($productExists && $existingProductId > 0) {
|
||
// Product exists - just assign it to the line
|
||
$lineObj->setProduct($existingProductId, 'datanorm', $user);
|
||
|
||
// Add additional supplier prices from selected alternatives (for existing products too)
|
||
$supplierPricesPost = GETPOST('supplier_prices', 'array');
|
||
if (!empty($supplierPricesPost[$lineObj->id])) {
|
||
foreach ($supplierPricesPost[$lineObj->id] as $altSocId => $altDatanormId) {
|
||
// Check if supplier price already exists for this product/supplier
|
||
$sqlCheckSupplier = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
||
$sqlCheckSupplier .= " WHERE fk_product = ".(int)$existingProductId;
|
||
$sqlCheckSupplier .= " AND fk_soc = ".(int)$altSocId;
|
||
$resCheckSupplier = $db->query($sqlCheckSupplier);
|
||
if ($resCheckSupplier && $db->num_rows($resCheckSupplier) > 0) {
|
||
continue; // Skip if supplier price already exists
|
||
}
|
||
|
||
// Fetch the alternative Datanorm article
|
||
$altDatanorm = new Datanorm($db);
|
||
if ($altDatanorm->fetch($altDatanormId) > 0) {
|
||
$altSupplier = new Societe($db);
|
||
$altSupplier->fetch($altSocId);
|
||
|
||
// Prepare extrafields
|
||
$altExtrafields = array();
|
||
if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0 && !empty($altDatanorm->price)) {
|
||
$altExtrafields['options_produktpreis'] = $altDatanorm->price;
|
||
}
|
||
if (!empty($altDatanorm->product_group)) {
|
||
$altExtrafields['options_warengruppe'] = $altDatanorm->product_group;
|
||
}
|
||
|
||
// Mindestbestellmenge, Verpackungseinheit, Steuersatz und kaufmenge vom bestehenden Preis übernehmen
|
||
$altMinQty = 1;
|
||
$altPackaging = null;
|
||
$altTvaTx = $lineObj->tax_percent ?: 19;
|
||
$altKaufmenge = null;
|
||
$sqlAltExisting = "SELECT pf.rowid, pf.quantity, pf.packaging, pf.tva_tx FROM " . MAIN_DB_PREFIX . "product_fournisseur_price pf";
|
||
$sqlAltExisting .= " WHERE pf.fk_product = " . (int)$existingProductId;
|
||
$sqlAltExisting .= " AND pf.quantity > 0 ORDER BY pf.rowid ASC LIMIT 1";
|
||
$resAltExisting = $db->query($sqlAltExisting);
|
||
if ($resAltExisting && $db->num_rows($resAltExisting) > 0) {
|
||
$objAltExisting = $db->fetch_object($resAltExisting);
|
||
if ($objAltExisting->quantity > 0) {
|
||
$altMinQty = $objAltExisting->quantity;
|
||
}
|
||
if (!empty($objAltExisting->packaging)) {
|
||
$altPackaging = $objAltExisting->packaging;
|
||
}
|
||
if ($objAltExisting->tva_tx > 0) {
|
||
$altTvaTx = $objAltExisting->tva_tx;
|
||
}
|
||
// kaufmenge aus Extrafields übernehmen (nur wenn numerisch und > 0)
|
||
$sqlKaufmenge = "SELECT kaufmenge FROM " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
|
||
$sqlKaufmenge .= " WHERE fk_object = " . (int)$objAltExisting->rowid;
|
||
$resKaufmenge = $db->query($sqlKaufmenge);
|
||
if ($resKaufmenge && $db->num_rows($resKaufmenge) > 0) {
|
||
$objKaufmenge = $db->fetch_object($resKaufmenge);
|
||
$kmValue = trim($objKaufmenge->kaufmenge);
|
||
if ($kmValue !== '' && is_numeric($kmValue) && (int)$kmValue > 0) {
|
||
$altKaufmenge = (int)$kmValue;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Preis berechnen - Datanorm-Preis ist für die price_unit Menge!
|
||
$altCableText = $altDatanorm->short_text1 . ' ' . $altDatanorm->short_text2;
|
||
$altRingSize = extractCableRingSize($altCableText);
|
||
$altPriceUnit = 1;
|
||
|
||
if ($altRingSize > 0) {
|
||
// Kabel mit Ringgröße
|
||
$altPriceUnit = $altRingSize;
|
||
$altExtrafields['options_preiseinheit'] = $altRingSize;
|
||
} elseif (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) {
|
||
// Nicht-Kabel mit price_unit > 1
|
||
$altPriceUnit = $altDatanorm->price_unit;
|
||
$altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit;
|
||
}
|
||
|
||
// WICHTIG: Datanorm price ist bereits für die altPriceUnit Menge!
|
||
// Nur umrechnen wenn altMinQty != altPriceUnit
|
||
if ($altMinQty == $altPriceUnit) {
|
||
// Gleiche Menge: Preis direkt übernehmen
|
||
$altTotalPrice = $altDatanorm->price;
|
||
} else {
|
||
// Unterschiedliche Menge: Umrechnen über Stückpreis
|
||
$altUnitPrice = $altDatanorm->price / $altPriceUnit;
|
||
$altTotalPrice = $altUnitPrice * $altMinQty;
|
||
}
|
||
|
||
// Add supplier price (use tva_tx from existing price)
|
||
$altProdfourn = new ProductFournisseur($db);
|
||
$altProdfourn->id = $existingProductId;
|
||
$altResult = $altProdfourn->update_buyprice(
|
||
$altMinQty, $altTotalPrice, $user, 'HT', $altSupplier, 0,
|
||
$altDatanorm->article_number, $altTvaTx,
|
||
0, 0, 0, 0, 0, 0, array(), '',
|
||
0, 'HT', 1, '',
|
||
trim($altDatanorm->short_text1 . ($altDatanorm->short_text2 ? ' ' . $altDatanorm->short_text2 : '')),
|
||
!empty($altDatanorm->ean) ? $altDatanorm->ean : '',
|
||
!empty($altDatanorm->ean) ? 2 : 0,
|
||
$altExtrafields
|
||
);
|
||
|
||
// Verpackungseinheit und kaufmenge nachträglich setzen
|
||
if ($altResult > 0) {
|
||
// Packaging = Mindestmenge (als Standard)
|
||
$sqlAltPkg = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||
$sqlAltPkg .= " SET packaging = " . (float)($altPackaging ?: $altMinQty);
|
||
$sqlAltPkg .= " WHERE rowid = " . (int)$altResult;
|
||
$db->query($sqlAltPkg);
|
||
|
||
// kaufmenge in Extrafields übernehmen wenn vorhanden
|
||
if (!empty($altKaufmenge)) {
|
||
$sqlCheckExtra = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields WHERE fk_object = " . (int)$altResult;
|
||
$resCheckExtra = $db->query($sqlCheckExtra);
|
||
if ($resCheckExtra && $db->num_rows($resCheckExtra) > 0) {
|
||
$sqlUpdateKm = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
|
||
$sqlUpdateKm .= " SET kaufmenge = " . (int)$altKaufmenge;
|
||
$sqlUpdateKm .= " WHERE fk_object = " . (int)$altResult;
|
||
$db->query($sqlUpdateKm);
|
||
} else {
|
||
$sqlInsertKm = "INSERT INTO " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
|
||
$sqlInsertKm .= " (fk_object, kaufmenge) VALUES (" . (int)$altResult . ", " . (int)$altKaufmenge . ")";
|
||
$db->query($sqlInsertKm);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Create product mapping
|
||
$altMapping = new ProductMapping($db);
|
||
$altMapping->fk_soc = $altSocId;
|
||
$altMapping->supplier_ref = $altDatanorm->article_number;
|
||
$altMapping->fk_product = $existingProductId;
|
||
$altMapping->ean = $altDatanorm->ean;
|
||
$altMapping->manufacturer_ref = $altDatanorm->manufacturer_ref;
|
||
$altMapping->description = $altDatanorm->short_text1;
|
||
$altMapping->create($user);
|
||
}
|
||
}
|
||
}
|
||
|
||
$assignedCount++;
|
||
} else {
|
||
// Create new product
|
||
$newproduct = new Product($db);
|
||
$newproduct->type = 0;
|
||
$newproduct->status = 1;
|
||
$newproduct->status_buy = 1;
|
||
$newproduct->ref = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
|
||
$newproduct->label = $datanorm->short_text1;
|
||
if (!empty($datanorm->short_text2) && strlen($newproduct->label) < 100) {
|
||
$newproduct->label .= ' '.$datanorm->short_text2;
|
||
}
|
||
$newproduct->description = $datanorm->getFullDescription();
|
||
|
||
// Set default accounting codes from module settings
|
||
$newproduct->accountancy_code_sell = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL', '');
|
||
$newproduct->accountancy_code_sell_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA', '');
|
||
$newproduct->accountancy_code_sell_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT', '');
|
||
$newproduct->accountancy_code_buy = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY', '');
|
||
$newproduct->accountancy_code_buy_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA', '');
|
||
$newproduct->accountancy_code_buy_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT', '');
|
||
|
||
// Selling price: (purchase price + copper surcharge) × (1 + markup%)
|
||
$sellingPrice = ($purchasePrice + $copperSurchargeForPrice) * (1 + $markup / 100);
|
||
$newproduct->price = $sellingPrice;
|
||
$newproduct->price_base_type = 'HT';
|
||
$newproduct->tva_tx = $lineObj->tax_percent ?: 19;
|
||
|
||
if (!empty($datanorm->weight)) {
|
||
$newproduct->weight = $datanorm->weight;
|
||
$newproduct->weight_units = 0;
|
||
}
|
||
|
||
if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
|
||
$newproduct->barcode = '-1';
|
||
}
|
||
|
||
$result = $newproduct->create($user);
|
||
|
||
if ($result > 0) {
|
||
// Add supplier price
|
||
$prodfourn = new ProductFournisseur($db);
|
||
$prodfourn->id = $newproduct->id;
|
||
$prodfourn->fourn_ref = $datanorm->article_number;
|
||
|
||
$supplierEan = '';
|
||
$supplierEanType = 0;
|
||
if (!empty($datanorm->ean)) {
|
||
$supplierEan = $datanorm->ean;
|
||
$supplierEanType = 2;
|
||
} elseif (!empty($lineObj->ean)) {
|
||
$supplierEan = $lineObj->ean;
|
||
$supplierEanType = 2;
|
||
}
|
||
|
||
// Prepare extrafields for supplier price
|
||
$supplierPriceExtrafields = array();
|
||
// Produktpreis (reiner Materialpreis) - nur bei Kabeln mit Metallzuschlag
|
||
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0 && !empty($datanorm->price)) {
|
||
$supplierPriceExtrafields['options_produktpreis'] = $datanorm->price;
|
||
}
|
||
// Preiseinheit - Priorität: 1. ZUGFeRD basis_quantity, 2. Datanorm price_unit
|
||
if (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
|
||
$supplierPriceExtrafields['options_preiseinheit'] = $lineObj->basis_quantity;
|
||
} elseif (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
|
||
$supplierPriceExtrafields['options_preiseinheit'] = $datanorm->price_unit;
|
||
}
|
||
// Warengruppe aus Datanorm
|
||
if (!empty($datanorm->product_group)) {
|
||
$supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group;
|
||
}
|
||
|
||
// Mindestbestellmenge und Preis mit zentraler Funktion berechnen
|
||
$pricing = calculateCablePricing($datanorm, 1);
|
||
$newMinQty = $pricing['priceUnit'];
|
||
$newPackaging = $pricing['priceUnit'];
|
||
$newTotalPrice = $datanorm->price; // Originalpreis aus Datanorm
|
||
|
||
// Fallback auf ZUGFeRD basis_quantity wenn keine Ringgröße erkannt
|
||
if ($newMinQty == 1 && !empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
|
||
$newMinQty = $lineObj->basis_quantity;
|
||
$newPackaging = $lineObj->basis_quantity;
|
||
$newTotalPrice = $purchasePrice * $newMinQty;
|
||
}
|
||
|
||
$newPriceResult = $prodfourn->update_buyprice(
|
||
$newMinQty, // Quantity (Mindestbestellmenge)
|
||
$newTotalPrice, // Price (Gesamtpreis für die Mindestmenge)
|
||
$user,
|
||
'HT', // Price base
|
||
$supplier, // Supplier
|
||
0, // Availability
|
||
$datanorm->article_number, // Supplier ref
|
||
$lineObj->tax_percent ?: 19, // VAT
|
||
0, // Charges
|
||
0, // Remise
|
||
0, // Remise percentage
|
||
0, // No price minimum
|
||
0, // Delivery delay
|
||
0, // Reputation
|
||
array(), // Localtaxes array
|
||
'', // Default VAT code
|
||
0, // Multicurrency price
|
||
'HT', // Multicurrency price base type
|
||
1, // Multicurrency tx
|
||
'', // Multicurrency code
|
||
trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : '')), // Description from Datanorm
|
||
$supplierEan, // Barcode/EAN
|
||
$supplierEanType, // Barcode type
|
||
$supplierPriceExtrafields // Extra fields
|
||
);
|
||
|
||
// Manually ensure extrafields record exists for supplier price
|
||
// (Dolibarr update_buyprice doesn't always create it properly)
|
||
$sqlGetPrice = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
||
$sqlGetPrice .= " WHERE fk_product = ".(int)$newproduct->id;
|
||
$sqlGetPrice .= " AND fk_soc = ".(int)$supplier->id;
|
||
$sqlGetPrice .= " ORDER BY rowid DESC LIMIT 1";
|
||
$resGetPrice = $db->query($sqlGetPrice);
|
||
if ($resGetPrice && $db->num_rows($resGetPrice) > 0) {
|
||
$objPrice = $db->fetch_object($resGetPrice);
|
||
$priceId = $objPrice->rowid;
|
||
|
||
// Check if extrafields record exists
|
||
$sqlCheckExtra = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
|
||
$sqlCheckExtra .= " WHERE fk_object = ".(int)$priceId;
|
||
$resCheckExtra = $db->query($sqlCheckExtra);
|
||
|
||
$produktpreis = !empty($supplierPriceExtrafields['options_produktpreis']) ? (float)$supplierPriceExtrafields['options_produktpreis'] : 'NULL';
|
||
$preiseinheit = !empty($supplierPriceExtrafields['options_preiseinheit']) ? (int)$supplierPriceExtrafields['options_preiseinheit'] : 1;
|
||
$warengruppe = !empty($supplierPriceExtrafields['options_warengruppe']) ? "'".$db->escape($supplierPriceExtrafields['options_warengruppe'])."'" : 'NULL';
|
||
|
||
if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) {
|
||
// Insert extrafields record
|
||
$sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
|
||
$sqlInsertExtra .= " (fk_object, produktpreis, preiseinheit, warengruppe) VALUES (";
|
||
$sqlInsertExtra .= (int)$priceId.", ";
|
||
$sqlInsertExtra .= ($produktpreis === 'NULL' ? "NULL" : $produktpreis).", ";
|
||
$sqlInsertExtra .= $preiseinheit.", ";
|
||
$sqlInsertExtra .= $warengruppe.")";
|
||
if (!$db->query($sqlInsertExtra)) {
|
||
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
|
||
}
|
||
} else {
|
||
// Update extrafields record
|
||
$sqlUpdateExtra = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields SET ";
|
||
$sqlUpdateExtra .= "produktpreis = ".($produktpreis === 'NULL' ? "NULL" : $produktpreis).", ";
|
||
$sqlUpdateExtra .= "preiseinheit = ".$preiseinheit.", ";
|
||
$sqlUpdateExtra .= "warengruppe = ".$warengruppe." ";
|
||
$sqlUpdateExtra .= "WHERE fk_object = ".(int)$priceId;
|
||
if (!$db->query($sqlUpdateExtra)) {
|
||
dol_syslog('ImportZugferd: Fehler beim Update der Extrafields: '.$db->lasterror(), LOG_ERR);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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);
|
||
|
||
// Add additional supplier prices from selected alternatives
|
||
$supplierPricesPost = GETPOST('supplier_prices', 'array');
|
||
if (!empty($supplierPricesPost[$lineObj->id])) {
|
||
// Hole Werte vom gerade angelegten Hauptpreis (Mindestmenge, Verpackung, Steuersatz)
|
||
$mainMinQty = $newMinQty;
|
||
$mainPackaging = $newPackaging;
|
||
$mainTvaTx = $lineObj->tax_percent ?: 19;
|
||
$mainKaufmenge = null;
|
||
|
||
// kaufmenge vom Hauptpreis-Extrafield holen (nur wenn numerisch und > 0)
|
||
if (!empty($priceId)) {
|
||
$sqlMainKm = "SELECT kaufmenge FROM " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
|
||
$sqlMainKm .= " WHERE fk_object = " . (int)$priceId;
|
||
$resMainKm = $db->query($sqlMainKm);
|
||
if ($resMainKm && $db->num_rows($resMainKm) > 0) {
|
||
$objMainKm = $db->fetch_object($resMainKm);
|
||
$kmValue = trim($objMainKm->kaufmenge);
|
||
if ($kmValue !== '' && is_numeric($kmValue) && (int)$kmValue > 0) {
|
||
$mainKaufmenge = (int)$kmValue;
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($supplierPricesPost[$lineObj->id] as $altSocId => $altDatanormId) {
|
||
// Skip the main invoice supplier (already added above)
|
||
if ($altSocId == $import->fk_soc) {
|
||
continue;
|
||
}
|
||
|
||
// Fetch the alternative Datanorm article
|
||
$altDatanorm = new Datanorm($db);
|
||
if ($altDatanorm->fetch($altDatanormId) > 0) {
|
||
$altSupplier = new Societe($db);
|
||
$altSupplier->fetch($altSocId);
|
||
|
||
// Prepare extrafields for alternative supplier price
|
||
$altExtrafields = array();
|
||
if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0 && !empty($altDatanorm->price)) {
|
||
$altExtrafields['options_produktpreis'] = $altDatanorm->price;
|
||
}
|
||
if (!empty($altDatanorm->product_group)) {
|
||
$altExtrafields['options_warengruppe'] = $altDatanorm->product_group;
|
||
}
|
||
|
||
// Preis berechnen - Datanorm-Preis ist für die price_unit Menge!
|
||
$altCableText = $altDatanorm->short_text1 . ' ' . $altDatanorm->short_text2;
|
||
$altRingSize = extractCableRingSize($altCableText);
|
||
$altPriceUnit = 1;
|
||
|
||
if ($altRingSize > 0) {
|
||
// Kabel mit Ringgröße
|
||
$altPriceUnit = $altRingSize;
|
||
$altExtrafields['options_preiseinheit'] = $altRingSize;
|
||
} elseif (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) {
|
||
// Nicht-Kabel mit price_unit > 1
|
||
$altPriceUnit = $altDatanorm->price_unit;
|
||
$altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit;
|
||
}
|
||
|
||
// WICHTIG: Datanorm price ist bereits für die altPriceUnit Menge!
|
||
// Umrechnen auf mainMinQty (vom Hauptpreis übernommen)
|
||
if ($mainMinQty == $altPriceUnit) {
|
||
// Gleiche Menge: Preis direkt übernehmen
|
||
$altTotalPrice = $altDatanorm->price;
|
||
} else {
|
||
// Unterschiedliche Menge: Umrechnen über Stückpreis
|
||
$altUnitPrice = $altDatanorm->price / $altPriceUnit;
|
||
$altTotalPrice = $altUnitPrice * $mainMinQty;
|
||
}
|
||
|
||
// Add supplier price for alternative supplier
|
||
$altProdfourn = new ProductFournisseur($db);
|
||
$altProdfourn->id = $newproduct->id;
|
||
$altResult = $altProdfourn->update_buyprice(
|
||
$mainMinQty, // Quantity (vom Hauptpreis übernommen)
|
||
$altTotalPrice, // Price (für mainMinQty berechnet)
|
||
$user,
|
||
'HT', // Price base
|
||
$altSupplier, // Alternative supplier
|
||
0, // Availability
|
||
$altDatanorm->article_number, // Supplier ref
|
||
$mainTvaTx, // VAT (vom Hauptpreis übernommen)
|
||
0, 0, 0, 0, 0, 0, array(), '',
|
||
0, 'HT', 1, '',
|
||
trim($altDatanorm->short_text1 . ($altDatanorm->short_text2 ? ' ' . $altDatanorm->short_text2 : '')),
|
||
!empty($altDatanorm->ean) ? $altDatanorm->ean : '',
|
||
!empty($altDatanorm->ean) ? 2 : 0,
|
||
$altExtrafields
|
||
);
|
||
|
||
// Verpackungseinheit und kaufmenge nachträglich setzen (vom Hauptpreis übernommen)
|
||
if ($altResult > 0) {
|
||
$sqlAltPkg = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||
$sqlAltPkg .= " SET packaging = " . (float)$mainPackaging;
|
||
$sqlAltPkg .= " WHERE rowid = " . (int)$altResult;
|
||
$db->query($sqlAltPkg);
|
||
|
||
// kaufmenge in Extrafields übernehmen wenn vorhanden
|
||
if (!empty($mainKaufmenge)) {
|
||
$sqlCheckExtra = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields WHERE fk_object = " . (int)$altResult;
|
||
$resCheckExtra = $db->query($sqlCheckExtra);
|
||
if ($resCheckExtra && $db->num_rows($resCheckExtra) > 0) {
|
||
$sqlUpdateKm = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
|
||
$sqlUpdateKm .= " SET kaufmenge = " . (int)$mainKaufmenge;
|
||
$sqlUpdateKm .= " WHERE fk_object = " . (int)$altResult;
|
||
$db->query($sqlUpdateKm);
|
||
} else {
|
||
$sqlInsertKm = "INSERT INTO " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
|
||
$sqlInsertKm .= " (fk_object, kaufmenge) VALUES (" . (int)$altResult . ", " . (int)$mainKaufmenge . ")";
|
||
$db->query($sqlInsertKm);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Create product mapping for alternative supplier
|
||
$altMapping = new ProductMapping($db);
|
||
$altMapping->fk_soc = $altSocId;
|
||
$altMapping->supplier_ref = $altDatanorm->article_number;
|
||
$altMapping->fk_product = $newproduct->id;
|
||
$altMapping->ean = $altDatanorm->ean;
|
||
$altMapping->manufacturer_ref = $altDatanorm->manufacturer_ref;
|
||
$altMapping->description = $altDatanorm->short_text1;
|
||
$altMapping->create($user);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Assign to import line
|
||
$lineObj->setProduct($newproduct->id, 'datanorm', $user);
|
||
$createdCount++;
|
||
} else {
|
||
$errorCount++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($createdCount > 0) {
|
||
setEventMessages($langs->trans('DatanormBatchCreated', $createdCount), null, 'mesgs');
|
||
}
|
||
if ($assignedCount > 0) {
|
||
setEventMessages($langs->trans('DatanormBatchAssigned', $assignedCount), null, 'mesgs');
|
||
}
|
||
if ($errorCount > 0) {
|
||
setEventMessages($langs->trans('DatanormBatchErrors', $errorCount), null, 'warnings');
|
||
}
|
||
if ($createdCount == 0 && $assignedCount == 0 && $errorCount == 0) {
|
||
setEventMessages($langs->trans('DatanormBatchNoMatches'), null, 'warnings');
|
||
}
|
||
|
||
// Check if all lines now have products
|
||
$allHaveProducts = $importLine->allLinesHaveProducts($id);
|
||
if ($allHaveProducts) {
|
||
$import->status = ZugferdImport::STATUS_IMPORTED;
|
||
$import->update($user);
|
||
}
|
||
}
|
||
$action = 'edit';
|
||
$import->fetch($id);
|
||
}
|
||
|
||
// Preview Datanorm matches (step 1 - show what will be created)
|
||
$datanormPreviewMatches = array();
|
||
if ($action == 'previewdatanorm' && $id > 0) {
|
||
$import->fetch($id);
|
||
|
||
if ($import->fk_soc > 0) {
|
||
// Get Datanorm settings
|
||
$markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30);
|
||
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
|
||
|
||
// Load supplier
|
||
$supplier = new Societe($db);
|
||
$supplier->fetch($import->fk_soc);
|
||
$supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3));
|
||
|
||
// Get all lines without product
|
||
$lines = $importLine->fetchAllByImport($import->id);
|
||
$datanorm = new Datanorm($db);
|
||
|
||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
|
||
|
||
foreach ($lines as $lineObj) {
|
||
// Skip lines that already have a product
|
||
if ($lineObj->fk_product > 0) {
|
||
continue;
|
||
}
|
||
|
||
// Skip lines without supplier_ref
|
||
if (empty($lineObj->supplier_ref)) {
|
||
continue;
|
||
}
|
||
|
||
// Search in Datanorm database - get ALL supplier alternatives
|
||
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 10);
|
||
|
||
if (empty($results) && !empty($lineObj->ean)) {
|
||
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 10);
|
||
}
|
||
|
||
if (!empty($results)) {
|
||
// Process the primary result (first = current supplier or cheapest)
|
||
$datanormArticle = $results[0];
|
||
$datanorm->fetch($datanormArticle['id']);
|
||
|
||
$purchasePrice = $datanorm->price;
|
||
if ($datanorm->price_unit > 1) {
|
||
$purchasePrice = $datanorm->price / $datanorm->price_unit;
|
||
}
|
||
|
||
// Get copper surcharge for selling price calculation
|
||
$copperSurchargeForPrice = 0;
|
||
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
|
||
$copperSurchargeForPrice = $datanorm->metal_surcharge;
|
||
if ($datanorm->price_unit > 1) {
|
||
$copperSurchargeForPrice = $datanorm->metal_surcharge / $datanorm->price_unit;
|
||
}
|
||
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
|
||
$copperSurchargeForPrice = $lineObj->copper_surcharge;
|
||
if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) {
|
||
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty;
|
||
}
|
||
}
|
||
|
||
// Calculate selling price
|
||
$sellingPrice = ($purchasePrice + $copperSurchargeForPrice) * (1 + $markup / 100);
|
||
|
||
// Check if product already exists in Dolibarr
|
||
$existingProductId = 0;
|
||
$productAction = 'create'; // 'create' or 'assign'
|
||
|
||
// 1. Check by supplier reference (ProductFournisseur)
|
||
$sqlCheck = "SELECT DISTINCT pf.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pf";
|
||
$sqlCheck .= " WHERE pf.fk_soc = ".(int)$import->fk_soc;
|
||
$sqlCheck .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'";
|
||
$sqlCheck .= " AND pf.entity IN (".getEntity('product').")";
|
||
$resqlCheck = $db->query($sqlCheck);
|
||
if ($resqlCheck && $db->num_rows($resqlCheck) > 0) {
|
||
$objCheck = $db->fetch_object($resqlCheck);
|
||
$existingProductId = $objCheck->fk_product;
|
||
$productAction = 'assign';
|
||
}
|
||
|
||
// 2. Check by product reference pattern
|
||
if ($existingProductId <= 0) {
|
||
$expectedRef = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number;
|
||
$existingProduct = new Product($db);
|
||
$fetchResult = $existingProduct->fetch(0, $expectedRef);
|
||
if ($fetchResult > 0) {
|
||
$existingProductId = $existingProduct->id;
|
||
$productAction = 'assign';
|
||
}
|
||
}
|
||
|
||
// 3. Check by EAN if available
|
||
if ($existingProductId <= 0 && !empty($datanorm->ean)) {
|
||
$sqlEan = "SELECT rowid FROM ".MAIN_DB_PREFIX."product";
|
||
$sqlEan .= " WHERE barcode = '".$db->escape($datanorm->ean)."'";
|
||
$sqlEan .= " AND entity IN (".getEntity('product').")";
|
||
$resqlEan = $db->query($sqlEan);
|
||
if ($resqlEan && $db->num_rows($resqlEan) > 0) {
|
||
$objEan = $db->fetch_object($resqlEan);
|
||
$existingProductId = $objEan->rowid;
|
||
$productAction = 'assign';
|
||
}
|
||
}
|
||
|
||
// Build supplier alternatives array
|
||
// Only show suppliers that don't already have a price for this product
|
||
$supplierAlternatives = array();
|
||
$existingPriceSuppliers = array();
|
||
|
||
// If product exists, load existing supplier prices
|
||
if ($existingProductId > 0) {
|
||
$sqlExisting = "SELECT fk_soc FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
||
$sqlExisting .= " WHERE fk_product = ".(int)$existingProductId;
|
||
$resExisting = $db->query($sqlExisting);
|
||
if ($resExisting) {
|
||
while ($objEx = $db->fetch_object($resExisting)) {
|
||
$existingPriceSuppliers[$objEx->fk_soc] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($results as $altResult) {
|
||
// Skip if supplier already has a price for this product
|
||
if ($existingProductId > 0 && isset($existingPriceSuppliers[$altResult['fk_soc']])) {
|
||
continue;
|
||
}
|
||
|
||
$altSupplier = new Societe($db);
|
||
$altSupplier->fetch($altResult['fk_soc']);
|
||
|
||
$altPurchasePrice = $altResult['price'];
|
||
if ($altResult['price_unit'] > 1) {
|
||
$altPurchasePrice = $altResult['price'] / $altResult['price_unit'];
|
||
}
|
||
|
||
$supplierAlternatives[] = array(
|
||
'datanorm_id' => $altResult['id'],
|
||
'fk_soc' => $altResult['fk_soc'],
|
||
'supplier_name' => $altSupplier->name,
|
||
'article_number' => $altResult['article_number'],
|
||
'short_text1' => $altResult['short_text1'],
|
||
'price' => $altResult['price'],
|
||
'price_unit' => $altResult['price_unit'],
|
||
'purchase_price' => $altPurchasePrice,
|
||
'ean' => $altResult['ean'],
|
||
'manufacturer_ref' => $altResult['manufacturer_ref'],
|
||
'is_invoice_supplier' => ($altResult['fk_soc'] == $import->fk_soc),
|
||
);
|
||
}
|
||
|
||
// Store match info for preview
|
||
$datanormPreviewMatches[] = array(
|
||
'line_id' => $lineObj->id,
|
||
'line_supplier_ref' => $lineObj->supplier_ref,
|
||
'line_product_name' => $lineObj->product_name,
|
||
'line_quantity' => $lineObj->quantity,
|
||
'line_unit_price' => $lineObj->unit_price,
|
||
'datanorm_id' => $datanorm->id,
|
||
'datanorm_article_number' => $datanorm->article_number,
|
||
'datanorm_short_text1' => $datanorm->short_text1,
|
||
'datanorm_short_text2' => $datanorm->short_text2,
|
||
'datanorm_price' => $datanorm->price,
|
||
'datanorm_price_unit' => $datanorm->price_unit,
|
||
'datanorm_ean' => $datanorm->ean,
|
||
'purchase_price' => $purchasePrice,
|
||
'selling_price' => $sellingPrice,
|
||
'copper_surcharge' => $copperSurchargeForPrice,
|
||
'existing_product_id' => $existingProductId,
|
||
'action' => $productAction,
|
||
'new_ref' => 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number,
|
||
'supplier_alternatives' => $supplierAlternatives
|
||
);
|
||
}
|
||
}
|
||
}
|
||
$action = 'edit';
|
||
}
|
||
|
||
// Create supplier invoice
|
||
if ($action == 'createinvoice' && $id > 0) {
|
||
$import->fetch($id);
|
||
|
||
// Check prerequisites
|
||
if ($import->fk_soc <= 0) {
|
||
$error++;
|
||
setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors');
|
||
} else {
|
||
// Check all lines have products
|
||
$lines = $importLine->fetchAllByImport($id);
|
||
$allHaveProducts = true;
|
||
foreach ($lines as $line) {
|
||
if ($line->fk_product <= 0) {
|
||
$allHaveProducts = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$allHaveProducts) {
|
||
$error++;
|
||
setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors');
|
||
} else {
|
||
// Load supplier to get default values
|
||
$supplier = new Societe($db);
|
||
$supplier->fetch($import->fk_soc);
|
||
|
||
// Create invoice
|
||
$invoice = new FactureFournisseur($db);
|
||
$invoice->socid = $import->fk_soc;
|
||
$invoice->ref_supplier = $import->invoice_number;
|
||
$invoice->date = $import->invoice_date;
|
||
$invoice->note_private = $langs->trans('ImportedFromZugferd').' ('.$import->ref.')';
|
||
|
||
// Set label to most expensive item (for list display)
|
||
$maxTotal = 0;
|
||
$mostExpensiveLabel = '';
|
||
foreach ($lines as $line) {
|
||
$lineTotal = $line->quantity * $line->unit_price;
|
||
if ($lineTotal > $maxTotal) {
|
||
$maxTotal = $lineTotal;
|
||
$mostExpensiveLabel = $line->product_name;
|
||
}
|
||
}
|
||
if (!empty($mostExpensiveLabel)) {
|
||
// Truncate to 255 chars (database field limit)
|
||
$invoice->label = dol_trunc($mostExpensiveLabel, 255);
|
||
}
|
||
|
||
// Use supplier default values for payment
|
||
$invoice->cond_reglement_id = $supplier->cond_reglement_supplier_id ?: 1;
|
||
$invoice->mode_reglement_id = $supplier->mode_reglement_supplier_id ?: 0;
|
||
$invoice->fk_account = $supplier->fk_account ?: 0;
|
||
|
||
$db->begin();
|
||
$result = $invoice->create($user);
|
||
|
||
if ($result > 0) {
|
||
// Add lines
|
||
foreach ($lines as $line) {
|
||
$res = $invoice->addline(
|
||
$line->product_name,
|
||
$line->unit_price,
|
||
$line->tax_percent,
|
||
0, 0,
|
||
$line->quantity,
|
||
$line->fk_product,
|
||
0, '', '',
|
||
0, 0,
|
||
'HT', // price_base_type - Netto-Preise aus ZUGFeRD
|
||
0 // type (0=product)
|
||
);
|
||
if ($res < 0) {
|
||
$error++;
|
||
setEventMessages($invoice->error, $invoice->errors, 'errors');
|
||
break;
|
||
}
|
||
|
||
// Update EAN on product if not set
|
||
if (!empty($line->ean) && $line->fk_product > 0) {
|
||
$product = new Product($db);
|
||
$product->fetch($line->fk_product);
|
||
if (empty($product->barcode)) {
|
||
$product->barcode = $line->ean;
|
||
$product->barcode_type = 2; // EAN13
|
||
$product->update($product->id, $user);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!$error) {
|
||
// Invoice stays as draft - user can validate manually
|
||
|
||
// Copy PDF to invoice and register in ECM
|
||
// Neuer Pfad: /imports/{id}/{filename}
|
||
$source_pdf = $conf->importzugferd->dir_output.'/imports/'.$import->id.'/'.$import->pdf_filename;
|
||
// Fallback: Alter Pfad /imports/{ref}_{filename}
|
||
if (!file_exists($source_pdf)) {
|
||
$source_pdf = $conf->importzugferd->dir_output.'/imports/'.$import->ref.'_'.$import->pdf_filename;
|
||
}
|
||
if (file_exists($source_pdf)) {
|
||
// Relativer Pfad für ECM (ohne DOL_DATA_ROOT Prefix)
|
||
$rel_dir = 'fournisseur/facture/'.get_exdir($invoice->id, 2, 0, 0, $invoice, 'invoice_supplier').$invoice->ref;
|
||
$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);
|
||
}
|
||
|
||
$dest_file = $dest_dir.'/'.$import->pdf_filename;
|
||
if (@copy($source_pdf, $dest_file)) {
|
||
// In ECM-Datenbank registrieren für korrekte Verknüpfung
|
||
require_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php';
|
||
$ecmfile = new EcmFiles($db);
|
||
$ecmfile->filepath = $rel_dir;
|
||
$ecmfile->filename = $import->pdf_filename;
|
||
$ecmfile->label = md5_file(dol_osencode($dest_file));
|
||
$ecmfile->fullpath_orig = $dest_file;
|
||
$ecmfile->gen_or_uploaded = 'uploaded';
|
||
$ecmfile->description = 'ZUGFeRD Import - '.$import->invoice_number;
|
||
$ecmfile->src_object_type = 'supplier_invoice';
|
||
$ecmfile->src_object_id = $invoice->id;
|
||
$ecmfile->entity = $conf->entity;
|
||
$result = $ecmfile->create($user);
|
||
if ($result < 0) {
|
||
dol_syslog('ImportZugferd: Fehler beim ECM-Eintrag: '.implode(',', $ecmfile->errors), LOG_ERR);
|
||
}
|
||
} else {
|
||
dol_syslog('ImportZugferd: Fehler beim Kopieren der PDF nach '.$dest_dir, LOG_ERR);
|
||
}
|
||
}
|
||
|
||
// 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 && $user->hasRight('importzugferd', 'import', 'delete')) {
|
||
$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>';
|
||
// Form fuer fehlende Lieferantenpreise (ausserhalb der Tabelle, um verschachtelte Forms zu vermeiden)
|
||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" id="missing_prices_form" style="display:none;">';
|
||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||
print '<input type="hidden" name="action" value="addmissingprices">';
|
||
print '<input type="hidden" name="id" value="'.$import->id.'">';
|
||
print '</form>';
|
||
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);
|
||
$allMissingPrices = array(); // Fehlende Lieferantenpreise sammeln
|
||
$hasMissingPrices = false; // Flag für globale Buttons
|
||
|
||
foreach ($lines as $line) {
|
||
$hasProduct = ($line->fk_product > 0);
|
||
$rowStyle = $hasProduct ? 'background-color: #dff0d8;' : ''; // Green for matched products
|
||
|
||
print '<tr class="oddeven" style="'.$rowStyle.'">';
|
||
print '<td>'.$line->line_id.'</td>';
|
||
print '<td>'.dol_escape_htmltag($line->supplier_ref).'</td>';
|
||
print '<td>';
|
||
print dol_escape_htmltag($line->product_name);
|
||
if (!empty($line->ean) && !$hasProduct) {
|
||
print '<br><span style="color: #666;">EAN: '.dol_escape_htmltag($line->ean).'</span>';
|
||
}
|
||
print '</td>';
|
||
print '<td class="right">'.price2num($line->quantity, 'MS').' '.zugferdGetUnitLabel($line->unit_code).'</td>';
|
||
print '<td class="right">';
|
||
print price($line->unit_price);
|
||
if (!empty($line->basis_quantity) && $line->basis_quantity != 1) {
|
||
print '<br><span style="color: #888; font-size: 0.9em;">('.price($line->unit_price_raw).'/'.price2num($line->basis_quantity, 'MS').zugferdGetUnitLabel($line->basis_quantity_unit).')</span>';
|
||
}
|
||
print '</td>';
|
||
|
||
// Dolibarr price column - show supplier price and difference
|
||
print '<td class="right nowraponall">';
|
||
$lineDolibarrTotal = 0;
|
||
if ($hasProduct && $import->fk_soc > 0) {
|
||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
|
||
$productFourn = new ProductFournisseur($db);
|
||
// Use line quantity to find price for matching quantity tier (e.g. 100m cables)
|
||
$searchQty = max(1, $line->quantity);
|
||
$result = $productFourn->find_min_price_product_fournisseur($line->fk_product, $searchQty, $import->fk_soc);
|
||
|
||
if ($result > 0 && $productFourn->fourn_unitprice > 0) {
|
||
// Use unit price for comparison (per-unit, not per-quantity-tier)
|
||
// Note: fourn_unitprice already INCLUDES copper surcharge - it's the total price from invoice
|
||
// The extrafield 'kupferzuschlag' is only informational (shows copper portion of price)
|
||
// The extrafield 'produktpreis' is only informational (shows material price without copper)
|
||
$dolibarrUnitPrice = $productFourn->fourn_unitprice;
|
||
$zugferdPrice = $line->unit_price;
|
||
$priceDiff = $zugferdPrice - $dolibarrUnitPrice;
|
||
$priceDiffPercent = ($dolibarrUnitPrice > 0) ? (($priceDiff / $dolibarrUnitPrice) * 100) : 0;
|
||
|
||
// Accumulate for summary
|
||
$lineDolibarrTotal = $dolibarrUnitPrice * $line->quantity;
|
||
$totalDolibarrHT += $lineDolibarrTotal;
|
||
$hasDolibarrPrices = true;
|
||
$matchedLinesCount++;
|
||
|
||
print price($dolibarrUnitPrice);
|
||
|
||
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> '.dol_escape_htmltag($line->ean).'</span>';
|
||
}
|
||
print ' <i class="fas fa-check-circle" style="color: green;"></i>';
|
||
|
||
// Alle Einkaufspreise des Produktes anzeigen
|
||
$sqlPrices = "SELECT pfp.fk_soc, pfp.price, pfp.unitprice, pfp.ref_fourn, pfp.quantity, s.nom as supplier_name";
|
||
$sqlPrices .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price pfp";
|
||
$sqlPrices .= " LEFT JOIN ".MAIN_DB_PREFIX."societe s ON s.rowid = pfp.fk_soc";
|
||
$sqlPrices .= " WHERE pfp.fk_product = ".(int)$line->fk_product;
|
||
$sqlPrices .= " ORDER BY pfp.unitprice ASC";
|
||
$resPrices = $db->query($sqlPrices);
|
||
if ($resPrices && $db->num_rows($resPrices) > 0) {
|
||
print '<div style="margin-top: 4px; font-size: 0.85em; line-height: 1.4;">';
|
||
while ($objP = $db->fetch_object($resPrices)) {
|
||
$isInvoiceSupplier = ($objP->fk_soc == $import->fk_soc);
|
||
$style = $isInvoiceSupplier ? 'font-weight: bold;' : 'color: #666;';
|
||
print '<div style="'.$style.'">';
|
||
print dol_escape_htmltag($objP->supplier_name);
|
||
print ': <strong>'.price($objP->unitprice).'</strong>';
|
||
if ($objP->quantity > 1) {
|
||
print ' <span class="opacitymedium">('.price($objP->price).'/'.(int)$objP->quantity.'Stk.)</span>';
|
||
}
|
||
if (!empty($objP->ref_fourn)) {
|
||
print ' <span class="opacitymedium">('.dol_escape_htmltag($objP->ref_fourn).')</span>';
|
||
}
|
||
if ($isInvoiceSupplier) {
|
||
print ' <i class="fas fa-file-invoice" style="color: #337ab7;" title="'.$langs->trans('SupplierAssignment').'"></i>';
|
||
}
|
||
print '</div>';
|
||
}
|
||
print '</div>';
|
||
}
|
||
} 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>';
|
||
|
||
// Fehlende Lieferantenpreise aus anderen Katalogen sammeln (Anzeige weiter unten)
|
||
if ($import->fk_soc > 0 && getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL')) {
|
||
// Alle vorhandenen Lieferantenpreise fuer dieses Produkt laden
|
||
$sqlExistingPrices = "SELECT fk_soc, price, unitprice, barcode FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
||
$sqlExistingPrices .= " WHERE fk_product = ".(int)$line->fk_product;
|
||
$resExistingPrices = $db->query($sqlExistingPrices);
|
||
$existingSupplierIds = array();
|
||
$currentSupplierPrice = 0;
|
||
$supplierEan = '';
|
||
if ($resExistingPrices) {
|
||
while ($objPrice = $db->fetch_object($resExistingPrices)) {
|
||
$existingSupplierIds[$objPrice->fk_soc] = true;
|
||
// Stueckpreis und EAN vom Rechnungslieferanten merken
|
||
if ($objPrice->fk_soc == $import->fk_soc) {
|
||
$currentSupplierPrice = $objPrice->unitprice;
|
||
if (!empty($objPrice->barcode)) {
|
||
$supplierEan = $objPrice->barcode;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Suche mit Lieferanten-Artikelnummer (die EAN wird intern für Cross-Catalog verwendet)
|
||
$datanormSearch = new Datanorm($db);
|
||
$allCatalogResults = array();
|
||
|
||
// Suche mit Artikelnummer - die Funktion nutzt dann die EAN für Cross-Catalog
|
||
if (!empty($line->supplier_ref)) {
|
||
$allCatalogResults = $datanormSearch->searchByArticleNumber($line->supplier_ref, $import->fk_soc, true, 10);
|
||
}
|
||
|
||
if (!empty($allCatalogResults)) {
|
||
|
||
$missingSuppliers = array();
|
||
foreach ($allCatalogResults as $catalogResult) {
|
||
if (!isset($existingSupplierIds[$catalogResult['fk_soc']])) {
|
||
$altSupplier = new Societe($db);
|
||
$altSupplier->fetch($catalogResult['fk_soc']);
|
||
|
||
$altPurchasePrice = $catalogResult['price'];
|
||
if ($catalogResult['price_unit'] > 1) {
|
||
$altPurchasePrice = $catalogResult['price'] / $catalogResult['price_unit'];
|
||
}
|
||
|
||
$missingSuppliers[] = array(
|
||
'datanorm_id' => $catalogResult['id'],
|
||
'fk_soc' => $catalogResult['fk_soc'],
|
||
'supplier_name' => $altSupplier->name,
|
||
'article_number' => $catalogResult['article_number'],
|
||
'price' => $catalogResult['price'],
|
||
'price_unit' => $catalogResult['price_unit'],
|
||
'purchase_price' => $altPurchasePrice,
|
||
'ean' => $catalogResult['ean'],
|
||
);
|
||
}
|
||
}
|
||
|
||
if (!empty($missingSuppliers)) {
|
||
// Inline-Anzeige der fehlenden Lieferantenpreise direkt bei der Produktzeile
|
||
$toggleId = 'missing_inline_'.$line->id;
|
||
$missingCount = count($missingSuppliers);
|
||
|
||
print '<div class="missing-suppliers-inline" style="margin-top: 8px; padding: 8px; background-color: #fcf8e3; border: 1px solid #faebcc; border-radius: 4px;">';
|
||
print '<div style="font-weight: bold; cursor: pointer; color: #8a6d3b;" onclick="$(\'#'.$toggleId.'\').toggle(); $(this).find(\'.toggle-icon\').toggleClass(\'fa-chevron-down fa-chevron-up\');">';
|
||
print '<i class="fas fa-plus-circle paddingright"></i>';
|
||
print $langs->trans('MissingSupplierPrices');
|
||
print ' <span class="badge badge-warning">'.$missingCount.'</span>';
|
||
print ' <i class="fas fa-chevron-up toggle-icon" style="float: right; color: #999;"></i>';
|
||
print '</div>';
|
||
|
||
// Aufklappbarer Bereich (Standard: sichtbar/aufgeklappt)
|
||
print '<div id="'.$toggleId.'" class="missing-suppliers-content" style="margin-top: 5px;">';
|
||
|
||
foreach ($missingSuppliers as $missing) {
|
||
$priceDiffHtml = '';
|
||
if ($currentSupplierPrice > 0) {
|
||
$pDiff = $missing['purchase_price'] - $currentSupplierPrice;
|
||
$pDiffPercent = ($pDiff / $currentSupplierPrice) * 100;
|
||
if ($pDiff < 0) {
|
||
$priceDiffHtml = ' <span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.number_format(abs($pDiffPercent), 1).'%</span>';
|
||
} elseif ($pDiff > 0) {
|
||
$priceDiffHtml = ' <span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($pDiffPercent, 1).'%</span>';
|
||
} else {
|
||
$priceDiffHtml = ' <span class="opacitymedium">=</span>';
|
||
}
|
||
}
|
||
|
||
// Wert: productId,socId,datanormId
|
||
$cbValue = $line->fk_product.','.$missing['fk_soc'].','.$missing['datanorm_id'];
|
||
print '<div style="padding: 3px 0 3px 10px;">';
|
||
print '<label style="cursor: pointer; display: block;">';
|
||
print '<input type="checkbox" name="add_prices[]" value="'.dol_escape_htmltag($cbValue).'" checked class="cb-missing-price" form="missing_prices_form" style="margin-right: 5px;">';
|
||
print '<strong>'.dol_escape_htmltag($missing['supplier_name']).'</strong>';
|
||
print ' <code style="font-size: 0.9em;">'.dol_escape_htmltag($missing['article_number']).'</code>';
|
||
print ' @ <strong>'.price($missing['purchase_price']).'</strong>';
|
||
print $priceDiffHtml;
|
||
print '</label>';
|
||
print '</div>';
|
||
}
|
||
print '</div>'; // End toggleable div
|
||
print '</div>'; // End inline box
|
||
|
||
// Track for global actions
|
||
$hasMissingPrices = true;
|
||
}
|
||
}
|
||
}
|
||
} 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_'.$line->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>';
|
||
|
||
// Refresh-Button nach Produktanlage
|
||
print ' <a href="'.$_SERVER['PHP_SELF'].'?id='.$import->id.'" class="button buttongen margintoponlyshort" title="'.$langs->trans('RefreshProductListHelp').'">';
|
||
print '<i class="fas fa-sync-alt"></i>';
|
||
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_'.$line->id, '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth100 maxwidth150', 1, '', 0);
|
||
print ' <button type="submit" class="button buttongen" title="'.$langs->trans('ProductTemplateHelp').'">';
|
||
print '<i class="fas fa-copy"></i>';
|
||
print '</button>';
|
||
print '</form>';
|
||
|
||
// Datanorm button (only if supplier is set and supplier_ref exists)
|
||
if ($import->fk_soc > 0 && !empty($line->supplier_ref)) {
|
||
// Check if Datanorm article exists
|
||
$datanormCheck = new Datanorm($db);
|
||
$searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0);
|
||
$datanormResults = $datanormCheck->searchByArticleNumber($line->supplier_ref, $import->fk_soc, $searchAll, 1);
|
||
|
||
if (!empty($datanormResults)) {
|
||
$datanormArticle = $datanormResults[0];
|
||
print '<br>';
|
||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=createfromdatanorm&line_id='.$line->id.'&id='.$import->id.'&token='.newToken().'" class="button buttongen margintoponlyshort" title="'.$langs->trans('CreateFromDatanormHelp').'">';
|
||
print '<i class="fas fa-database paddingright"></i>'.$langs->trans('CreateFromDatanorm');
|
||
print '</a>';
|
||
// Button to show raw Datanorm data
|
||
print ' <a href="#" class="button buttongen margintoponlyshort" onclick="showRawDatanorm(\''.dol_escape_js($line->supplier_ref).'\', '.$import->fk_soc.'); return false;" title="'.$langs->trans('ShowRawDatanorm').'">';
|
||
print '<i class="fas fa-file-code"></i>';
|
||
print '</a>';
|
||
// Show comparison: Invoice name vs Datanorm name
|
||
print '<div class="small" style="margin-top: 5px; padding: 5px; background-color: #e8f4fd; border-radius: 3px;">';
|
||
print '<table class="noborder" style="width: 100%; font-size: 0.85em;">';
|
||
print '<tr><td style="color: #666; width: 70px;"><i class="fas fa-file-invoice"></i> Rechnung:</td>';
|
||
print '<td><strong>'.dol_trunc($line->product_name, 50).'</strong></td></tr>';
|
||
print '<tr><td style="color: #666;"><i class="fas fa-database"></i> Datanorm:</td>';
|
||
print '<td style="color: #2980b9;"><strong>'.dol_trunc($datanormArticle['short_text1'], 50).'</strong>';
|
||
print ' <span style="color: #27ae60;">('.price($datanormArticle['price']).')</span></td></tr>';
|
||
print '</table>';
|
||
print '</div>';
|
||
}
|
||
}
|
||
}
|
||
print '</td>';
|
||
print '</tr>';
|
||
|
||
// Accumulate ZUGFeRD total
|
||
$totalZugferdHT += $line->line_total;
|
||
}
|
||
|
||
// Summary row with total comparison
|
||
// Only show full comparison if ALL products are matched with Dolibarr prices
|
||
print '<tr style="background-color: #f5f5f5; font-weight: bold;">';
|
||
print '<td colspan="5" class="right"><strong>'.$langs->trans('Total').' '.$langs->trans('TotalHT').'</strong></td>';
|
||
|
||
if ($allProductsMatched && $hasDolibarrPrices) {
|
||
// Full comparison possible - all products matched with prices
|
||
$totalDiff = $totalZugferdHT - $totalDolibarrHT;
|
||
$totalDiffPercent = ($totalDolibarrHT > 0) ? (($totalDiff / $totalDolibarrHT) * 100) : 0;
|
||
|
||
// Determine colors: green if close match, red if significant difference
|
||
$threshold = getDolGlobalInt('IMPORTZUGFERD_PRICE_DIFF_THRESHOLD', 10);
|
||
$isMatch = (abs($totalDiffPercent) < 0.5); // Less than 0.5% difference = match
|
||
$isSignificant = (abs($totalDiffPercent) >= $threshold);
|
||
|
||
if ($isMatch) {
|
||
$cellStyle = 'background-color: #dff0d8;'; // Green
|
||
} elseif ($isSignificant) {
|
||
$cellStyle = 'background-color: #f2dede;'; // Red
|
||
} else {
|
||
$cellStyle = 'background-color: #fcf8e3;'; // Yellow/warning
|
||
}
|
||
|
||
print '<td class="right nowraponall" style="'.$cellStyle.'">';
|
||
print '<strong>'.price($totalDolibarrHT).'</strong>';
|
||
if (abs($totalDiffPercent) >= 0.01) {
|
||
print '<br>';
|
||
if ($totalDiff > 0) {
|
||
print '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($totalDiffPercent, 1).'%</span>';
|
||
} elseif ($totalDiff < 0) {
|
||
print '<span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.number_format($totalDiffPercent, 1).'%</span>';
|
||
}
|
||
}
|
||
print '</td>';
|
||
print '<td class="right" style="'.$cellStyle.'"><strong>'.price($totalZugferdHT).'</strong></td>';
|
||
print '<td colspan="2" class="nowraponall" style="'.$cellStyle.'">';
|
||
if ($isMatch) {
|
||
print '<span style="color: #3c763d;"><i class="fas fa-check-circle"></i> '.$langs->trans('SumValidationOk').'</span>';
|
||
} else {
|
||
print '<span style="color: #a94442;"><i class="fas fa-exclamation-triangle"></i> '.$langs->trans('Difference').': '.price($totalDiff).' '.$import->currency.'</span>';
|
||
}
|
||
print '</td>';
|
||
} else {
|
||
// Not all products matched - show totals but no comparison
|
||
print '<td class="right nowraponall">';
|
||
if ($hasDolibarrPrices) {
|
||
print '<span class="opacitymedium">'.price($totalDolibarrHT).'</span>';
|
||
print '<br><span class="opacitymedium small">('.$matchedLinesCount.'/'.$totalLinesCount.')</span>';
|
||
} else {
|
||
print '<span class="opacitymedium">-</span>';
|
||
}
|
||
print '</td>';
|
||
print '<td class="right"><strong>'.price($totalZugferdHT).'</strong></td>';
|
||
print '<td colspan="2" class="nowraponall">';
|
||
print '<span class="opacitymedium"><i class="fas fa-info-circle"></i> '.$langs->trans('ProductsNotAssigned').'</span>';
|
||
print '</td>';
|
||
}
|
||
print '</tr>';
|
||
|
||
print '</table>';
|
||
print '</div>';
|
||
|
||
// Aktionsbereich für fehlende Lieferantenpreise (wenn vorhanden)
|
||
if ($hasMissingPrices) {
|
||
print '<div style="margin-top: 10px; padding: 10px; background-color: #fcf8e3; border: 1px solid #faebcc; border-radius: 4px;">';
|
||
print '<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 10px;">';
|
||
|
||
// Toggle Buttons
|
||
print '<span style="color: #8a6d3b; font-weight: bold;"><i class="fas fa-plus-circle paddingright"></i>'.$langs->trans('MissingSupplierPrices').'</span>';
|
||
print '<a href="#" onclick="$(\'.missing-suppliers-content\').show(); $(\'.missing-suppliers-inline .toggle-icon\').removeClass(\'fa-chevron-down\').addClass(\'fa-chevron-up\'); return false;" class="button buttongen small">';
|
||
print '<i class="fas fa-expand-alt paddingright"></i>'.$langs->trans('ExpandAll');
|
||
print '</a>';
|
||
print '<a href="#" onclick="$(\'.missing-suppliers-content\').hide(); $(\'.missing-suppliers-inline .toggle-icon\').removeClass(\'fa-chevron-up\').addClass(\'fa-chevron-down\'); return false;" class="button buttongen small">';
|
||
print '<i class="fas fa-compress-alt paddingright"></i>'.$langs->trans('CollapseAll');
|
||
print '</a>';
|
||
|
||
// Checkbox Buttons
|
||
print '<span style="border-left: 1px solid #ccc; padding-left: 10px;">';
|
||
print '<a href="#" onclick="$(\'.cb-missing-price\').prop(\'checked\', true); return false;" class="button buttongen small">';
|
||
print '<i class="fas fa-check-square paddingright"></i>'.$langs->trans('SelectAll');
|
||
print '</a>';
|
||
print '</span>';
|
||
print '<a href="#" onclick="$(\'.cb-missing-price\').prop(\'checked\', false); return false;" class="button buttongen small">';
|
||
print '<i class="fas fa-square paddingright"></i>'.$langs->trans('DeselectAll');
|
||
print '</a>';
|
||
|
||
// Submit Button
|
||
print '<span style="border-left: 1px solid #ccc; padding-left: 10px;">';
|
||
print '<button type="submit" form="missing_prices_form" class="button buttongen">';
|
||
print '<i class="fas fa-plus paddingright"></i>'.$langs->trans('AddSelectedPrices');
|
||
print '</button>';
|
||
print '</span>';
|
||
|
||
print '</div>';
|
||
print '</div>';
|
||
}
|
||
|
||
// missing_prices_form ist bereits oben geschlossen (ausgelagert wegen verschachtelter Forms)
|
||
|
||
// Datanorm Preview Section (shown when preview action was triggered)
|
||
if (!empty($datanormPreviewMatches)) {
|
||
print '<div class="div-table-responsive-no-min" style="margin-top: 20px;">';
|
||
print '<div class="titre" style="margin-bottom: 10px;">';
|
||
print '<i class="fas fa-database paddingright"></i>'.$langs->trans('DatanormPreview');
|
||
print ' <span class="badge badge-info">'.count($datanormPreviewMatches).' '.$langs->trans('Matches').'</span>';
|
||
print '</div>';
|
||
|
||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" id="datanorm_confirm_form">';
|
||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||
print '<input type="hidden" name="action" value="createallfromdatanorm">';
|
||
print '<input type="hidden" name="id" value="'.$import->id.'">';
|
||
|
||
print '<table class="noborder centpercent">';
|
||
print '<tr class="liste_titre">';
|
||
print '<th class="center" style="width: 30px;"><input type="checkbox" id="checkall_datanorm" checked></th>';
|
||
print '<th>'.$langs->trans('SupplierRef').'</th>';
|
||
print '<th>'.$langs->trans('InvoiceProductName').'</th>';
|
||
print '<th>'.$langs->trans('DatanormProductName').'</th>';
|
||
print '<th class="right">'.$langs->trans('InvoicePrice').'</th>';
|
||
print '<th class="right">'.$langs->trans('DatanormPrice').'</th>';
|
||
print '<th class="right">'.$langs->trans('SellingPrice').'</th>';
|
||
print '<th class="center">'.$langs->trans('Action').'</th>';
|
||
print '</tr>';
|
||
|
||
$countCreate = 0;
|
||
$countAssign = 0;
|
||
foreach ($datanormPreviewMatches as $match) {
|
||
$rowClass = ($match['action'] == 'assign') ? 'background-color: #d9edf7;' : 'background-color: #dff0d8;';
|
||
|
||
print '<tr class="oddeven" style="'.$rowClass.'">';
|
||
print '<td class="center"><input type="checkbox" name="selected_lines[]" value="'.$match['line_id'].'" checked></td>';
|
||
print '<td><strong>'.$match['datanorm_article_number'].'</strong>';
|
||
if (!empty($match['datanorm_ean'])) {
|
||
print '<br><span class="small" style="color: #666;">EAN: '.$match['datanorm_ean'].'</span>';
|
||
}
|
||
print '</td>';
|
||
|
||
// Invoice product name
|
||
print '<td>';
|
||
print '<span style="color: #666;">'.dol_trunc($match['line_product_name'], 40).'</span>';
|
||
print '</td>';
|
||
|
||
// Datanorm product name
|
||
print '<td>';
|
||
print '<strong style="color: #2980b9;">'.dol_trunc($match['datanorm_short_text1'], 40).'</strong>';
|
||
if (!empty($match['datanorm_short_text2'])) {
|
||
print '<br><span class="small" style="color: #666;">'.dol_trunc($match['datanorm_short_text2'], 40).'</span>';
|
||
}
|
||
print '</td>';
|
||
|
||
// Invoice price (from ZUGFeRD)
|
||
print '<td class="right nowraponall">';
|
||
print '<strong>'.price($match['line_unit_price']).'</strong>';
|
||
print '</td>';
|
||
|
||
// Datanorm price - show original price and calculated unit price
|
||
print '<td class="right nowraponall">';
|
||
if ($match['datanorm_price_unit'] > 1) {
|
||
// Show original price and price unit
|
||
print '<span class="small" style="color: #666;">'.price($match['datanorm_price']).'/'.$match['datanorm_price_unit'].'</span>';
|
||
print '<br><strong>= '.price($match['purchase_price']).'</strong>';
|
||
} else {
|
||
print '<strong>'.price($match['purchase_price']).'</strong>';
|
||
}
|
||
if ($match['copper_surcharge'] > 0) {
|
||
print '<br><span class="small" style="color: #d9534f;">+ '.price($match['copper_surcharge']).' Cu</span>';
|
||
}
|
||
print '</td>';
|
||
|
||
// Selling price
|
||
print '<td class="right nowraponall"><strong style="color: #27ae60;">'.price($match['selling_price']).'</strong></td>';
|
||
|
||
// Action
|
||
print '<td class="center nowraponall">';
|
||
if ($match['action'] == 'assign') {
|
||
print '<span class="badge badge-info" title="'.$langs->trans('ProductAlreadyExists').'"><i class="fas fa-link"></i> '.$langs->trans('Assign').'</span>';
|
||
$countAssign++;
|
||
} else {
|
||
print '<span class="badge badge-success"><i class="fas fa-plus"></i> '.$langs->trans('Create').'</span>';
|
||
print '<br><span class="small" style="color: #666;">'.$match['new_ref'].'</span>';
|
||
$countCreate++;
|
||
}
|
||
print '</td>';
|
||
print '</tr>';
|
||
|
||
// Show supplier alternatives if available (more than 1 supplier found)
|
||
if (!empty($match['supplier_alternatives']) && count($match['supplier_alternatives']) > 1) {
|
||
$altCount = count($match['supplier_alternatives']);
|
||
$toggleId = 'alt_'.$match['line_id'];
|
||
print '<tr class="oddeven" style="background-color: #f9f9f9;">';
|
||
print '<td></td>'; // Empty checkbox column
|
||
print '<td colspan="7" style="padding: 10px;">';
|
||
print '<div style="background-color: #fff; border: 1px solid #ddd; border-radius: 5px; padding: 10px;">';
|
||
// Header mit Anzahl und Toggle-Button
|
||
print '<div style="font-weight: bold; margin-bottom: 8px; color: #555; cursor: pointer;" onclick="$(\'#'.$toggleId.'\').toggle(); $(this).find(\'.toggle-icon\').toggleClass(\'fa-chevron-down fa-chevron-up\');">';
|
||
print '<i class="fas fa-store-alt paddingright"></i>'.$langs->trans('SupplierAlternatives');
|
||
print ' <span class="badge" style="background-color: #5bc0de;">'.$altCount.' '.$langs->trans('Suppliers').'</span>';
|
||
print ' <i class="fas fa-chevron-up toggle-icon" style="float: right; color: #999;"></i>';
|
||
print '</div>';
|
||
// Aufklappbarer Bereich (Standard: sichtbar)
|
||
print '<div id="'.$toggleId.'">';
|
||
|
||
print '<table class="noborder" style="width: 100%; font-size: 0.9em;">';
|
||
print '<tr style="background-color: #f5f5f5;">';
|
||
print '<th class="center" style="width: 40px;">'.$langs->trans('Select').'</th>';
|
||
print '<th>'.$langs->trans('Supplier').'</th>';
|
||
print '<th>'.$langs->trans('SupplierRef').'</th>';
|
||
print '<th>'.$langs->trans('EAN').'</th>';
|
||
print '<th>'.$langs->trans('ManufacturerRef').'</th>';
|
||
print '<th class="right">'.$langs->trans('UnitPrice').'</th>';
|
||
print '<th class="right">'.$langs->trans('Difference').'</th>';
|
||
print '</tr>';
|
||
|
||
$lowestPrice = PHP_FLOAT_MAX;
|
||
foreach ($match['supplier_alternatives'] as $alt) {
|
||
if ($alt['purchase_price'] < $lowestPrice) {
|
||
$lowestPrice = $alt['purchase_price'];
|
||
}
|
||
}
|
||
|
||
foreach ($match['supplier_alternatives'] as $altIdx => $alt) {
|
||
$isInvoiceSupplier = $alt['is_invoice_supplier'];
|
||
$isCheapest = ($alt['purchase_price'] == $lowestPrice);
|
||
$rowStyle = '';
|
||
if ($isInvoiceSupplier) {
|
||
$rowStyle = 'background-color: #d9edf7;'; // Blue for invoice supplier
|
||
} elseif ($isCheapest) {
|
||
$rowStyle = 'background-color: #dff0d8;'; // Green for cheapest
|
||
}
|
||
|
||
print '<tr style="'.$rowStyle.'">';
|
||
|
||
// Checkbox for selecting this supplier as purchase source
|
||
print '<td class="center">';
|
||
$checkboxName = 'supplier_prices['.$match['line_id'].']['.$alt['fk_soc'].']';
|
||
$checked = $isInvoiceSupplier ? ' checked' : '';
|
||
print '<input type="checkbox" name="'.$checkboxName.'" value="'.$alt['datanorm_id'].'"'.$checked.' title="'.$langs->trans('AddAsPurchasePrice').'">';
|
||
print '</td>';
|
||
|
||
// Supplier name
|
||
print '<td>';
|
||
print '<strong>'.dol_escape_htmltag($alt['supplier_name']).'</strong>';
|
||
if ($isInvoiceSupplier) {
|
||
print ' <span class="badge" style="background-color: #337ab7; font-size: 0.75em;"><i class="fas fa-file-invoice"></i></span>';
|
||
}
|
||
if ($isCheapest) {
|
||
print ' <span class="badge" style="background-color: #5cb85c; font-size: 0.75em;"><i class="fas fa-tag"></i></span>';
|
||
}
|
||
print '</td>';
|
||
|
||
// Supplier article number
|
||
print '<td><code>'.dol_escape_htmltag($alt['article_number']).'</code></td>';
|
||
|
||
// EAN
|
||
print '<td>';
|
||
if (!empty($alt['ean'])) {
|
||
print '<span class="small">'.dol_escape_htmltag($alt['ean']).'</span>';
|
||
} else {
|
||
print '<span class="opacitymedium">-</span>';
|
||
}
|
||
print '</td>';
|
||
|
||
// Manufacturer ref
|
||
print '<td>';
|
||
if (!empty($alt['manufacturer_ref'])) {
|
||
print '<span class="small">'.dol_escape_htmltag($alt['manufacturer_ref']).'</span>';
|
||
} else {
|
||
print '<span class="opacitymedium">-</span>';
|
||
}
|
||
print '</td>';
|
||
|
||
// Price
|
||
print '<td class="right nowraponall">';
|
||
if ($alt['price_unit'] > 1) {
|
||
print '<span class="small" style="color: #666;">'.price($alt['price']).'/'.$alt['price_unit'].'</span><br>';
|
||
}
|
||
print '<strong>'.price($alt['purchase_price']).'</strong>';
|
||
print '</td>';
|
||
|
||
// Difference from invoice supplier price
|
||
print '<td class="right nowraponall">';
|
||
if (!$isInvoiceSupplier && isset($match['purchase_price'])) {
|
||
$diff = $alt['purchase_price'] - $match['purchase_price'];
|
||
$diffPercent = ($match['purchase_price'] > 0) ? ($diff / $match['purchase_price'] * 100) : 0;
|
||
if ($diff < 0) {
|
||
print '<span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.price(abs($diff)).' ('.number_format(abs($diffPercent), 1).'%)</span>';
|
||
} elseif ($diff > 0) {
|
||
print '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.price($diff).' (+'.number_format($diffPercent, 1).'%)</span>';
|
||
} else {
|
||
print '<span class="opacitymedium">=</span>';
|
||
}
|
||
} else {
|
||
print '<span class="opacitymedium">-</span>';
|
||
}
|
||
print '</td>';
|
||
|
||
print '</tr>';
|
||
}
|
||
|
||
print '</table>';
|
||
print '<div class="small opacitymedium" style="margin-top: 5px;">';
|
||
print '<i class="fas fa-info-circle"></i> '.$langs->trans('SelectSuppliersForPurchasePrices');
|
||
print '</div>';
|
||
print '</div>'; // End toggleable div
|
||
print '</div>'; // End white box
|
||
print '</td>';
|
||
print '</tr>';
|
||
}
|
||
}
|
||
|
||
print '</table>';
|
||
|
||
// Summary and confirm button
|
||
print '<div class="center" style="margin-top: 15px; padding: 15px; background-color: #f5f5f5; border-radius: 5px;">';
|
||
print '<div style="margin-bottom: 10px;">';
|
||
if ($countCreate > 0) {
|
||
print '<span class="badge badge-success" style="margin-right: 10px;"><i class="fas fa-plus"></i> '.$countCreate.' '.$langs->trans('ToCreate').'</span>';
|
||
}
|
||
if ($countAssign > 0) {
|
||
print '<span class="badge badge-info"><i class="fas fa-link"></i> '.$countAssign.' '.$langs->trans('ToAssign').'</span>';
|
||
}
|
||
print '</div>';
|
||
print '<button type="submit" class="button button-primary">';
|
||
print '<i class="fas fa-check paddingright"></i>'.$langs->trans('ConfirmAndCreateProducts');
|
||
print '</button>';
|
||
print ' ';
|
||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=edit&id='.$import->id.'" class="button">';
|
||
print '<i class="fas fa-times paddingright"></i>'.$langs->trans('Cancel');
|
||
print '</a>';
|
||
print '</div>';
|
||
|
||
print '</form>';
|
||
print '</div>';
|
||
|
||
// JavaScript for select all checkbox
|
||
print '<script>
|
||
$(document).ready(function() {
|
||
$("#checkall_datanorm").change(function() {
|
||
$("input[name=\'selected_lines[]\']").prop("checked", this.checked);
|
||
});
|
||
});
|
||
</script>';
|
||
}
|
||
|
||
// Action buttons
|
||
print '<div class="center" style="margin-top: 20px;">';
|
||
|
||
if ($allComplete) {
|
||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=createinvoice&id='.$import->id.'&token='.newToken().'" class="button button-primary">';
|
||
print '<i class="fas fa-file-invoice paddingright"></i>'.$langs->trans('CreateSupplierInvoice');
|
||
print '</a>';
|
||
print ' ';
|
||
}
|
||
|
||
// Finish import button - shown when pending status and all products assigned
|
||
if ($import->status == ZugferdImport::STATUS_PENDING && $allComplete) {
|
||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=finishimport&id='.$import->id.'&token='.newToken().'" class="button">';
|
||
print '<i class="fas fa-check paddingright"></i>'.$langs->trans('FinishImport');
|
||
print '</a>';
|
||
print ' ';
|
||
}
|
||
|
||
// Datanorm buttons - show when products are missing and supplier is set
|
||
if ($missingProducts > 0 && $import->fk_soc > 0 && empty($datanormPreviewMatches)) {
|
||
// "Alle zuordnen" - creates all products from Datanorm
|
||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=createallfromdatanorm&id='.$import->id.'&token='.newToken().'" class="button">';
|
||
print '<i class="fas fa-database paddingright"></i>'.$langs->trans('AssignAllFromDatanorm');
|
||
print '</a>';
|
||
print ' ';
|
||
|
||
// "Datanorm Vorschau" - preview what will be created
|
||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=previewdatanorm&id='.$import->id.'&token='.newToken().'" class="button">';
|
||
print '<i class="fas fa-search paddingright"></i>'.$langs->trans('PreviewDatanormMatches');
|
||
print '</a>';
|
||
print ' ';
|
||
}
|
||
|
||
print '<a href="'.dol_buildpath('/importzugferd/list.php', 1).'" class="button">'.$langs->trans('BackToList').'</a>';
|
||
|
||
// Delete button - show for pending imports or imports without linked invoice
|
||
$canDelete = ($import->status == ZugferdImport::STATUS_PENDING) ||
|
||
($import->status == ZugferdImport::STATUS_IMPORTED && $import->fk_facture_fourn <= 0);
|
||
if ($canDelete) {
|
||
print ' ';
|
||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&id='.$import->id.'&token='.newToken().'" class="button button-cancel">';
|
||
print '<i class="fas fa-trash paddingright"></i>'.$langs->trans('Delete');
|
||
print '</a>';
|
||
}
|
||
|
||
print '</div>';
|
||
|
||
print '</div>';
|
||
|
||
// Modal CSS and HTML for raw Datanorm data
|
||
print '<style>
|
||
.datanorm-modal-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.5);
|
||
z-index: 9999;
|
||
}
|
||
.datanorm-modal {
|
||
position: relative;
|
||
background: white;
|
||
width: 90%;
|
||
max-width: 700px;
|
||
max-height: 85vh;
|
||
margin: 50px auto;
|
||
padding: 20px;
|
||
border-radius: 5px;
|
||
overflow-y: auto;
|
||
}
|
||
.datanorm-modal h3 {
|
||
margin-top: 0;
|
||
color: #333;
|
||
}
|
||
.datanorm-modal h4 {
|
||
margin-bottom: 5px;
|
||
color: #555;
|
||
border-bottom: 1px solid #ddd;
|
||
padding-bottom: 5px;
|
||
}
|
||
.datanorm-modal pre {
|
||
background: #f5f5f5;
|
||
padding: 10px;
|
||
font-size: 11px;
|
||
overflow-x: auto;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
.datanorm-modal .close-btn {
|
||
float: right;
|
||
cursor: pointer;
|
||
font-size: 20px;
|
||
color: #999;
|
||
}
|
||
.datanorm-modal .close-btn:hover {
|
||
color: #333;
|
||
}
|
||
</style>';
|
||
|
||
// Modal for raw data
|
||
print '<div id="datanormRawModal" class="datanorm-modal-overlay" onclick="closeRawDatanormModal(event)">';
|
||
print '<div class="datanorm-modal" onclick="event.stopPropagation()">';
|
||
print '<span class="close-btn" onclick="closeRawDatanormModal()">×</span>';
|
||
print '<h3><i class="fas fa-file-code"></i> Rohdaten: <span id="modalArticleNumber"></span></h3>';
|
||
print '<div id="modalContent">';
|
||
print '<p><em>Laden...</em></p>';
|
||
print '</div>';
|
||
print '</div>';
|
||
print '</div>';
|
||
|
||
print '<script>
|
||
function showRawDatanorm(articleNumber, fkSoc) {
|
||
document.getElementById("datanormRawModal").style.display = "block";
|
||
document.getElementById("modalArticleNumber").textContent = articleNumber;
|
||
document.getElementById("modalContent").innerHTML = "<p><em>Laden...</em></p>";
|
||
|
||
// AJAX request
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open("GET", "'.dol_escape_js($_SERVER['PHP_SELF']).'?action=get_raw_lines&article_number=" + encodeURIComponent(articleNumber) + "&fk_soc=" + fkSoc + "&token='.newToken().'", true);
|
||
xhr.onreadystatechange = function() {
|
||
if (xhr.readyState === 4) {
|
||
if (xhr.status === 200) {
|
||
try {
|
||
var data = JSON.parse(xhr.responseText);
|
||
var html = "";
|
||
|
||
html += "<h4>DATANORM (A-Satz)</h4>";
|
||
if (data.datanorm_line) {
|
||
html += "<pre>" + escapeHtml(data.datanorm_line) + "</pre>";
|
||
// Parse and show fields
|
||
var parts = data.datanorm_line.split(";");
|
||
html += "<table class=\"noborder\" style=\"margin-top:10px;font-size:12px;\">";
|
||
html += "<tr><td><b>0: Satzart</b></td><td>" + escapeHtml(parts[0] || "") + "</td></tr>";
|
||
html += "<tr><td><b>1: Aktionscode</b></td><td>" + escapeHtml(parts[1] || "") + "</td></tr>";
|
||
html += "<tr><td><b>2: Artikelnummer</b></td><td>" + escapeHtml(parts[2] || "") + "</td></tr>";
|
||
html += "<tr><td><b>3: Textkennzeichen</b></td><td>" + escapeHtml(parts[3] || "") + "</td></tr>";
|
||
html += "<tr><td><b>4: Kurztext 1</b></td><td>" + escapeHtml(parts[4] || "") + "</td></tr>";
|
||
html += "<tr><td><b>5: Kurztext 2</b></td><td>" + escapeHtml(parts[5] || "") + "</td></tr>";
|
||
html += "<tr><td><b>6: Preiskennzeichen</b></td><td>" + escapeHtml(parts[6] || "") + " (1=Brutto, 2=Netto)</td></tr>";
|
||
html += "<tr><td><b>7: PE-Code</b></td><td>" + escapeHtml(parts[7] || "") + " (0=1, 1=10, 2=100, 3=1000)</td></tr>";
|
||
html += "<tr><td><b>8: Mengeneinheit</b></td><td>" + escapeHtml(parts[8] || "") + "</td></tr>";
|
||
html += "<tr><td><b>9: Preis (Cent)</b></td><td>" + escapeHtml(parts[9] || "") + "</td></tr>";
|
||
html += "<tr><td><b>10: Rabattgruppe</b></td><td>" + escapeHtml(parts[10] || "") + "</td></tr>";
|
||
html += "<tr><td><b>11: Warengruppe</b></td><td>" + escapeHtml(parts[11] || "") + "</td></tr>";
|
||
html += "</table>";
|
||
} else {
|
||
html += "<p class=\"opacitymedium\">Keine DATANORM-Zeile gefunden</p>";
|
||
}
|
||
|
||
html += "<br><h4>DATPREIS (P-Satz)</h4>";
|
||
if (data.datpreis_line) {
|
||
html += "<pre>" + escapeHtml(data.datpreis_line) + "</pre>";
|
||
// Parse P-Satz
|
||
var pparts = data.datpreis_line.split(";");
|
||
html += "<table class=\"noborder\" style=\"margin-top:10px;font-size:12px;\">";
|
||
html += "<tr><td><b>0: Satzart</b></td><td>" + escapeHtml(pparts[0] || "") + "</td></tr>";
|
||
html += "<tr><td><b>1: Aktionscode</b></td><td>" + escapeHtml(pparts[1] || "") + "</td></tr>";
|
||
// Find the article in the P-Satz (multiple articles per line)
|
||
for (var i = 2; i < pparts.length; i += 9) {
|
||
if (pparts[i] && pparts[i].trim() === articleNumber) {
|
||
html += "<tr><td colspan=\"2\"><b>--- Artikel " + escapeHtml(pparts[i]) + " ---</b></td></tr>";
|
||
html += "<tr><td><b>" + i + ": Artikelnummer</b></td><td>" + escapeHtml(pparts[i] || "") + "</td></tr>";
|
||
html += "<tr><td><b>" + (i+1) + ": Preiskennzeichen</b></td><td>" + escapeHtml(pparts[i+1] || "") + " (2=Netto)</td></tr>";
|
||
html += "<tr><td><b>" + (i+2) + ": Preis (Cent)</b></td><td>" + escapeHtml(pparts[i+2] || "") + " = " + (parseFloat(pparts[i+2] || 0) / 100).toFixed(2) + " EUR</td></tr>";
|
||
html += "<tr><td><b>" + (i+3) + ": Unbekannt</b></td><td>" + escapeHtml(pparts[i+3] || "") + "</td></tr>";
|
||
html += "<tr><td><b>" + (i+4) + ": Metallzuschlag (Cent)</b></td><td>" + escapeHtml(pparts[i+4] || "") + " = " + (parseFloat(pparts[i+4] || 0) / 100).toFixed(2) + " EUR</td></tr>";
|
||
break;
|
||
}
|
||
}
|
||
html += "</table>";
|
||
} else {
|
||
html += "<p class=\"opacitymedium\">Keine DATPREIS-Zeile gefunden</p>";
|
||
}
|
||
|
||
if (data.error) {
|
||
html += "<br><p class=\"error\">" + escapeHtml(data.error) + "</p>";
|
||
}
|
||
|
||
document.getElementById("modalContent").innerHTML = html;
|
||
} catch(e) {
|
||
document.getElementById("modalContent").innerHTML = "<p class=\"error\">Fehler beim Parsen: " + e.message + "</p><pre>" + escapeHtml(xhr.responseText) + "</pre>";
|
||
}
|
||
} else {
|
||
document.getElementById("modalContent").innerHTML = "<p class=\"error\">HTTP Fehler: " + xhr.status + "</p>";
|
||
}
|
||
}
|
||
};
|
||
xhr.send();
|
||
}
|
||
|
||
function closeRawDatanormModal(event) {
|
||
if (!event || event.target.id === "datanormRawModal") {
|
||
document.getElementById("datanormRawModal").style.display = "none";
|
||
}
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
var div = document.createElement("div");
|
||
div.appendChild(document.createTextNode(text || ""));
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Close modal with ESC key
|
||
document.addEventListener("keydown", function(e) {
|
||
if (e.key === "Escape") {
|
||
closeRawDatanormModal();
|
||
}
|
||
});
|
||
</script>';
|
||
}
|
||
|
||
llxFooter();
|
||
$db->close();
|