Fehler beseitigt import verbessert

This commit is contained in:
Eduard Wisch 2026-02-02 20:20:36 +01:00
parent 1b71636306
commit 6b19944504
12 changed files with 1015 additions and 34 deletions

Binary file not shown.

View file

@ -10,7 +10,8 @@
"Bash(grep:*)",
"Bash(mysql:*)",
"Bash(ls:*)",
"Bash(php:*)"
"Bash(php:*)",
"Bash(mariadb:*)"
]
}
}

View file

@ -166,6 +166,39 @@ $item->fieldAttr['placeholder'] = '30';
$formSetup->newItem('IMPORTZUGFERD_DATANORM_SEARCH_ALL')->setAsYesNo();
// Accounting Codes Section (Standard-Konten für neue Produkte)
$formSetup->newItem('AccountingSettings')->setAsTitle();
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_SELL');
$item->defaultFieldValue = '';
$item->cssClass = 'minwidth200';
$item->fieldAttr['placeholder'] = '700000';
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA');
$item->defaultFieldValue = '';
$item->cssClass = 'minwidth200';
$item->fieldAttr['placeholder'] = '700100';
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT');
$item->defaultFieldValue = '';
$item->cssClass = 'minwidth200';
$item->fieldAttr['placeholder'] = '700200';
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_BUY');
$item->defaultFieldValue = '';
$item->cssClass = 'minwidth200';
$item->fieldAttr['placeholder'] = '400000';
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA');
$item->defaultFieldValue = '';
$item->cssClass = 'minwidth200';
$item->fieldAttr['placeholder'] = '400100';
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT');
$item->defaultFieldValue = '';
$item->cssClass = 'minwidth200';
$item->fieldAttr['placeholder'] = '400200';
/*
* Actions
*/

View file

@ -528,18 +528,18 @@ class Datanorm extends CommonObject
$sql .= " WHERE (article_number LIKE '" . $this->db->escape($article_number) . "%'";
$sql .= " OR ean = '" . $this->db->escape($article_number) . "'";
$sql .= " OR manufacturer_ref LIKE '" . $this->db->escape($article_number) . "%')";
if ($fk_soc > 0 && !$searchAll) {
$sql .= " AND fk_soc = " . (int) $fk_soc;
} elseif ($fk_soc > 0 && $searchAll) {
// Order by matching supplier first
$sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, article_number";
}
$sql .= " AND active = 1";
$sql .= " AND entity = " . (int) $conf->entity;
if ($fk_soc == 0 || !$searchAll) {
if ($fk_soc > 0 && !$searchAll) {
$sql .= " AND fk_soc = " . (int) $fk_soc;
}
// ORDER BY clause
if ($fk_soc > 0 && $searchAll) {
// Order by matching supplier first
$sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, article_number";
} else {
$sql .= " ORDER BY article_number";
}

View file

@ -114,11 +114,26 @@ class ImportLine
*/
public $ean;
/**
* @var float Copper surcharge per unit (Kupferzuschlag)
*/
public $copper_surcharge;
/**
* @var float Basis quantity for copper surcharge
*/
public $copper_surcharge_basis_qty;
/**
* @var int Assigned Dolibarr product ID
*/
public $fk_product;
/**
* @var int Assigned Datanorm article ID
*/
public $fk_datanorm;
/**
* @var string Match method description
*/
@ -157,7 +172,8 @@ class ImportLine
$sql = "INSERT INTO " . MAIN_DB_PREFIX . $this->table_element . " (";
$sql .= "fk_import, line_id, supplier_ref, product_name, description,";
$sql .= "quantity, unit_code, unit_price, unit_price_raw, basis_quantity, basis_quantity_unit,";
$sql .= "line_total, tax_percent, ean, fk_product, match_method, date_creation";
$sql .= "line_total, tax_percent, ean, copper_surcharge, copper_surcharge_basis_qty,";
$sql .= "fk_product, match_method, date_creation";
$sql .= ") VALUES (";
$sql .= ((int) $this->fk_import) . ",";
$sql .= "'" . $this->db->escape($this->line_id) . "',";
@ -173,6 +189,8 @@ class ImportLine
$sql .= ((float) $this->line_total) . ",";
$sql .= ((float) $this->tax_percent) . ",";
$sql .= "'" . $this->db->escape($this->ean) . "',";
$sql .= ($this->copper_surcharge !== null ? ((float) $this->copper_surcharge) : "NULL") . ",";
$sql .= ($this->copper_surcharge_basis_qty !== null ? ((float) $this->copper_surcharge_basis_qty) : "NULL") . ",";
$sql .= ($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL") . ",";
$sql .= "'" . $this->db->escape($this->match_method) . "',";
$sql .= "'" . $this->db->idate($this->date_creation) . "'";
@ -199,7 +217,8 @@ class ImportLine
{
$sql = "SELECT rowid, fk_import, line_id, supplier_ref, product_name, description,";
$sql .= " quantity, unit_code, unit_price, unit_price_raw, basis_quantity, basis_quantity_unit,";
$sql .= " line_total, tax_percent, ean, fk_product, match_method, date_creation";
$sql .= " line_total, tax_percent, ean, copper_surcharge, copper_surcharge_basis_qty,";
$sql .= " fk_product, fk_datanorm, match_method, date_creation";
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
$sql .= " WHERE rowid = " . ((int) $id);
@ -223,7 +242,10 @@ class ImportLine
$this->line_total = $obj->line_total;
$this->tax_percent = $obj->tax_percent;
$this->ean = $obj->ean;
$this->copper_surcharge = $obj->copper_surcharge;
$this->copper_surcharge_basis_qty = $obj->copper_surcharge_basis_qty;
$this->fk_product = $obj->fk_product;
$this->fk_datanorm = $obj->fk_datanorm;
$this->match_method = $obj->match_method;
$this->date_creation = $this->db->jdate($obj->date_creation);
return 1;
@ -244,6 +266,7 @@ class ImportLine
{
$sql = "UPDATE " . MAIN_DB_PREFIX . $this->table_element . " SET";
$sql .= " fk_product = " . ($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL") . ",";
$sql .= " fk_datanorm = " . ($this->fk_datanorm > 0 ? ((int) $this->fk_datanorm) : "NULL") . ",";
$sql .= " match_method = '" . $this->db->escape($this->match_method) . "'";
$sql .= " WHERE rowid = " . ((int) $this->id);
@ -374,4 +397,38 @@ class ImportLine
$this->match_method = $match_method;
return $this->update($user);
}
/**
* Set Datanorm reference for this line
*
* @param int $fk_datanorm Datanorm article ID
* @param User $user User making the change
* @return int >0 if OK, <0 if KO
*/
public function setDatanorm($fk_datanorm, $user)
{
$this->fk_datanorm = $fk_datanorm;
$this->match_method = 'datanorm_assigned';
return $this->update($user);
}
/**
* Count lines with Datanorm assignment
*
* @param int $fk_import Import ID
* @return int Number of lines with Datanorm
*/
public function countLinesWithDatanorm($fk_import)
{
$sql = "SELECT COUNT(*) as cnt FROM " . MAIN_DB_PREFIX . $this->table_element;
$sql .= " WHERE fk_import = " . ((int) $fk_import);
$sql .= " AND fk_datanorm IS NOT NULL AND fk_datanorm > 0";
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
return (int) $obj->cnt;
}
return 0;
}
}

View file

@ -606,6 +606,15 @@ class ZugferdImport extends CommonObject
$line->tax_percent = $line_data['tax_percent'];
$line->ean = $line_data['product']['global_id'];
// Copper surcharge (Kupferzuschlag) from ZUGFeRD - always set (0 if not present)
if (isset($line_data['copper_surcharge']) && $line_data['copper_surcharge'] > 0) {
$line->copper_surcharge = $line_data['copper_surcharge'];
$line->copper_surcharge_basis_qty = isset($line_data['copper_surcharge_basis_qty']) ? $line_data['copper_surcharge_basis_qty'] : $line->basis_quantity;
} else {
$line->copper_surcharge = 0;
$line->copper_surcharge_basis_qty = $line->basis_quantity;
}
// Try to match product
$fk_product = 0;
$match_method = '';

View file

@ -638,6 +638,28 @@ class modImportZugferd extends DolibarrModules
'isModEnabled("importzugferd")' // enabled condition
);
// Add extrafield for product group (Warengruppe) on supplier prices
$extrafields->addExtraField(
'warengruppe', // attribute code
'Warengruppe', // label (translation key)
'varchar', // type
125, // position
32, // size
'product_fournisseur_price', // element type
0, // unique
0, // required
'', // default value
'', // param
1, // always editable
'', // permission
1, // list (show in list)
0, // printable
'', // totalizable
'', // langfile
'importzugferd@importzugferd', // module
'isModEnabled("importzugferd")' // enabled condition
);
// Add extrafield for copper content (Kupfergehalt) on product - kg per km for cables
$extrafields->addExtraField(
'kupfergehalt', // attribute code

View file

@ -93,6 +93,74 @@ $message = '';
* 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'])) {
@ -436,6 +504,14 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
// 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) {
@ -451,8 +527,27 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
// Selling price with markup
$sellingPrice = $purchasePrice * (1 + $markup / 100);
// 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 basis quantity differs
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 to per-unit if basis quantity differs
if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) {
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty;
} elseif (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
$copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->basis_quantity;
}
}
// Selling price with markup: (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;
@ -490,7 +585,30 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
$supplierEanType = 2; // EAN13
}
// Add supplier price entry with EAN
// Prepare extrafields for supplier price
$supplierPriceExtrafields = array();
// Kupferzuschlag (metal surcharge) - händlerspezifisch
// Priorität: 1. Datanorm, 2. ZUGFeRD Import Line
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$supplierPriceExtrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge;
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
// Kupferzuschlag aus ZUGFeRD-Rechnung
$supplierPriceExtrafields['options_kupferzuschlag'] = $lineObj->copper_surcharge;
}
// Preiseinheit - händlerspezifisch
// Priorität: 1. Datanorm, 2. ZUGFeRD basis_quantity
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
$supplierPriceExtrafields['options_preiseinheit'] = $datanorm->price_unit;
} elseif (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
// Preiseinheit aus ZUGFeRD-Rechnung (z.B. 100 für "pro 100 Meter")
$supplierPriceExtrafields['options_preiseinheit'] = $lineObj->basis_quantity;
}
// Warengruppe aus Datanorm
if (!empty($datanorm->product_group)) {
$supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group;
}
// Add supplier price entry with EAN and extrafields
$res = $prodfourn->update_buyprice(
1, // Quantity
$purchasePrice, // Price
@ -506,12 +624,49 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
0, // No price minimum
0, // Delivery delay
0, // Reputation
array(), // Extra fields
0, // Charges array
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)
$supplierEanType, // Barcode type (EAN13)
$supplierPriceExtrafields // Extra fields (kupferzuschlag, preiseinheit)
);
// 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);
if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) {
// Insert extrafields record
$kupferzuschlag = !empty($supplierPriceExtrafields['options_kupferzuschlag']) ? (float)$supplierPriceExtrafields['options_kupferzuschlag'] : 'NULL';
$preiseinheit = !empty($supplierPriceExtrafields['options_preiseinheit']) ? (int)$supplierPriceExtrafields['options_preiseinheit'] : 1;
$warengruppe = !empty($supplierPriceExtrafields['options_warengruppe']) ? "'".$db->escape($supplierPriceExtrafields['options_warengruppe'])."'" : 'NULL';
$sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sqlInsertExtra .= " (fk_object, kupferzuschlag, preiseinheit, warengruppe) VALUES (";
$sqlInsertExtra .= (int)$priceId.", ";
$sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
$sqlInsertExtra .= $preiseinheit.", ";
$sqlInsertExtra .= $warengruppe.")";
$db->query($sqlInsertExtra);
}
}
// Create product mapping for future imports
$mapping = new ProductMapping($db);
$mapping->fk_soc = $import->fk_soc;
@ -544,6 +699,80 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
$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);
@ -594,6 +823,23 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
$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;
@ -651,7 +897,16 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
}
$newproduct->description = $datanorm->getFullDescription();
$sellingPrice = $purchasePrice * (1 + $markup / 100);
// 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;
@ -683,12 +938,83 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
$supplierEanType = 2;
}
// Prepare extrafields for supplier price
$supplierPriceExtrafields = array();
// Kupferzuschlag - Priorität: 1. Datanorm, 2. ZUGFeRD
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$supplierPriceExtrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge;
} elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
$supplierPriceExtrafields['options_kupferzuschlag'] = $lineObj->copper_surcharge;
}
// Preiseinheit - Priorität: 1. Datanorm, 2. ZUGFeRD basis_quantity
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
$supplierPriceExtrafields['options_preiseinheit'] = $datanorm->price_unit;
} 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;
}
$prodfourn->update_buyprice(
1, $purchasePrice, $user, 'HT', $supplier, 0,
$datanorm->article_number, $lineObj->tax_percent ?: 19,
0, 0, 0, 0, 0, 0, array(), 0, $supplierEan, $supplierEanType
1, // Quantity
$purchasePrice, // Price
$user,
'HT', // Price base
$supplier, // Supplier
0, // Availability
$datanorm->article_number, // Supplier ref
$lineObj->tax_percent ?: 19, // VAT
0, // Charges
0, // Remise
0, // Remise percentage
0, // No price minimum
0, // Delivery delay
0, // Reputation
array(), // 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);
if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) {
// Insert extrafields record
$kupferzuschlag = !empty($supplierPriceExtrafields['options_kupferzuschlag']) ? (float)$supplierPriceExtrafields['options_kupferzuschlag'] : 'NULL';
$preiseinheit = !empty($supplierPriceExtrafields['options_preiseinheit']) ? (int)$supplierPriceExtrafields['options_preiseinheit'] : 1;
$warengruppe = !empty($supplierPriceExtrafields['options_warengruppe']) ? "'".$db->escape($supplierPriceExtrafields['options_warengruppe'])."'" : 'NULL';
$sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sqlInsertExtra .= " (fk_object, kupferzuschlag, preiseinheit, warengruppe) VALUES (";
$sqlInsertExtra .= (int)$priceId.", ";
$sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
$sqlInsertExtra .= $preiseinheit.", ";
$sqlInsertExtra .= $warengruppe.")";
$db->query($sqlInsertExtra);
}
}
// Create product mapping
$mapping = new ProductMapping($db);
$mapping->fk_soc = $import->fk_soc;
@ -733,6 +1059,138 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
$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
$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
$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';
}
}
// 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
);
}
}
}
$action = 'edit';
}
// Create supplier invoice
if ($action == 'createinvoice' && $id > 0) {
$import->fetch($id);
@ -756,13 +1214,21 @@ if ($action == 'createinvoice' && $id > 0) {
$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.')';
$invoice->cond_reglement_id = 1;
// 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);
@ -1160,22 +1626,22 @@ if ($action == 'edit' && $import->id > 0) {
foreach ($lines as $line) {
$hasProduct = ($line->fk_product > 0);
$rowClass = $hasProduct ? 'oddeven opacitymedium' : 'oddeven';
$rowStyle = $hasProduct ? 'background-color: #dff0d8;' : ''; // Green for matched products
print '<tr class="'.$rowClass.'">';
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 class="opacitymedium">EAN: '.$line->ean.'</span>';
print '<br><span style="color: #666;">EAN: '.$line->ean.'</span>';
}
print '</td>';
print '<td class="right">'.price2num($line->quantity, 'MS').' '.zugferdGetUnitLabel($line->unit_code).'</td>';
print '<td class="right">';
print price($line->unit_price);
if (!empty($line->basis_quantity) && $line->basis_quantity != 1) {
print '<br><span class="opacitymedium">('.price($line->unit_price_raw).'/'.price2num($line->basis_quantity, 'MS').zugferdGetUnitLabel($line->basis_quantity_unit).')</span>';
print '<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>';
@ -1316,10 +1782,20 @@ if ($action == 'edit' && $import->id > 0) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=createfromdatanorm&line_id='.$line->id.'&id='.$import->id.'&token='.newToken().'" class="button buttongen margintoponlyshort" title="'.$langs->trans('CreateFromDatanormHelp').'">';
print '<i class="fas fa-database paddingright"></i>'.$langs->trans('CreateFromDatanorm');
print '</a>';
print '<br><span class="opacitymedium small">';
print dol_trunc($datanormArticle['short_text1'], 40);
print ' - '.price($datanormArticle['price']);
print '</span>';
// 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>';
}
}
}
@ -1392,6 +1868,127 @@ if ($action == 'edit' && $import->id > 0) {
print '</table>';
print '</div>';
// 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>';
}
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 ' &nbsp; ';
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;">';
@ -1410,10 +2007,17 @@ if ($action == 'edit' && $import->id > 0) {
print ' &nbsp; ';
}
// Button to create all products from Datanorm
if ($missingProducts > 0 && $import->fk_soc > 0) {
// 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('CreateAllFromDatanorm');
print '<i class="fas fa-database paddingright"></i>'.$langs->trans('AssignAllFromDatanorm');
print '</a>';
print ' &nbsp; ';
// "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 ' &nbsp; ';
}
@ -1433,6 +2037,169 @@ if ($action == 'edit' && $import->id > 0) {
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()">&times;</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", "'.$_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();

View file

@ -229,6 +229,22 @@ IMPORTZUGFERD_DATANORM_MARKUP = Preisaufschlag (%)
IMPORTZUGFERD_DATANORM_MARKUPTooltip = Prozentualer Aufschlag auf den Datanorm-Einkaufspreis für den Verkaufspreis
IMPORTZUGFERD_DATANORM_SEARCH_ALL = In allen Lieferanten-Katalogen suchen
IMPORTZUGFERD_DATANORM_SEARCH_ALLTooltip = Bei Aktivierung wird nicht nur im Katalog des aktuellen Lieferanten gesucht, sondern in allen Datanorm-Katalogen
# Accounting Settings (Standard-Konten für neue Produkte)
AccountingSettings = Buchungskonten für neue Produkte
IMPORTZUGFERD_ACCOUNTING_CODE_SELL = Erlöskonto (Verkauf)
IMPORTZUGFERD_ACCOUNTING_CODE_SELLTooltip = Standard-Erlöskonto für neue Produkte aus Datanorm (z.B. 700000)
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA = Erlöskonto (innergemeinschaftlich)
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRATooltip = Erlöskonto für innergemeinschaftliche Lieferungen (z.B. 700100)
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT = Erlöskonto (Export)
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORTTooltip = Erlöskonto für Exporte außerhalb EU (z.B. 700200)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY = Aufwandskonto (Einkauf)
IMPORTZUGFERD_ACCOUNTING_CODE_BUYTooltip = Standard-Aufwandskonto für neue Produkte aus Datanorm (z.B. 400000)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA = Aufwandskonto (innergemeinschaftlich)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRATooltip = Aufwandskonto für innergemeinschaftliche Erwerbe (z.B. 400100)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT = Aufwandskonto (Import)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORTTooltip = Aufwandskonto für Importe von außerhalb EU (z.B. 400200)
UploadDatanorm = Datanorm hochladen
DatanormFiles = Datanorm Dateien
DatanormFileHelp = DATANORM.001, DATANORM.WRG oder XML-Dateien (Datanorm 4.0/5.0)
@ -259,6 +275,29 @@ DatanormBatchCreated = %s Produkte aus Datanorm erstellt
DatanormBatchAssigned = %s vorhandene Produkte zugeordnet
DatanormBatchErrors = %s Produkte konnten nicht erstellt werden
DatanormBatchNoMatches = Keine passenden Datanorm-Artikel gefunden
PreviewDatanormMatches = Datanorm Vorschau
DatanormPreview = Datanorm Vorschau - Gefundene Übereinstimmungen
Matches = Treffer
InvoiceProductName = Rechnung Bezeichnung
DatanormProductName = Datanorm Bezeichnung
InvoicePrice = Rechnungspreis
DatanormPrice = Datanorm EK
PurchasePrice = Einkaufspreis
SellingPrice = Verkaufspreis
ProductAlreadyExists = Produkt existiert bereits
Assign = Zuordnen
Create = Anlegen
ToCreate = anzulegen
ToAssign = zuzuordnen
ConfirmAndCreateProducts = Bestätigen und Produkte anlegen
CreateAllWithoutPreview = Direkt anlegen
ConfirmCreateAllWithoutPreview = Alle passenden Produkte aus Datanorm anlegen (ohne Vorschau)?
AssignAllFromDatanorm = Alle zuordnen
ConfirmAssignAllFromDatanorm = Alle vorhandenen Produkte aus Datanorm zuordnen?
NoProductsToAssign = Keine vorhandenen Produkte zum Zuordnen gefunden
ProductsAssignedFromDatanorm = %s Produkte wurden aus Datanorm zugeordnet
DatanormMatchesFoundNotAssigned = %s Datanorm-Treffer gefunden (Produkte können mit "Direkt anlegen" erstellt werden)
ShowRawDatanorm = Rohdaten anzeigen
#
# Scheduling
@ -414,6 +453,8 @@ Kupferzuschlag = Kupferzuschlag
KupferzuschlagHelp = Metallzuschlag pro Einheit (wird aus Rechnungen extrahiert)
Preiseinheit = Preiseinheit
PreiseinheitHelp = Anzahl Einheiten pro Preis (z.B. 100 = Preis pro 100 Stück)
Warengruppe = Warengruppe
WarengruppeHelp = Produktgruppe aus Datanorm (für Rabattsteuerung und Kategorisierung)
MetalSurchargeDetected = Metallzuschlag erkannt
MetalSurchargeUpdated = Kupferzuschlag aktualisiert auf %s €/Einheit
AddAllWithDifferences = Alle mit Unterschieden hinzufügen

View file

@ -229,6 +229,22 @@ IMPORTZUGFERD_DATANORM_MARKUP = Price Markup (%)
IMPORTZUGFERD_DATANORM_MARKUPTooltip = Percentage markup on Datanorm purchase price for selling price
IMPORTZUGFERD_DATANORM_SEARCH_ALL = Search in all supplier catalogs
IMPORTZUGFERD_DATANORM_SEARCH_ALLTooltip = When enabled, search all Datanorm catalogs, not just the current supplier
# Accounting Settings (Default accounts for new products)
AccountingSettings = Accounting Codes for New Products
IMPORTZUGFERD_ACCOUNTING_CODE_SELL = Sales Account (Domestic)
IMPORTZUGFERD_ACCOUNTING_CODE_SELLTooltip = Default sales account for new products from Datanorm (e.g. 700000)
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA = Sales Account (Intra-EU)
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRATooltip = Sales account for intra-community deliveries (e.g. 700100)
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT = Sales Account (Export)
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORTTooltip = Sales account for exports outside EU (e.g. 700200)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY = Purchase Account (Domestic)
IMPORTZUGFERD_ACCOUNTING_CODE_BUYTooltip = Default purchase account for new products from Datanorm (e.g. 400000)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA = Purchase Account (Intra-EU)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRATooltip = Purchase account for intra-community acquisitions (e.g. 400100)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT = Purchase Account (Import)
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORTTooltip = Purchase account for imports from outside EU (e.g. 400200)
UploadDatanorm = Upload Datanorm
DatanormFiles = Datanorm Files
DatanormFileHelp = DATANORM.001, DATANORM.WRG or XML files (Datanorm 4.0/5.0)
@ -259,6 +275,29 @@ DatanormBatchCreated = %s products created from Datanorm
DatanormBatchAssigned = %s existing products assigned
DatanormBatchErrors = %s products could not be created
DatanormBatchNoMatches = No matching Datanorm articles found
PreviewDatanormMatches = Datanorm Preview
DatanormPreview = Datanorm Preview - Found Matches
Matches = matches
InvoiceProductName = Invoice Product Name
DatanormProductName = Datanorm Product Name
InvoicePrice = Invoice Price
DatanormPrice = Datanorm Price
PurchasePrice = Purchase Price
SellingPrice = Selling Price
ProductAlreadyExists = Product already exists
Assign = Assign
Create = Create
ToCreate = to create
ToAssign = to assign
ConfirmAndCreateProducts = Confirm and Create Products
CreateAllWithoutPreview = Create directly
ConfirmCreateAllWithoutPreview = Create all matching products from Datanorm (without preview)?
AssignAllFromDatanorm = Assign all
ConfirmAssignAllFromDatanorm = Assign all existing products from Datanorm?
NoProductsToAssign = No existing products found to assign
ProductsAssignedFromDatanorm = %s products have been assigned from Datanorm
DatanormMatchesFoundNotAssigned = %s Datanorm matches found (products can be created with "Create directly")
ShowRawDatanorm = Show raw data
#
# Scheduling
@ -352,6 +391,8 @@ Kupferzuschlag = Copper Surcharge
KupferzuschlagHelp = Metal surcharge per unit (extracted from invoices)
Preiseinheit = Price Unit
PreiseinheitHelp = Number of units per price (e.g. 100 = price per 100 pieces)
Warengruppe = Product Group
WarengruppeHelp = Product group from Datanorm (for discount control and categorization)
MetalSurchargeDetected = Metal surcharge detected
MetalSurchargeUpdated = Metal surcharge updated to %s €/unit

View file

@ -1,3 +1,10 @@
--
-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version.
--
-- Add copper surcharge fields to import_line table (v2.8)
ALTER TABLE llx_importzugferd_import_line ADD COLUMN copper_surcharge double(24,8) DEFAULT NULL AFTER ean;
ALTER TABLE llx_importzugferd_import_line ADD COLUMN copper_surcharge_basis_qty double(24,8) DEFAULT NULL AFTER copper_surcharge;
-- Add fk_datanorm field to import_line table (v2.9)
ALTER TABLE llx_importzugferd_import_line ADD COLUMN fk_datanorm integer DEFAULT NULL AFTER fk_product;

View file

@ -28,7 +28,10 @@ CREATE TABLE llx_importzugferd_import_line (
line_total double(24,8) DEFAULT 0, -- Zeilensumme netto
tax_percent double(24,8) DEFAULT 0, -- MwSt-Satz
ean varchar(20), -- EAN/GTIN falls vorhanden
copper_surcharge double(24,8) DEFAULT NULL, -- Kupferzuschlag pro Einheit
copper_surcharge_basis_qty double(24,8) DEFAULT NULL, -- Basismenge für Kupferzuschlag
fk_product integer, -- Zugeordnetes Dolibarr-Produkt
fk_datanorm integer, -- Zugeordneter Datanorm-Artikel
match_method varchar(50), -- Wie wurde Produkt gefunden
date_creation datetime NOT NULL,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP