Fehler beseitigt, Massenimport eingefügt Datenorm korrigiert.
This commit is contained in:
parent
e420698a58
commit
244e41c353
18 changed files with 21093 additions and 133 deletions
|
|
@ -5,7 +5,12 @@
|
|||
"Bash(python3:*)",
|
||||
"Bash(xmllint:*)",
|
||||
"Bash(php -r:*)",
|
||||
"Bash(chmod:*)"
|
||||
"Bash(chmod:*)",
|
||||
"Bash(cut:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(mysql:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(php:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
18971
2026-02-04 - Zugferd Rechnung - Sonepar - 9010548449 - 3581,33 EUR.pdf
Executable file
18971
2026-02-04 - Zugferd Rechnung - Sonepar - 9010548449 - 3581,33 EUR.pdf
Executable file
File diff suppressed because one or more lines are too long
22
ChangeLog.md
22
ChangeLog.md
|
|
@ -1,5 +1,27 @@
|
|||
# CHANGELOG MODULE IMPORTZUGFERD FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
|
||||
|
||||
## 2.1
|
||||
|
||||
### Bugfixes
|
||||
- Rechnungsimport: Preise wurden falsch als Brutto (TTC) statt Netto (HT) behandelt - korrigierte Parameterreihenfolge in addline()
|
||||
- Datanorm Massenaktualisierung: Lieferantenauswahl ging nach Aktionen verloren - Redirects hinzugefuegt
|
||||
- Datanorm Massenaktualisierung: "Alle Aenderungen uebernehmen" Button war nicht sichtbar ohne Suchergebnisse
|
||||
- Datanorm Massenaktualisierung: Filter-Auswahl (Preis/Beschreibung/Bezeichnung) wurde bei "Alle hinzufuegen" ignoriert
|
||||
- ProductFournisseur::update_buyprice erwartet Societe-Objekt, nicht Integer-ID
|
||||
|
||||
### Verbesserungen
|
||||
- Bestaetungsdialog fuer Massenaktionen verwendet jetzt Dolibarr jQuery UI Dialog statt JavaScript confirm()
|
||||
- Manuelles Metallzuschlag-Eingabefeld entfernt (nicht mehr benoetigt - Kupferzuschlag wird aus ZUGFeRD XML extrahiert)
|
||||
- Ausstehende Aenderungen werden immer angezeigt wenn vorhanden, unabhaengig von Suchergebnissen
|
||||
|
||||
## 2.0
|
||||
|
||||
- Datanorm 4.0/5.0 Katalog-Import
|
||||
- Kupferzuschlag-Extraktion aus ZUGFeRD XML (AllowanceCharge)
|
||||
- Automatischer Preisvergleich zwischen Datanorm und aktuellen Einkaufspreisen
|
||||
- Massenaktualisierung von Produktpreisen und Beschreibungen
|
||||
- Aenderungsprotokoll fuer Preisanpassungen
|
||||
|
||||
## 1.0
|
||||
|
||||
Initial version
|
||||
|
|
|
|||
|
|
@ -264,8 +264,15 @@ class ActionsImportZugferd
|
|||
public function processLineItems($lines, $supplier_id)
|
||||
{
|
||||
$processed = array();
|
||||
$last_product_index = -1;
|
||||
|
||||
foreach ($lines as $idx => $line) {
|
||||
// Check if this is a metal surcharge line
|
||||
$is_surcharge = $this->isMetalSurchargeLine($line);
|
||||
|
||||
// Get copper surcharge directly from parsed line data (if available)
|
||||
$copper_surcharge_per_unit = isset($line['copper_surcharge_per_unit']) ? $line['copper_surcharge_per_unit'] : null;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$processed_line = array(
|
||||
'line_id' => $line['line_id'],
|
||||
'supplier_ref' => $line['product']['seller_id'],
|
||||
|
|
@ -285,10 +292,14 @@ class ActionsImportZugferd
|
|||
'product_label' => '',
|
||||
'match_method' => '',
|
||||
'needs_creation' => false,
|
||||
'is_metal_surcharge' => $is_surcharge,
|
||||
'metal_surcharge' => $copper_surcharge_per_unit ?: 0, // From parsed XML or will be filled from surcharge lines
|
||||
'copper_surcharge_raw' => isset($line['copper_surcharge']) ? $line['copper_surcharge'] : null,
|
||||
'copper_surcharge_basis_qty' => isset($line['copper_surcharge_basis_qty']) ? $line['copper_surcharge_basis_qty'] : null,
|
||||
);
|
||||
|
||||
// Try to find product
|
||||
if ($supplier_id > 0) {
|
||||
if ($supplier_id > 0 && !$is_surcharge) {
|
||||
$match = $this->mapping->findProduct($supplier_id, $line['product']);
|
||||
if ($match['fk_product'] > 0) {
|
||||
$processed_line['fk_product'] = $match['fk_product'];
|
||||
|
|
@ -303,11 +314,43 @@ class ActionsImportZugferd
|
|||
} else {
|
||||
$processed_line['needs_creation'] = true;
|
||||
}
|
||||
} else {
|
||||
} elseif (!$is_surcharge) {
|
||||
$processed_line['needs_creation'] = true;
|
||||
}
|
||||
|
||||
$processed[] = $processed_line;
|
||||
$current_index = count($processed) - 1;
|
||||
|
||||
// If this is a metal surcharge line, associate it with the previous product
|
||||
// Only use this fallback if the product line doesn't already have copper_surcharge from XML
|
||||
if ($is_surcharge && $last_product_index >= 0) {
|
||||
// Only apply if the previous product doesn't already have a copper surcharge from XML
|
||||
if (empty($processed[$last_product_index]['metal_surcharge'])) {
|
||||
// Calculate surcharge per unit based on the product's quantity
|
||||
$product_qty = $processed[$last_product_index]['quantity'];
|
||||
if ($product_qty > 0) {
|
||||
$surcharge_per_unit = $line['line_total'] / $product_qty;
|
||||
$processed[$last_product_index]['metal_surcharge'] = $surcharge_per_unit;
|
||||
|
||||
dol_syslog("Metal surcharge from separate line: " . $line['line_total'] . " EUR for " . $product_qty . " units = " . $surcharge_per_unit . " EUR/unit", LOG_INFO);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy product info to surcharge line for reference
|
||||
$processed_line['fk_product'] = $processed[$last_product_index]['fk_product'];
|
||||
$processed_line['associated_product_ref'] = $processed[$last_product_index]['product_ref'];
|
||||
$processed[$current_index] = $processed_line;
|
||||
}
|
||||
|
||||
// Log if copper surcharge was extracted from XML
|
||||
if ($copper_surcharge_per_unit > 0) {
|
||||
dol_syslog("Copper surcharge from XML: " . $copper_surcharge_per_unit . " EUR/unit for " . $line['product']['name'], LOG_INFO);
|
||||
}
|
||||
|
||||
// Remember the last non-surcharge product line
|
||||
if (!$is_surcharge && $processed_line['fk_product'] > 0) {
|
||||
$last_product_index = $current_index;
|
||||
}
|
||||
}
|
||||
|
||||
return $processed;
|
||||
|
|
@ -577,9 +620,125 @@ class ActionsImportZugferd
|
|||
$this->updateSupplierPriceBarcode($invoice->socid, $line['fk_product'], $line['ean'], $line['supplier_ref']);
|
||||
}
|
||||
|
||||
// Check if this line has a metal surcharge associated and update extrafield
|
||||
if ($line['fk_product'] > 0 && !empty($line['metal_surcharge']) && $line['metal_surcharge'] > 0) {
|
||||
$this->updateSupplierPriceMetalSurcharge(
|
||||
$invoice->socid,
|
||||
$line['fk_product'],
|
||||
$line['metal_surcharge'],
|
||||
$line['supplier_ref']
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line is a metal surcharge line
|
||||
*
|
||||
* @param array $line Line data from invoice
|
||||
* @return bool
|
||||
*/
|
||||
public function isMetalSurchargeLine($line)
|
||||
{
|
||||
$name = strtolower($line['product']['name'] ?? '');
|
||||
$description = strtolower($line['product']['description'] ?? '');
|
||||
$text = $name . ' ' . $description;
|
||||
|
||||
// Keywords that indicate metal surcharge
|
||||
$keywords = array(
|
||||
'metallzuschlag',
|
||||
'kupferzuschlag',
|
||||
'cu-zuschlag',
|
||||
'cuzuschlag',
|
||||
'metallnotierung',
|
||||
'kupfernotierung',
|
||||
'metal surcharge',
|
||||
'copper surcharge',
|
||||
'metallaufschlag',
|
||||
'kupferaufschlag',
|
||||
'mez ', // MEZ = Metallzuschlag (with space to avoid false positives)
|
||||
' mez',
|
||||
);
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
if (strpos($text, $keyword) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update metal surcharge extrafield on supplier price
|
||||
*
|
||||
* @param int $supplier_id Supplier ID
|
||||
* @param int $product_id Product ID
|
||||
* @param float $surcharge Metal surcharge amount per unit
|
||||
* @param string $ref_fourn Supplier reference
|
||||
* @return int >0 if updated, 0 if no update, <0 if error
|
||||
*/
|
||||
public function updateSupplierPriceMetalSurcharge($supplier_id, $product_id, $surcharge, $ref_fourn = '')
|
||||
{
|
||||
global $conf;
|
||||
|
||||
if ($surcharge <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Find supplier price record
|
||||
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sql .= " WHERE fk_soc = " . (int) $supplier_id;
|
||||
$sql .= " AND fk_product = " . (int) $product_id;
|
||||
$sql .= " AND entity IN (" . getEntity('product') . ")";
|
||||
if (!empty($ref_fourn)) {
|
||||
$sql .= " AND ref_fourn = '" . $this->db->escape($ref_fourn) . "'";
|
||||
}
|
||||
$sql .= " ORDER BY rowid DESC LIMIT 1";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$price_id = $obj->rowid;
|
||||
|
||||
// Check if extrafield table exists
|
||||
$sql_check = "SHOW TABLES LIKE '" . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields'";
|
||||
$res_check = $this->db->query($sql_check);
|
||||
if (!$res_check || $this->db->num_rows($res_check) == 0) {
|
||||
dol_syslog("Extrafield table does not exist, skipping metal surcharge update", LOG_WARNING);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if record exists in extrafields
|
||||
$sql_exists = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
|
||||
$sql_exists .= " WHERE fk_object = " . (int) $price_id;
|
||||
|
||||
$res_exists = $this->db->query($sql_exists);
|
||||
if ($res_exists && $this->db->num_rows($res_exists) > 0) {
|
||||
// Update existing record
|
||||
$sql_update = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
|
||||
$sql_update .= " SET kupferzuschlag = " . (float) $surcharge;
|
||||
$sql_update .= " WHERE fk_object = " . (int) $price_id;
|
||||
} else {
|
||||
// Insert new record
|
||||
$sql_update = "INSERT INTO " . MAIN_DB_PREFIX . "product_fournisseur_price_extrafields";
|
||||
$sql_update .= " (fk_object, kupferzuschlag) VALUES (" . (int) $price_id . ", " . (float) $surcharge . ")";
|
||||
}
|
||||
|
||||
$res = $this->db->query($sql_update);
|
||||
if ($res) {
|
||||
dol_syslog("Updated metal surcharge for product " . $product_id . " supplier " . $supplier_id . " to " . $surcharge, LOG_INFO);
|
||||
return 1;
|
||||
} else {
|
||||
dol_syslog("Error updating metal surcharge: " . $this->db->lasterror(), LOG_ERR);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // No supplier price record found
|
||||
}
|
||||
|
||||
/**
|
||||
* Update barcode in supplier price record
|
||||
*
|
||||
|
|
|
|||
|
|
@ -87,10 +87,30 @@ class Datanorm extends CommonObject
|
|||
public $price = 0;
|
||||
|
||||
/**
|
||||
* @var int Price unit (pieces per price)
|
||||
* @var int Price unit (actual quantity: 1, 10, 100, or 1000)
|
||||
*/
|
||||
public $price_unit = 1;
|
||||
|
||||
/**
|
||||
* @var int Price unit code (original Datanorm PE code: 0, 1, 2, or 3)
|
||||
*/
|
||||
public $price_unit_code = 0;
|
||||
|
||||
/**
|
||||
* @var int Price type (1=gross/Brutto, 2=net/Netto)
|
||||
*/
|
||||
public $price_type = 1;
|
||||
|
||||
/**
|
||||
* @var int VPE - Verpackungseinheit (packaging quantity from B-record)
|
||||
*/
|
||||
public $vpe;
|
||||
|
||||
/**
|
||||
* @var float Metal surcharge (Metallzuschlag/Kupferzuschlag) for cables
|
||||
*/
|
||||
public $metal_surcharge = 0;
|
||||
|
||||
/**
|
||||
* @var string Discount group
|
||||
*/
|
||||
|
|
@ -126,6 +146,11 @@ class Datanorm extends CommonObject
|
|||
*/
|
||||
public $datanorm_version;
|
||||
|
||||
/**
|
||||
* @var string Action code (N=New, A=Update, L=Delete)
|
||||
*/
|
||||
public $action_code = 'N';
|
||||
|
||||
/**
|
||||
* @var string Import date
|
||||
*/
|
||||
|
|
@ -182,12 +207,17 @@ class Datanorm extends CommonObject
|
|||
|
||||
$this->fk_user_creat = $user->id;
|
||||
|
||||
// Set active=0 if action_code is L (deleted article)
|
||||
if ($this->action_code === 'L') {
|
||||
$this->active = 0;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO " . MAIN_DB_PREFIX . $this->table_element . " (";
|
||||
$sql .= "fk_soc, article_number, short_text1, short_text2, long_text,";
|
||||
$sql .= "ean, manufacturer_ref, manufacturer_name, unit_code,";
|
||||
$sql .= "price, price_unit, discount_group, product_group,";
|
||||
$sql .= "price, price_unit, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group,";
|
||||
$sql .= "alt_unit, alt_unit_factor, weight, matchcode,";
|
||||
$sql .= "datanorm_version, import_date, active, date_creation, fk_user_creat, entity";
|
||||
$sql .= "datanorm_version, action_code, import_date, active, date_creation, fk_user_creat, entity";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= (int) $this->fk_soc . ",";
|
||||
$sql .= "'" . $this->db->escape($this->article_number) . "',";
|
||||
|
|
@ -200,6 +230,10 @@ class Datanorm extends CommonObject
|
|||
$sql .= "'" . $this->db->escape($this->unit_code) . "',";
|
||||
$sql .= (float) $this->price . ",";
|
||||
$sql .= (int) $this->price_unit . ",";
|
||||
$sql .= (int) $this->price_unit_code . ",";
|
||||
$sql .= (int) $this->price_type . ",";
|
||||
$sql .= (float) $this->metal_surcharge . ",";
|
||||
$sql .= ($this->vpe !== null ? (int) $this->vpe : 'NULL') . ",";
|
||||
$sql .= "'" . $this->db->escape($this->discount_group) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->product_group) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->alt_unit) . "',";
|
||||
|
|
@ -207,6 +241,7 @@ class Datanorm extends CommonObject
|
|||
$sql .= ($this->weight !== null ? (float) $this->weight : 'NULL') . ",";
|
||||
$sql .= "'" . $this->db->escape($this->matchcode) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->datanorm_version) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->action_code) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->db->idate($this->import_date)) . "',";
|
||||
$sql .= (int) $this->active . ",";
|
||||
$sql .= "'" . $this->db->escape($this->db->idate($this->date_creation)) . "',";
|
||||
|
|
@ -254,9 +289,9 @@ class Datanorm extends CommonObject
|
|||
{
|
||||
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2, long_text,";
|
||||
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
|
||||
$sql .= " price, price_unit, discount_group, product_group,";
|
||||
$sql .= " price, price_unit, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group,";
|
||||
$sql .= " alt_unit, alt_unit_factor, weight, matchcode,";
|
||||
$sql .= " datanorm_version, import_date, active, date_creation, tms, fk_user_creat, fk_user_modif, entity";
|
||||
$sql .= " datanorm_version, action_code, import_date, active, date_creation, tms, fk_user_creat, fk_user_modif, entity";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE rowid = " . (int) $id;
|
||||
|
||||
|
|
@ -292,9 +327,9 @@ class Datanorm extends CommonObject
|
|||
|
||||
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2, long_text,";
|
||||
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
|
||||
$sql .= " price, price_unit, discount_group, product_group,";
|
||||
$sql .= " price, price_unit, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group,";
|
||||
$sql .= " alt_unit, alt_unit_factor, weight, matchcode,";
|
||||
$sql .= " datanorm_version, import_date, active, date_creation, tms, fk_user_creat, fk_user_modif, entity";
|
||||
$sql .= " datanorm_version, action_code, import_date, active, date_creation, tms, fk_user_creat, fk_user_modif, entity";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE fk_soc = " . (int) $fk_soc;
|
||||
$sql .= " AND article_number = '" . $this->db->escape($article_number) . "'";
|
||||
|
|
@ -338,6 +373,10 @@ class Datanorm extends CommonObject
|
|||
$this->unit_code = $obj->unit_code;
|
||||
$this->price = $obj->price;
|
||||
$this->price_unit = $obj->price_unit;
|
||||
$this->price_unit_code = $obj->price_unit_code ?? 0;
|
||||
$this->price_type = $obj->price_type ?? 1;
|
||||
$this->metal_surcharge = $obj->metal_surcharge ?? 0;
|
||||
$this->vpe = $obj->vpe;
|
||||
$this->discount_group = $obj->discount_group;
|
||||
$this->product_group = $obj->product_group;
|
||||
$this->alt_unit = $obj->alt_unit;
|
||||
|
|
@ -345,6 +384,7 @@ class Datanorm extends CommonObject
|
|||
$this->weight = $obj->weight;
|
||||
$this->matchcode = $obj->matchcode;
|
||||
$this->datanorm_version = $obj->datanorm_version;
|
||||
$this->action_code = $obj->action_code ?? 'N';
|
||||
$this->import_date = $this->db->jdate($obj->import_date);
|
||||
$this->active = $obj->active;
|
||||
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||
|
|
@ -365,6 +405,11 @@ class Datanorm extends CommonObject
|
|||
$this->fk_user_modif = $user->id;
|
||||
$this->import_date = dol_now();
|
||||
|
||||
// Set active=0 if action_code is L (deleted article)
|
||||
if ($this->action_code === 'L') {
|
||||
$this->active = 0;
|
||||
}
|
||||
|
||||
$sql = "UPDATE " . MAIN_DB_PREFIX . $this->table_element . " SET";
|
||||
$sql .= " short_text1 = '" . $this->db->escape($this->short_text1) . "',";
|
||||
$sql .= " short_text2 = '" . $this->db->escape($this->short_text2) . "',";
|
||||
|
|
@ -375,6 +420,10 @@ class Datanorm extends CommonObject
|
|||
$sql .= " unit_code = '" . $this->db->escape($this->unit_code) . "',";
|
||||
$sql .= " price = " . (float) $this->price . ",";
|
||||
$sql .= " price_unit = " . (int) $this->price_unit . ",";
|
||||
$sql .= " price_unit_code = " . (int) $this->price_unit_code . ",";
|
||||
$sql .= " price_type = " . (int) $this->price_type . ",";
|
||||
$sql .= " metal_surcharge = " . (float) $this->metal_surcharge . ",";
|
||||
$sql .= " vpe = " . ($this->vpe !== null ? (int) $this->vpe : 'NULL') . ",";
|
||||
$sql .= " discount_group = '" . $this->db->escape($this->discount_group) . "',";
|
||||
$sql .= " product_group = '" . $this->db->escape($this->product_group) . "',";
|
||||
$sql .= " alt_unit = '" . $this->db->escape($this->alt_unit) . "',";
|
||||
|
|
@ -382,6 +431,7 @@ class Datanorm extends CommonObject
|
|||
$sql .= " weight = " . ($this->weight !== null ? (float) $this->weight : 'NULL') . ",";
|
||||
$sql .= " matchcode = '" . $this->db->escape($this->matchcode) . "',";
|
||||
$sql .= " datanorm_version = '" . $this->db->escape($this->datanorm_version) . "',";
|
||||
$sql .= " action_code = '" . $this->db->escape($this->action_code) . "',";
|
||||
$sql .= " import_date = '" . $this->db->escape($this->db->idate($this->import_date)) . "',";
|
||||
$sql .= " active = " . (int) $this->active . ",";
|
||||
$sql .= " fk_user_modif = " . (int) $this->fk_user_modif;
|
||||
|
|
@ -649,10 +699,15 @@ class Datanorm extends CommonObject
|
|||
$article->unit_code = $articleData['unit_code'] ?? '';
|
||||
$article->price = $articleData['price'] ?? 0;
|
||||
$article->price_unit = $articleData['price_unit'] ?? 1;
|
||||
$article->price_unit_code = $articleData['price_unit_code'] ?? 0;
|
||||
$article->price_type = $articleData['price_type'] ?? 1;
|
||||
$article->metal_surcharge = $articleData['metal_surcharge'] ?? 0;
|
||||
$article->vpe = $articleData['vpe'] ?? null;
|
||||
$article->discount_group = $articleData['discount_group'] ?? '';
|
||||
$article->product_group = $articleData['product_group'] ?? '';
|
||||
$article->matchcode = $articleData['matchcode'] ?? '';
|
||||
$article->datanorm_version = $parser->version;
|
||||
$article->action_code = $articleData['action_code'] ?? 'N';
|
||||
|
||||
$result = $article->createOrUpdate($user);
|
||||
if ($result > 0) {
|
||||
|
|
@ -712,8 +767,11 @@ class Datanorm extends CommonObject
|
|||
$now = $db->idate(dol_now());
|
||||
|
||||
foreach ($articles as $articleData) {
|
||||
$vpe = isset($articleData['vpe']) ? (int)$articleData['vpe'] : 'NULL';
|
||||
$actionCode = $articleData['action_code'] ?? 'N';
|
||||
$active = ($actionCode === 'L') ? 0 : 1; // Set active=0 for deleted articles
|
||||
$values[] = sprintf(
|
||||
"(%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %f, %d, '%s', '%s', '%s', '%s', '%s', %d, '%s', %d)",
|
||||
"(%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %f, %d, %d, %d, %f, %s, '%s', '%s', '%s', '%s', '%s', %d, '%s', %d, '%s', %d)",
|
||||
(int) $fk_soc,
|
||||
$db->escape($articleData['article_number'] ?? ''),
|
||||
$db->escape($articleData['short_text1'] ?? ''),
|
||||
|
|
@ -725,10 +783,16 @@ class Datanorm extends CommonObject
|
|||
$db->escape($articleData['unit_code'] ?? ''),
|
||||
(float) ($articleData['price'] ?? 0),
|
||||
(int) ($articleData['price_unit'] ?? 1),
|
||||
(int) ($articleData['price_unit_code'] ?? 0),
|
||||
(int) ($articleData['price_type'] ?? 1),
|
||||
(float) ($articleData['metal_surcharge'] ?? 0),
|
||||
$vpe,
|
||||
$db->escape($articleData['discount_group'] ?? ''),
|
||||
$db->escape($articleData['product_group'] ?? ''),
|
||||
$db->escape($articleData['matchcode'] ?? ''),
|
||||
$db->escape($version),
|
||||
$db->escape($actionCode),
|
||||
$active,
|
||||
$now,
|
||||
(int) $user->id,
|
||||
$now,
|
||||
|
|
@ -741,8 +805,8 @@ class Datanorm extends CommonObject
|
|||
$sql = "INSERT INTO " . MAIN_DB_PREFIX . "importzugferd_datanorm ";
|
||||
$sql .= "(fk_soc, article_number, short_text1, short_text2, long_text, ";
|
||||
$sql .= "ean, manufacturer_ref, manufacturer_name, unit_code, ";
|
||||
$sql .= "price, price_unit, discount_group, product_group, matchcode, ";
|
||||
$sql .= "datanorm_version, import_date, fk_user_creat, date_creation, entity) VALUES ";
|
||||
$sql .= "price, price_unit, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group, matchcode, ";
|
||||
$sql .= "datanorm_version, action_code, active, import_date, fk_user_creat, date_creation, entity) VALUES ";
|
||||
$sql .= implode(", ", $values);
|
||||
|
||||
// For updates of existing articles, use ON DUPLICATE KEY UPDATE
|
||||
|
|
@ -756,10 +820,16 @@ class Datanorm extends CommonObject
|
|||
$sql .= "unit_code = VALUES(unit_code), ";
|
||||
$sql .= "price = VALUES(price), ";
|
||||
$sql .= "price_unit = VALUES(price_unit), ";
|
||||
$sql .= "price_unit_code = VALUES(price_unit_code), ";
|
||||
$sql .= "price_type = VALUES(price_type), ";
|
||||
$sql .= "metal_surcharge = VALUES(metal_surcharge), ";
|
||||
$sql .= "vpe = VALUES(vpe), ";
|
||||
$sql .= "discount_group = VALUES(discount_group), ";
|
||||
$sql .= "product_group = VALUES(product_group), ";
|
||||
$sql .= "matchcode = VALUES(matchcode), ";
|
||||
$sql .= "datanorm_version = VALUES(datanorm_version), ";
|
||||
$sql .= "action_code = VALUES(action_code), ";
|
||||
$sql .= "active = VALUES(active), ";
|
||||
$sql .= "import_date = VALUES(import_date), ";
|
||||
$sql .= "fk_user_modif = " . (int) $user->id;
|
||||
|
||||
|
|
@ -785,15 +855,20 @@ class Datanorm extends CommonObject
|
|||
}
|
||||
|
||||
// Second pass: Update prices from DATPREIS files
|
||||
$priceFiles = glob($directory . '/DATPREIS.*');
|
||||
// Use case-insensitive search for Linux compatibility
|
||||
$priceFiles = array();
|
||||
$allFiles = glob($directory . '/*');
|
||||
foreach ($allFiles as $file) {
|
||||
$basename = strtoupper(basename($file));
|
||||
if (preg_match('/^DATPREIS\.\d{3}$/', $basename)) {
|
||||
$priceFiles[] = $file;
|
||||
}
|
||||
}
|
||||
if (!empty($priceFiles)) {
|
||||
foreach ($priceFiles as $file) {
|
||||
$ext = strtoupper(pathinfo($file, PATHINFO_EXTENSION));
|
||||
if (preg_match('/^\d{3}$/', $ext)) {
|
||||
$this->updatePricesFromFile($fk_soc, $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $importCount;
|
||||
}
|
||||
|
|
@ -835,22 +910,33 @@ class Datanorm extends CommonObject
|
|||
$recordType = trim($parts[0] ?? '');
|
||||
|
||||
// P;A format - multiple articles per line
|
||||
// Format: P;A;ArtNr;PreisKz;Preis;PE;Zuschlag;x;x;x;ArtNr2;...
|
||||
// PE is the price unit code from DATPREIS (may differ from A-record!)
|
||||
if ($recordType === 'P' && isset($parts[1]) && $parts[1] === 'A') {
|
||||
$i = 2;
|
||||
while ($i < count($parts) - 2) {
|
||||
$articleNumber = trim($parts[$i] ?? '');
|
||||
$priceRaw = trim($parts[$i + 2] ?? '0');
|
||||
$datpreisPeCode = (int)trim($parts[$i + 3] ?? '0'); // PE code from DATPREIS
|
||||
$metalSurchargeRaw = trim($parts[$i + 4] ?? '0');
|
||||
$price = (float)$priceRaw / 100; // Convert cents to euros
|
||||
$metalSurcharge = (float)$metalSurchargeRaw / 100; // Convert cents to euros
|
||||
|
||||
if (!empty($articleNumber) && $price > 0) {
|
||||
$batch[$articleNumber] = $price;
|
||||
$batch[$articleNumber] = array(
|
||||
'price' => $price,
|
||||
'metal_surcharge' => $metalSurcharge,
|
||||
'datpreis_pe_code' => $datpreisPeCode
|
||||
);
|
||||
}
|
||||
|
||||
$i += 9; // 9 fields per article
|
||||
}
|
||||
} elseif ($recordType === 'P' || $recordType === '0') {
|
||||
// Simple format: P;ArtNr;PreisKz;Preis;PE;...
|
||||
$articleNumber = trim($parts[1] ?? '');
|
||||
$priceRaw = trim($parts[3] ?? '0');
|
||||
$datpreisPeCode = (int)trim($parts[4] ?? '0'); // PE code if available
|
||||
|
||||
if (strpos($priceRaw, ',') === false && strpos($priceRaw, '.') === false) {
|
||||
$price = (float)$priceRaw / 100;
|
||||
|
|
@ -860,7 +946,11 @@ class Datanorm extends CommonObject
|
|||
}
|
||||
|
||||
if (!empty($articleNumber) && $price > 0) {
|
||||
$batch[$articleNumber] = $price;
|
||||
$batch[$articleNumber] = array(
|
||||
'price' => $price,
|
||||
'metal_surcharge' => 0,
|
||||
'datpreis_pe_code' => $datpreisPeCode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -882,9 +972,10 @@ class Datanorm extends CommonObject
|
|||
|
||||
/**
|
||||
* Flush price batch to database
|
||||
* DATPREIS prices are already given for the A-Satz PE unit - no normalization needed!
|
||||
*
|
||||
* @param int $fk_soc Supplier ID
|
||||
* @param array $prices Array of article_number => price
|
||||
* @param array $prices Array of article_number => array('price' => ..., 'metal_surcharge' => ...)
|
||||
* @return int Number of rows updated
|
||||
*/
|
||||
protected function flushPriceBatch($fk_soc, $prices)
|
||||
|
|
@ -897,19 +988,25 @@ class Datanorm extends CommonObject
|
|||
|
||||
$updated = 0;
|
||||
|
||||
// Build CASE statement for batch update
|
||||
$cases = array();
|
||||
// Build CASE statements for batch update
|
||||
// Note: DATPREIS prices are already for the A-Satz PE unit, no normalization needed
|
||||
$priceCases = array();
|
||||
$metalCases = array();
|
||||
$articleNumbers = array();
|
||||
|
||||
foreach ($prices as $artNum => $price) {
|
||||
$cases[] = "WHEN '" . $this->db->escape($artNum) . "' THEN " . (float)$price;
|
||||
foreach ($prices as $artNum => $priceData) {
|
||||
$priceCases[] = "WHEN '" . $this->db->escape($artNum) . "' THEN " . (float)$priceData['price'];
|
||||
$metalCases[] = "WHEN '" . $this->db->escape($artNum) . "' THEN " . (float)$priceData['metal_surcharge'];
|
||||
$articleNumbers[] = "'" . $this->db->escape($artNum) . "'";
|
||||
}
|
||||
|
||||
if (!empty($cases)) {
|
||||
if (!empty($priceCases)) {
|
||||
$sql = "UPDATE " . MAIN_DB_PREFIX . "importzugferd_datanorm SET ";
|
||||
$sql .= "price = CASE article_number ";
|
||||
$sql .= implode(" ", $cases);
|
||||
$sql .= implode(" ", $priceCases);
|
||||
$sql .= " END, ";
|
||||
$sql .= "metal_surcharge = CASE article_number ";
|
||||
$sql .= implode(" ", $metalCases);
|
||||
$sql .= " END ";
|
||||
$sql .= "WHERE fk_soc = " . (int)$fk_soc;
|
||||
$sql .= " AND entity = " . (int)$conf->entity;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,41 @@
|
|||
/**
|
||||
* Class DatanormParser
|
||||
* Parses Datanorm catalog files (Version 4.0 and 5.0)
|
||||
*
|
||||
* Datanorm Price Unit (PE) Codes:
|
||||
* 0 or empty = per 1 piece
|
||||
* 1 = per 10 pieces
|
||||
* 2 = per 100 pieces
|
||||
* 3 = per 1000 pieces
|
||||
*
|
||||
* The price in Datanorm is given for the quantity specified by the PE code.
|
||||
* To get the unit price: divide price by PE quantity.
|
||||
*/
|
||||
class DatanormParser
|
||||
{
|
||||
/**
|
||||
* Price unit code mapping
|
||||
* Datanorm uses codes 0-3 to represent price units
|
||||
*/
|
||||
const PRICE_UNIT_CODES = array(
|
||||
0 => 1,
|
||||
1 => 10,
|
||||
2 => 100,
|
||||
3 => 1000,
|
||||
);
|
||||
|
||||
/**
|
||||
* Convert Datanorm PE code to actual quantity
|
||||
*
|
||||
* @param int|string $peCode The PE code from Datanorm (0, 1, 2, or 3)
|
||||
* @return int The actual quantity (1, 10, 100, or 1000)
|
||||
*/
|
||||
public static function convertPriceUnitCode($peCode)
|
||||
{
|
||||
$code = (int)$peCode;
|
||||
return self::PRICE_UNIT_CODES[$code] ?? 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string Detected Datanorm version
|
||||
*/
|
||||
|
|
@ -122,6 +154,9 @@ class DatanormParser
|
|||
foreach ($this->batchArticles as $artNum => &$article) {
|
||||
if (isset($this->prices[$artNum])) {
|
||||
$article['price'] = $this->prices[$artNum]['price'];
|
||||
if (!empty($this->prices[$artNum]['metal_surcharge'])) {
|
||||
$article['metal_surcharge'] = $this->prices[$artNum]['metal_surcharge'];
|
||||
}
|
||||
unset($this->prices[$artNum]); // Free memory
|
||||
}
|
||||
}
|
||||
|
|
@ -157,40 +192,54 @@ class DatanormParser
|
|||
{
|
||||
$totalArticles = 0;
|
||||
|
||||
// For non-streaming mode, load prices first
|
||||
// For streaming mode with very large files, prices must be handled separately
|
||||
// Use case-insensitive search for Linux compatibility
|
||||
$allFiles = glob($dir . '/*');
|
||||
|
||||
// For non-streaming mode, load prices first into memory
|
||||
// For streaming mode, prices are updated via second pass directly to DB
|
||||
if (!$this->streamingMode) {
|
||||
$priceFiles = glob($dir . '/DATPREIS.*');
|
||||
$priceFiles = array();
|
||||
foreach ($allFiles as $file) {
|
||||
$basename = strtoupper(basename($file));
|
||||
if (preg_match('/^DATPREIS\.\d{3}$/', $basename)) {
|
||||
$priceFiles[] = $file;
|
||||
}
|
||||
}
|
||||
if (!empty($priceFiles)) {
|
||||
$this->version = '4.0';
|
||||
foreach ($priceFiles as $file) {
|
||||
$ext = strtoupper(pathinfo($file, PATHINFO_EXTENSION));
|
||||
if (preg_match('/^\d{3}$/', $ext)) {
|
||||
$this->parseDatapreis4File($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for Datanorm 4.0 files (DATANORM.xxx)
|
||||
$files = glob($dir . '/DATANORM.*');
|
||||
if (!empty($files)) {
|
||||
// Look for Datanorm 4.0 files (DATANORM.xxx) - case-insensitive
|
||||
$datanormFiles = array();
|
||||
$wrgFiles = array();
|
||||
$rabFiles = array();
|
||||
foreach ($allFiles as $file) {
|
||||
$basename = strtoupper(basename($file));
|
||||
if (preg_match('/^DATANORM\.\d{3}$/', $basename)) {
|
||||
$datanormFiles[] = $file;
|
||||
} elseif ($basename === 'DATANORM.WRG') {
|
||||
$wrgFiles[] = $file;
|
||||
} elseif ($basename === 'DATANORM.RAB') {
|
||||
$rabFiles[] = $file;
|
||||
}
|
||||
}
|
||||
if (!empty($datanormFiles)) {
|
||||
$this->version = '4.0';
|
||||
foreach ($files as $file) {
|
||||
$ext = strtoupper(pathinfo($file, PATHINFO_EXTENSION));
|
||||
if (preg_match('/^\d{3}$/', $ext)) {
|
||||
// Main article file (DATANORM.001, etc.)
|
||||
foreach ($datanormFiles as $file) {
|
||||
$count = $this->parseDatanorm4File($file);
|
||||
if ($count > 0) {
|
||||
$totalArticles += $count;
|
||||
}
|
||||
} elseif ($ext === 'WRG') {
|
||||
// Product groups file
|
||||
$this->parseDatanorm4Groups($file);
|
||||
} elseif ($ext === 'RAB') {
|
||||
// Discount groups file
|
||||
$this->parseDatanorm4Discounts($file);
|
||||
}
|
||||
foreach ($wrgFiles as $file) {
|
||||
$this->parseDatanorm4Groups($file);
|
||||
}
|
||||
foreach ($rabFiles as $file) {
|
||||
$this->parseDatanorm4Discounts($file);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,14 +378,20 @@ class DatanormParser
|
|||
return $this->parseDatanorm4TypeASemicolon($line);
|
||||
}
|
||||
|
||||
// Fixed-width format (classic)
|
||||
// Fixed-width format (classic Datanorm 3.0/4.0)
|
||||
// PE code is at position 112-116 and is a CODE (0=1, 1=10, 2=100, 3=1000)
|
||||
$peCode = (int)trim(substr($line, 111, 5));
|
||||
$priceUnit = self::convertPriceUnitCode($peCode);
|
||||
|
||||
$article = array(
|
||||
'article_number' => trim(substr($line, 1, 15)), // Pos 2-16: Artikelnummer
|
||||
'action_code' => 'N', // Fixed-width format has no action code
|
||||
'matchcode' => trim(substr($line, 16, 12)), // Pos 17-28: Matchcode
|
||||
'short_text1' => trim(substr($line, 28, 40)), // Pos 29-68: Kurztext 1
|
||||
'short_text2' => trim(substr($line, 68, 40)), // Pos 69-108: Kurztext 2
|
||||
'unit_code' => trim(substr($line, 108, 3)), // Pos 109-111: Mengeneinheit
|
||||
'price_unit' => (int)trim(substr($line, 111, 5)), // Pos 112-116: Preiseinheit
|
||||
'price_unit' => $priceUnit, // Converted from PE code
|
||||
'price_unit_code' => $peCode, // Original PE code
|
||||
'discount_group' => trim(substr($line, 116, 4)), // Pos 117-120: Rabattgruppe
|
||||
'product_group' => trim(substr($line, 120, 7)), // Pos 121-127: Warengruppe
|
||||
'manufacturer_ref' => trim(substr($line, 127, 15)), // Pos 128-142: Hersteller-Artikelnummer
|
||||
|
|
@ -355,11 +410,6 @@ class DatanormParser
|
|||
return null;
|
||||
}
|
||||
|
||||
// Default price unit to 1 if not set
|
||||
if ($article['price_unit'] <= 0) {
|
||||
$article['price_unit'] = 1;
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
|
|
@ -378,22 +428,32 @@ class DatanormParser
|
|||
}
|
||||
|
||||
// Detect format variant
|
||||
// Sonepar format: A;N;ArtNr;WG;Kurztext1;Kurztext2;PE;ME;METext;RabGrp;PreisGrp;WG2;...
|
||||
// Sonepar format: A;N;ArtNr;TextKz;Kurztext1;Kurztext2;PreisKz;PE;ME;Preis;RabGrp;WG;...
|
||||
// Index: 0 1 2 3 4 5 6 7 8 9 10 11
|
||||
// Standard format: A;ArtNr;Matchcode;Kurztext1;Kurztext2;ME;PE;RabGrp;WG;...
|
||||
|
||||
$firstField = trim($parts[0] ?? '');
|
||||
|
||||
if ($firstField === 'A' && isset($parts[1]) && strlen(trim($parts[1])) <= 2) {
|
||||
// Sonepar format: A;N;ArtNr;WG;Kurztext1;Kurztext2;PE;ME;METext;RabGrp;PreisGrp;WG2;...
|
||||
// Sonepar format with action code (N=New, L=Delete, A=Update)
|
||||
// A;N;ArtNr;TextKz;Kurztext1;Kurztext2;PreisKz;PE;ME;Preis;RabGrp;WG;LangTextKey
|
||||
// PE is at index 7 and is a CODE (0=1, 1=10, 2=100, 3=1000)
|
||||
$actionCode = strtoupper(trim($parts[1] ?? 'N'));
|
||||
$peCode = (int)trim($parts[7] ?? '0');
|
||||
$priceUnit = self::convertPriceUnitCode($peCode);
|
||||
|
||||
$article = array(
|
||||
'article_number' => trim($parts[2] ?? ''),
|
||||
'action_code' => $actionCode, // N=New, A=Update, L=Delete
|
||||
'matchcode' => '', // Will be set from B record
|
||||
'short_text1' => trim($parts[4] ?? ''),
|
||||
'short_text2' => trim($parts[5] ?? ''),
|
||||
'unit_code' => trim($parts[8] ?? trim($parts[7] ?? '')), // METext or ME
|
||||
'price_unit' => (int)trim($parts[6] ?? '1'), // PE
|
||||
'discount_group' => trim($parts[9] ?? ''),
|
||||
'product_group' => trim($parts[3] ?? ''), // WG at position 3
|
||||
'unit_code' => trim($parts[8] ?? ''), // ME (Mengeneinheit) at index 8
|
||||
'price_unit' => $priceUnit, // Converted from PE code at index 7
|
||||
'price_unit_code' => $peCode, // Original PE code for reference
|
||||
'discount_group' => trim($parts[10] ?? ''), // Rabattgruppe at index 10
|
||||
'product_group' => trim($parts[11] ?? ''), // Warengruppe at index 11
|
||||
'price_type' => trim($parts[6] ?? ''), // Preiskennzeichen (1=Brutto, 2=Netto)
|
||||
'manufacturer_ref' => '',
|
||||
'manufacturer_name' => '',
|
||||
'ean' => '',
|
||||
|
|
@ -401,14 +461,20 @@ class DatanormParser
|
|||
'price' => 0,
|
||||
);
|
||||
} else {
|
||||
// Standard format
|
||||
// Standard format: A;ArtNr;Matchcode;Kurztext1;Kurztext2;ME;PE;RabGrp;WG;...
|
||||
// PE at index 6 is a CODE (0=1, 1=10, 2=100, 3=1000)
|
||||
$peCode = (int)trim($parts[6] ?? '0');
|
||||
$priceUnit = self::convertPriceUnitCode($peCode);
|
||||
|
||||
$article = array(
|
||||
'article_number' => trim($parts[1] ?? ''),
|
||||
'action_code' => 'N', // Default to New for standard format
|
||||
'matchcode' => trim($parts[2] ?? ''),
|
||||
'short_text1' => trim($parts[3] ?? ''),
|
||||
'short_text2' => trim($parts[4] ?? ''),
|
||||
'unit_code' => trim($parts[5] ?? ''),
|
||||
'price_unit' => (int)trim($parts[6] ?? '1'),
|
||||
'price_unit' => $priceUnit,
|
||||
'price_unit_code' => $peCode,
|
||||
'discount_group' => trim($parts[7] ?? ''),
|
||||
'product_group' => trim($parts[8] ?? ''),
|
||||
'manufacturer_ref' => trim($parts[14] ?? ''),
|
||||
|
|
@ -423,10 +489,6 @@ class DatanormParser
|
|||
return null;
|
||||
}
|
||||
|
||||
if ($article['price_unit'] <= 0) {
|
||||
$article['price_unit'] = 1;
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
|
|
@ -453,6 +515,7 @@ class DatanormParser
|
|||
|
||||
/**
|
||||
* Parse Datanorm 4.0 Type B record (Article info/long text)
|
||||
* Sonepar format: B;N;ArtNr;Matchcode; ; ;;;;EAN; ; ;0;VPE;;;
|
||||
*
|
||||
* @param string $line Record line
|
||||
* @param string $articleNumber Current article number
|
||||
|
|
@ -467,7 +530,8 @@ class DatanormParser
|
|||
if (strpos($line, ';') !== false) {
|
||||
$parts = explode(';', $line);
|
||||
|
||||
// Sonepar format: B;N;ArtNr;Matchcode;...
|
||||
// Sonepar format: B;N;ArtNr;Matchcode; ; ;...;EAN; ; ;0;VPE;;;
|
||||
// Field positions can vary, so we search for EAN and VPE
|
||||
if (isset($parts[1]) && strlen(trim($parts[1])) <= 2) {
|
||||
// Get article number from B record to verify
|
||||
$bArticleNumber = trim($parts[2] ?? '');
|
||||
|
|
@ -477,6 +541,28 @@ class DatanormParser
|
|||
if (!empty($matchcode) && empty($article['matchcode'])) {
|
||||
$article['matchcode'] = $matchcode;
|
||||
}
|
||||
|
||||
// Search for EAN (13-digit numeric code) in any field
|
||||
if (empty($article['ean'])) {
|
||||
foreach ($parts as $part) {
|
||||
$part = trim($part);
|
||||
if (preg_match('/^\d{13}$/', $part)) {
|
||||
$article['ean'] = $part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VPE (Verpackungseinheit) in B record is the packaging quantity
|
||||
// This is informational - the price unit from A record PE code is authoritative
|
||||
// We store VPE separately for reference but don't override price_unit
|
||||
for ($i = 12; $i <= min(15, count($parts) - 1); $i++) {
|
||||
$vpe = (int)trim($parts[$i] ?? '0');
|
||||
if ($vpe > 1) {
|
||||
$article['vpe'] = $vpe; // Store as separate field
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard format: text at position 2
|
||||
|
|
@ -604,27 +690,34 @@ class DatanormParser
|
|||
$recordType = trim($parts[0] ?? '');
|
||||
|
||||
// P;A format - multiple articles per line
|
||||
// Format: P;A;ArtNr;PreisKz;Preis;PE;x;x;x;x;ArtNr2;PreisKz2;Preis2;...
|
||||
// Format: P;A;ArtNr;PreisKz;Preis;PE;Zuschlag;x;x;x;ArtNr2;PreisKz2;Preis2;...
|
||||
// For cables: Preis = Materialpreis, Zuschlag = Metallzuschlag (copper surcharge)
|
||||
// PE code from DATPREIS may differ from A-record - used for price normalization
|
||||
if ($recordType === 'P' && isset($parts[1]) && $parts[1] === 'A') {
|
||||
// Parse multiple price entries per line
|
||||
// Each entry is: ArtNr;PreisKz;Preis;PE;0;1;0;1;0
|
||||
// Each entry is: ArtNr;PreisKz;Preis;PE;Zuschlag;x;x;x;x
|
||||
$i = 2; // Start after P;A
|
||||
while ($i < count($parts) - 2) {
|
||||
$articleNumber = trim($parts[$i] ?? '');
|
||||
$priceType = trim($parts[$i + 1] ?? '');
|
||||
$priceRaw = trim($parts[$i + 2] ?? '0');
|
||||
$datpreisPeCode = (int)trim($parts[$i + 3] ?? '0'); // PE code from DATPREIS
|
||||
$metalSurchargeRaw = trim($parts[$i + 4] ?? '0');
|
||||
|
||||
// Price is in cents, convert to euros
|
||||
$price = (float)$priceRaw / 100;
|
||||
$metalSurcharge = (float)$metalSurchargeRaw / 100;
|
||||
|
||||
if (!empty($articleNumber) && $price > 0) {
|
||||
$this->prices[$articleNumber] = array(
|
||||
'price' => $price,
|
||||
'price_type' => $priceType,
|
||||
'metal_surcharge' => $metalSurcharge,
|
||||
'datpreis_pe_code' => $datpreisPeCode,
|
||||
);
|
||||
}
|
||||
|
||||
// Move to next article (9 fields per article: ArtNr;Kz;Preis;PE;0;1;0;1;0)
|
||||
// Move to next article (9 fields per article: ArtNr;Kz;Preis;PE;Zuschlag;x;x;x;x)
|
||||
$i += 9;
|
||||
}
|
||||
} elseif ($recordType === 'P' || $recordType === '0') {
|
||||
|
|
@ -678,12 +771,16 @@ class DatanormParser
|
|||
|
||||
/**
|
||||
* Merge prices into articles
|
||||
* DATPREIS prices are already for the A-Satz PE unit - no normalization needed!
|
||||
*/
|
||||
protected function mergePricesIntoArticles()
|
||||
{
|
||||
foreach ($this->prices as $articleNumber => $priceData) {
|
||||
if (isset($this->articles[$articleNumber])) {
|
||||
$this->articles[$articleNumber]['price'] = $priceData['price'];
|
||||
if (!empty($priceData['metal_surcharge'])) {
|
||||
$this->articles[$articleNumber]['metal_surcharge'] = $priceData['metal_surcharge'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,6 +327,45 @@ class ZugferdParser
|
|||
$unitPrice = $chargeAmount;
|
||||
}
|
||||
|
||||
// Extract copper surcharge (Kupferzuschlag) from AppliedTradeAllowanceCharge
|
||||
$copperSurcharge = null;
|
||||
$copperSurchargeBasisQty = null;
|
||||
$allowanceCharges = $line->xpath('ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:AppliedTradeAllowanceCharge');
|
||||
foreach ($allowanceCharges as $charge) {
|
||||
$charge->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12');
|
||||
$reason = (string) $this->getNodeValue($charge->xpath('ram:Reason'));
|
||||
if (stripos($reason, 'Kupfer') !== false || stripos($reason, 'copper') !== false || stripos($reason, 'Metall') !== false) {
|
||||
$copperSurcharge = (float) $this->getNodeValue($charge->xpath('ram:ActualAmount'));
|
||||
$copperSurchargeBasisQty = (float) $this->getNodeValue($charge->xpath('ram:BasisQuantity'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check ApplicableProductCharacteristic for copper surcharge
|
||||
if ($copperSurcharge === null) {
|
||||
$characteristics = $line->xpath('ram:SpecifiedTradeProduct/ram:ApplicableProductCharacteristic');
|
||||
foreach ($characteristics as $char) {
|
||||
$char->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12');
|
||||
$desc = (string) $this->getNodeValue($char->xpath('ram:Description'));
|
||||
if (stripos($desc, 'Kupfer') !== false || stripos($desc, 'copper') !== false || stripos($desc, 'Metall') !== false) {
|
||||
$copperSurcharge = (float) $this->getNodeValue($char->xpath('ram:Value'));
|
||||
// Usually refers to same basis quantity as the price
|
||||
$copperSurchargeBasisQty = $basisQuantity ?: 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate copper surcharge per single unit
|
||||
$copperSurchargePerUnit = null;
|
||||
if ($copperSurcharge !== null && $copperSurcharge > 0) {
|
||||
if ($copperSurchargeBasisQty > 0 && $copperSurchargeBasisQty != 1) {
|
||||
$copperSurchargePerUnit = $copperSurcharge / $copperSurchargeBasisQty;
|
||||
} else {
|
||||
$copperSurchargePerUnit = $copperSurcharge;
|
||||
}
|
||||
}
|
||||
|
||||
$lineData = array(
|
||||
'line_id' => (string) $this->getNodeValue($line->xpath('ram:AssociatedDocumentLineDocument/ram:LineID')),
|
||||
'product' => array(
|
||||
|
|
@ -344,6 +383,10 @@ class ZugferdParser
|
|||
'basis_quantity_unit' => $basisQuantityUnit,
|
||||
'line_total' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedSupplyChainTradeSettlement/ram:SpecifiedTradeSettlementMonetarySummation/ram:LineTotalAmount')),
|
||||
'tax_percent' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:ApplicablePercent')),
|
||||
// Copper surcharge data
|
||||
'copper_surcharge' => $copperSurcharge,
|
||||
'copper_surcharge_basis_qty' => $copperSurchargeBasisQty,
|
||||
'copper_surcharge_per_unit' => $copperSurchargePerUnit,
|
||||
);
|
||||
|
||||
$data['lines'][] = $lineData;
|
||||
|
|
@ -426,6 +469,44 @@ class ZugferdParser
|
|||
$unitPrice = $chargeAmount;
|
||||
}
|
||||
|
||||
// Extract copper surcharge (Kupferzuschlag) from AppliedTradeAllowanceCharge (v2)
|
||||
$copperSurcharge = null;
|
||||
$copperSurchargeBasisQty = null;
|
||||
$allowanceCharges = $line->xpath('ram:SpecifiedLineTradeAgreement/ram:GrossPriceProductTradePrice/ram:AppliedTradeAllowanceCharge');
|
||||
foreach ($allowanceCharges as $charge) {
|
||||
$charge->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');
|
||||
$reason = (string) $this->getNodeValue($charge->xpath('ram:Reason'));
|
||||
if (stripos($reason, 'Kupfer') !== false || stripos($reason, 'copper') !== false || stripos($reason, 'Metall') !== false) {
|
||||
$copperSurcharge = (float) $this->getNodeValue($charge->xpath('ram:ActualAmount'));
|
||||
$copperSurchargeBasisQty = (float) $this->getNodeValue($charge->xpath('ram:BasisQuantity'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check ApplicableProductCharacteristic for copper surcharge (v2)
|
||||
if ($copperSurcharge === null) {
|
||||
$characteristics = $line->xpath('ram:SpecifiedTradeProduct/ram:ApplicableProductCharacteristic');
|
||||
foreach ($characteristics as $char) {
|
||||
$char->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');
|
||||
$desc = (string) $this->getNodeValue($char->xpath('ram:Description'));
|
||||
if (stripos($desc, 'Kupfer') !== false || stripos($desc, 'copper') !== false || stripos($desc, 'Metall') !== false) {
|
||||
$copperSurcharge = (float) $this->getNodeValue($char->xpath('ram:Value'));
|
||||
$copperSurchargeBasisQty = $basisQuantity ?: 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate copper surcharge per single unit
|
||||
$copperSurchargePerUnit = null;
|
||||
if ($copperSurcharge !== null && $copperSurcharge > 0) {
|
||||
if ($copperSurchargeBasisQty > 0 && $copperSurchargeBasisQty != 1) {
|
||||
$copperSurchargePerUnit = $copperSurcharge / $copperSurchargeBasisQty;
|
||||
} else {
|
||||
$copperSurchargePerUnit = $copperSurcharge;
|
||||
}
|
||||
}
|
||||
|
||||
$lineData = array(
|
||||
'line_id' => (string) $this->getNodeValue($line->xpath('ram:AssociatedDocumentLineDocument/ram:LineID')),
|
||||
'product' => array(
|
||||
|
|
@ -443,6 +524,10 @@ class ZugferdParser
|
|||
'basis_quantity_unit' => $basisQuantityUnit,
|
||||
'line_total' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount')),
|
||||
'tax_percent' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent')),
|
||||
// Copper surcharge data
|
||||
'copper_surcharge' => $copperSurcharge,
|
||||
'copper_surcharge_basis_qty' => $copperSurchargeBasisQty,
|
||||
'copper_surcharge_per_unit' => $copperSurchargePerUnit,
|
||||
);
|
||||
|
||||
$data['lines'][] = $lineData;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class modImportZugferd extends DolibarrModules
|
|||
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@importzugferd'
|
||||
|
||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||
$this->version = '2.0';
|
||||
$this->version = '2.1';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
@ -430,6 +430,40 @@ class modImportZugferd extends DolibarrModules
|
|||
'user' => 2,
|
||||
);
|
||||
|
||||
// Left menu: Datanorm Mass Update
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=importzugferd',
|
||||
'type' => 'left',
|
||||
'titre' => 'DatanormMassUpdate',
|
||||
'prefix' => img_picto('', 'fa-sync', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => 'zugferd_datanorm_update',
|
||||
'url' => '/importzugferd/datanorm_update.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")',
|
||||
'perms' => '$user->hasRight("produit", "creer")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
// Left menu: Datanorm Change Log
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=importzugferd',
|
||||
'type' => 'left',
|
||||
'titre' => 'DatanormChangeLog',
|
||||
'prefix' => img_picto('', 'fa-history', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => 'zugferd_datanorm_log',
|
||||
'url' => '/importzugferd/datanorm_changelog.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")',
|
||||
'perms' => '$user->hasRight("produit", "lire")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
|
||||
// Exports profiles provided by this module
|
||||
$r = 0;
|
||||
|
|
@ -544,6 +578,72 @@ class modImportZugferd extends DolibarrModules
|
|||
'isModEnabled("importzugferd")' // enabled condition
|
||||
);
|
||||
|
||||
// Add extrafield for metal surcharge (Kupferzuschlag) on supplier prices
|
||||
$extrafields->addExtraField(
|
||||
'kupferzuschlag', // attribute code
|
||||
'Kupferzuschlag', // label (translation key)
|
||||
'price', // type (price field)
|
||||
110, // position
|
||||
'24,8', // 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 price unit (Preiseinheit) on supplier prices
|
||||
$extrafields->addExtraField(
|
||||
'preiseinheit', // attribute code
|
||||
'Preiseinheit', // label (translation key)
|
||||
'int', // type
|
||||
120, // position
|
||||
11, // size
|
||||
'product_fournisseur_price', // element type
|
||||
0, // unique
|
||||
0, // required
|
||||
'1', // 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
|
||||
'Kupfergehalt', // label (translation key)
|
||||
'double', // type (decimal number)
|
||||
130, // position
|
||||
'24,4', // size (precision,scale)
|
||||
'product', // 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
|
||||
);
|
||||
|
||||
// Permissions
|
||||
$this->remove($options);
|
||||
|
||||
|
|
|
|||
333
datanorm_changelog.php
Normal file
333
datanorm_changelog.php
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
<?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 datanorm_changelog.php
|
||||
* \ingroup importzugferd
|
||||
* \brief View Datanorm update change log
|
||||
*/
|
||||
|
||||
// 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/admin.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formcompany.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
||||
dol_include_once('/importzugferd/lib/importzugferd.lib.php');
|
||||
|
||||
// Load translations
|
||||
$langs->loadLangs(array("importzugferd@importzugferd", "products", "bills"));
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('produit', 'lire')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$batch_id = GETPOST('batch_id', 'alphanohtml');
|
||||
$fk_product = GETPOSTINT('fk_product');
|
||||
$fk_soc = GETPOSTINT('fk_soc');
|
||||
$date_start = GETPOST('date_start', 'alpha');
|
||||
$date_end = GETPOST('date_end', 'alpha');
|
||||
|
||||
// Pagination
|
||||
$sortfield = GETPOST('sortfield', 'aZ09comma');
|
||||
$sortorder = GETPOST('sortorder', 'aZ09comma');
|
||||
$page = GETPOSTISSET('pageplusone') ? (GETPOSTINT('pageplusone') - 1) : GETPOSTINT('page');
|
||||
$limit = GETPOSTINT('limit') ? GETPOSTINT('limit') : $conf->liste_limit;
|
||||
$offset = $limit * $page;
|
||||
|
||||
if (!$sortfield) $sortfield = 'l.date_change';
|
||||
if (!$sortorder) $sortorder = 'DESC';
|
||||
|
||||
// Initialize objects
|
||||
$form = new Form($db);
|
||||
$formcompany = new FormCompany($db);
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$title = $langs->trans('DatanormChangeLog');
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-datanorm-changelog');
|
||||
|
||||
print load_fiche_titre($title, '<a href="'.dol_buildpath('/importzugferd/datanorm_update.php', 1).'" class="button">'.$langs->trans('DatanormMassUpdate').'</a>', 'fa-history');
|
||||
|
||||
// Filter form
|
||||
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="6">'.$langs->trans('Filters').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Batch ID filter
|
||||
print '<td class="titlefield">'.$langs->trans('BatchUpdate').'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" name="batch_id" value="'.dol_escape_htmltag($batch_id).'" class="minwidth200" placeholder="batch_...">';
|
||||
print '</td>';
|
||||
|
||||
// Supplier filter
|
||||
print '<td>'.$langs->trans('Supplier').'</td>';
|
||||
print '<td>';
|
||||
$sql = "SELECT DISTINCT s.rowid, s.nom FROM ".MAIN_DB_PREFIX."societe s";
|
||||
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."importzugferd_datanorm_log l ON l.fk_soc = s.rowid";
|
||||
$sql .= " ORDER BY s.nom";
|
||||
$resql = $db->query($sql);
|
||||
print '<select name="fk_soc" class="flat minwidth200">';
|
||||
print '<option value="">'.$langs->trans('All').'</option>';
|
||||
if ($resql) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
$selected = ($obj->rowid == $fk_soc) ? 'selected' : '';
|
||||
print '<option value="'.$obj->rowid.'" '.$selected.'>'.dol_escape_htmltag($obj->nom).'</option>';
|
||||
}
|
||||
}
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
|
||||
// Date range
|
||||
print '<td>'.$langs->trans('DateRange').'</td>';
|
||||
print '<td>';
|
||||
print '<input type="date" name="date_start" value="'.dol_escape_htmltag($date_start).'" class="minwidth100">';
|
||||
print ' - ';
|
||||
print '<input type="date" name="date_end" value="'.dol_escape_htmltag($date_end).'" class="minwidth100">';
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="6" class="center">';
|
||||
print '<input type="submit" class="button" value="'.$langs->trans('Search').'">';
|
||||
print ' <a href="'.$_SERVER['PHP_SELF'].'" class="button">'.$langs->trans('Reset').'</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
// Build SQL query
|
||||
$sql = "SELECT l.*, p.ref as product_ref, p.label as product_label, s.nom as supplier_name, u.login as user_login";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."importzugferd_datanorm_log l";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = l.fk_product";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe s ON s.rowid = l.fk_soc";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user u ON u.rowid = l.fk_user";
|
||||
$sql .= " WHERE l.entity IN (".getEntity('product').")";
|
||||
|
||||
if (!empty($batch_id)) {
|
||||
$sql .= " AND l.batch_id = '".$db->escape($batch_id)."'";
|
||||
}
|
||||
if ($fk_soc > 0) {
|
||||
$sql .= " AND l.fk_soc = ".((int)$fk_soc);
|
||||
}
|
||||
if ($fk_product > 0) {
|
||||
$sql .= " AND l.fk_product = ".((int)$fk_product);
|
||||
}
|
||||
if (!empty($date_start)) {
|
||||
$sql .= " AND l.date_change >= '".$db->escape($date_start)." 00:00:00'";
|
||||
}
|
||||
if (!empty($date_end)) {
|
||||
$sql .= " AND l.date_change <= '".$db->escape($date_end)." 23:59:59'";
|
||||
}
|
||||
|
||||
// Count total
|
||||
$sqlcount = preg_replace('/SELECT.*FROM/', 'SELECT COUNT(*) as total FROM', $sql);
|
||||
$resqlcount = $db->query($sqlcount);
|
||||
$total = 0;
|
||||
if ($resqlcount) {
|
||||
$objcount = $db->fetch_object($resqlcount);
|
||||
$total = $objcount->total;
|
||||
}
|
||||
|
||||
$sql .= $db->order($sortfield, $sortorder);
|
||||
$sql .= $db->plimit($limit + 1, $offset);
|
||||
|
||||
$resql = $db->query($sql);
|
||||
|
||||
print '<br>';
|
||||
|
||||
// Batch summary if filtering by batch
|
||||
if (!empty($batch_id)) {
|
||||
$sql_batch = "SELECT MIN(date_change) as start_date, MAX(date_change) as end_date, COUNT(DISTINCT fk_product) as product_count, COUNT(*) as change_count";
|
||||
$sql_batch .= " FROM ".MAIN_DB_PREFIX."importzugferd_datanorm_log";
|
||||
$sql_batch .= " WHERE batch_id = '".$db->escape($batch_id)."'";
|
||||
$res_batch = $db->query($sql_batch);
|
||||
if ($res_batch) {
|
||||
$batch_info = $db->fetch_object($res_batch);
|
||||
print '<div class="info">';
|
||||
print '<strong>'.$langs->trans('BatchUpdate').':</strong> '.$batch_id.'<br>';
|
||||
print '<strong>'.$langs->trans('DateChange').':</strong> '.dol_print_date($db->jdate($batch_info->start_date), 'dayhour').'<br>';
|
||||
print '<strong>'.$langs->trans('Products').':</strong> '.$batch_info->product_count.' | ';
|
||||
print '<strong>'.$langs->trans('Changes').':</strong> '.$batch_info->change_count;
|
||||
print '</div><br>';
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
print_barre_liste($langs->trans('ChangeHistory'), $page, $_SERVER['PHP_SELF'], '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc.'&date_start='.$date_start.'&date_end='.$date_end, $sortfield, $sortorder, '', $resql ? $db->num_rows($resql) : 0, $total, '', 0, '', '', $limit);
|
||||
|
||||
if ($resql) {
|
||||
$num = $db->num_rows($resql);
|
||||
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="noborder centpercent">';
|
||||
|
||||
// Header
|
||||
print '<tr class="liste_titre">';
|
||||
print_liste_field_titre($langs->trans('DateChange'), $_SERVER['PHP_SELF'], 'l.date_change', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('Product'), $_SERVER['PHP_SELF'], 'p.ref', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('Supplier'), $_SERVER['PHP_SELF'], 's.nom', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('DatanormArticle'), $_SERVER['PHP_SELF'], 'l.datanorm_ref', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('FieldChanged'), $_SERVER['PHP_SELF'], 'l.field_changed', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('OldValue'), $_SERVER['PHP_SELF'], '', '', '', '');
|
||||
print_liste_field_titre($langs->trans('NewValue'), $_SERVER['PHP_SELF'], '', '', '', '');
|
||||
print_liste_field_titre($langs->trans('ChangedBy'), $_SERVER['PHP_SELF'], 'u.login', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print '</tr>';
|
||||
|
||||
if ($num > 0) {
|
||||
$i = 0;
|
||||
while ($i < min($num, $limit)) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Date
|
||||
print '<td class="nowraponall">'.dol_print_date($db->jdate($obj->date_change), 'dayhour').'</td>';
|
||||
|
||||
// Product
|
||||
print '<td>';
|
||||
$product = new Product($db);
|
||||
if ($product->fetch($obj->fk_product) > 0) {
|
||||
print $product->getNomUrl(1);
|
||||
print '<br><span class="opacitymedium">'.$obj->product_label.'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Supplier
|
||||
print '<td>'.dol_escape_htmltag($obj->supplier_name).'</td>';
|
||||
|
||||
// Datanorm ref
|
||||
print '<td>'.dol_escape_htmltag($obj->datanorm_ref).'</td>';
|
||||
|
||||
// Field changed
|
||||
print '<td>';
|
||||
$field_label = '';
|
||||
switch ($obj->field_changed) {
|
||||
case 'price':
|
||||
$field_label = $langs->trans('Price');
|
||||
print '<i class="fas fa-euro-sign paddingright"></i>';
|
||||
break;
|
||||
case 'description':
|
||||
$field_label = $langs->trans('Description');
|
||||
print '<i class="fas fa-align-left paddingright"></i>';
|
||||
break;
|
||||
case 'label':
|
||||
$field_label = $langs->trans('Label');
|
||||
print '<i class="fas fa-tag paddingright"></i>';
|
||||
break;
|
||||
default:
|
||||
$field_label = $obj->field_changed;
|
||||
}
|
||||
print $field_label;
|
||||
print '</td>';
|
||||
|
||||
// Old value
|
||||
print '<td class="tdoverflowmax200" style="background-color: #fff3cd;">';
|
||||
if ($obj->field_changed == 'price') {
|
||||
print price($obj->old_value);
|
||||
} else {
|
||||
print dol_escape_htmltag(dol_trunc($obj->old_value, 100));
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// New value
|
||||
print '<td class="tdoverflowmax200" style="background-color: #d4edda;">';
|
||||
if ($obj->field_changed == 'price') {
|
||||
print price($obj->new_value);
|
||||
// Show difference
|
||||
$diff = $obj->new_value - $obj->old_value;
|
||||
if ($obj->old_value > 0) {
|
||||
$diff_percent = ($diff / $obj->old_value) * 100;
|
||||
print '<br>';
|
||||
if ($diff > 0) {
|
||||
print '<span style="color: #d9534f;">+'.number_format($diff_percent, 1).'%</span>';
|
||||
} else {
|
||||
print '<span style="color: #5cb85c;">'.number_format($diff_percent, 1).'%</span>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print dol_escape_htmltag(dol_trunc($obj->new_value, 100));
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// User
|
||||
print '<td>'.dol_escape_htmltag($obj->user_login).'</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
$i++;
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="8" class="opacitymedium center">'.$langs->trans('NoChangesRecorded').'</td></tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
$db->free($resql);
|
||||
} else {
|
||||
dol_print_error($db);
|
||||
}
|
||||
|
||||
// Export buttons
|
||||
if ($total > 0) {
|
||||
print '<br><div class="center">';
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=export&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc.'&date_start='.$date_start.'&date_end='.$date_end.'" class="button">';
|
||||
print '<i class="fas fa-download paddingright"></i>'.$langs->trans('Export');
|
||||
print '</a>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
File diff suppressed because it is too large
Load diff
215
docs/DATANORM_FORMAT.md
Normal file
215
docs/DATANORM_FORMAT.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Datanorm Format Dokumentation
|
||||
|
||||
## Allgemeines
|
||||
|
||||
Datanorm ist ein Dateiformat für den Datenaustausch von Artikelstammdaten zwischen Produktlieferant, Fachgroßhandel und Handwerksbetrieb. Es wird vornehmlich im Baunebengewerbe (Sanitär, Heizung, Elektro, Maler) verwendet.
|
||||
|
||||
**Wichtig:** Datanorm ist kein offener Standard. Die offizielle Spezifikation ist kostenpflichtig über den Krammer Verlag erhältlich.
|
||||
|
||||
## Datanorm Versionen
|
||||
|
||||
- **Datanorm 3.0**: Feste Feldbreiten (128 Zeichen pro Satz), ASCII
|
||||
- **Datanorm 4.0**: Semikolon-getrennte Felder, erweiterte Funktionen
|
||||
- **Datanorm 5.0**: XML-basiert
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
Eine Datanorm-Lieferung besteht aus mehreren Dateien:
|
||||
|
||||
| Datei | Inhalt |
|
||||
|-------|--------|
|
||||
| `DATANORM.001` - `.999` | Artikelstammdaten (A/B-Sätze) |
|
||||
| `DATPREIS.001` - `.999` | Preisdaten (P-Sätze) |
|
||||
| `DATANORM.WRG` | Warengruppen |
|
||||
| `DATANORM.RAB` | Rabattgruppen |
|
||||
|
||||
## Satzarten
|
||||
|
||||
| Kennzeichen | Typ | Beschreibung |
|
||||
|-------------|-----|--------------|
|
||||
| A | Artikelsatz | Stammdaten des Artikels |
|
||||
| B | Ergänzungssatz | Zusatzinfos, EAN, VPE, Langtext |
|
||||
| P | Preissatz | Preisinformationen |
|
||||
| T | Textsatz | Mehrzeilige Texte |
|
||||
| G | Grafiksatz | Bildverknüpfungen |
|
||||
|
||||
## A-Satz (Artikelstammdaten) - Datanorm 4.0 Semikolon-Format
|
||||
|
||||
### Sonepar-Format
|
||||
|
||||
```
|
||||
A;N;ArtNr;TextKz;Kurztext1;Kurztext2;PreisKz;PE;ME;Preis;RabGrp;WG;LangTextKey
|
||||
```
|
||||
|
||||
| Index | Feld | Beschreibung | Beispiel |
|
||||
|-------|------|--------------|----------|
|
||||
| 0 | Satzart | Immer "A" | `A` |
|
||||
| 1 | **Aktionscode** | **N=Neu, A=Ändern, L=Löschen** | `N` |
|
||||
| 2 | Artikelnummer | Eindeutige Nummer | `0480145` |
|
||||
| 3 | Textkennzeichen | Text-Typ | `00` |
|
||||
| 4 | Kurztext 1 | Erste Bezeichnung (max 40 Z.) | `OBO BETT. Verschraubung` |
|
||||
| 5 | Kurztext 2 | Zweite Bezeichnung (max 40 Z.) | `V-TEC PG21 LGR` |
|
||||
| 6 | Preiskennzeichen | 1=Brutto, 2=Netto | `1` |
|
||||
| 7 | **PE (Preiseinheit)** | **CODE** (siehe unten) | `2` |
|
||||
| 8 | ME (Mengeneinheit) | Einheit | `Stck` |
|
||||
| 9 | Preis | In Cent (wenn vorhanden) | `59085` |
|
||||
| 10 | Rabattgruppe | Rabatt-Code | `A12N` |
|
||||
| 11 | Warengruppe | Waren-Code | `303` |
|
||||
| 12 | Langtextschlüssel | Verknüpfung zu Texten | ` ` |
|
||||
|
||||
### Preiseinheit-Codes (PE) - WICHTIG!
|
||||
|
||||
**Die Preiseinheit ist ein CODE, nicht die tatsächliche Menge!**
|
||||
|
||||
| Code | Bedeutung | Divisor |
|
||||
|------|-----------|---------|
|
||||
| 0 (oder leer) | Preis pro 1 Stück | 1 |
|
||||
| 1 | Preis pro 10 Stück | 10 |
|
||||
| 2 | Preis pro 100 Stück | 100 |
|
||||
| 3 | Preis pro 1000 Stück | 1000 |
|
||||
|
||||
**Beispiel:**
|
||||
```
|
||||
A;N;0480145;00;OBO BETT. Verschraubung;V-TEC PG21 LGR;1;2;Stck;...
|
||||
↑
|
||||
PE-Code 2 = pro 100 Stück
|
||||
```
|
||||
|
||||
Wenn DATPREIS den Preis 9997 (= 99,97 €) liefert:
|
||||
- Stückpreis = 99,97 € / 100 = **0,9997 €**
|
||||
|
||||
### Aktionscode - Artikelstatus
|
||||
|
||||
Der Aktionscode gibt an, ob ein Artikel neu ist, geändert wurde oder nicht mehr verfügbar ist:
|
||||
|
||||
| Code | Bedeutung | Verhalten |
|
||||
|------|-----------|-----------|
|
||||
| N | Neu | Artikel wird angelegt |
|
||||
| A | Ändern | Artikel wird aktualisiert |
|
||||
| L | Löschen | Artikel wird als inaktiv markiert (`active=0`) |
|
||||
|
||||
**Wichtig:** Bei `L`-Artikeln wird das Feld `active` auf `0` gesetzt. Diese Artikel erscheinen nicht mehr in Suchergebnissen und können beim Massenupdate als "nicht mehr verfügbar" gekennzeichnet werden.
|
||||
|
||||
## B-Satz (Ergänzungssatz) - Sonepar-Format
|
||||
|
||||
```
|
||||
B;N;ArtNr;Matchcode; ; ;;;;EAN; ; ;0;VPE;;;
|
||||
```
|
||||
|
||||
| Index | Feld | Beschreibung |
|
||||
|-------|------|--------------|
|
||||
| 0 | Satzart | `B` |
|
||||
| 1 | Aktion | N/L/A |
|
||||
| 2 | Artikelnummer | Bezug zum A-Satz |
|
||||
| 3 | Matchcode | Suchbegriff |
|
||||
| 8 | EAN | 13-stellige EAN/GTIN |
|
||||
| 13 | VPE | Verpackungseinheit (tatsächliche Menge) |
|
||||
|
||||
**Hinweis:** Die VPE im B-Satz ist die Verpackungseinheit (z.B. 100 Stück pro Packung), während der PE-Code im A-Satz die Preisbasis definiert. Diese können unterschiedlich sein!
|
||||
|
||||
## P-Satz (Preissatz) - DATPREIS-Datei
|
||||
|
||||
### Format: Mehrere Artikel pro Zeile
|
||||
|
||||
```
|
||||
P;A;ArtNr1;PreisKz1;Preis1;x;Zuschlag1;x;x;x;ArtNr2;PreisKz2;Preis2;x;Zuschlag2;...
|
||||
```
|
||||
|
||||
| Index | Feld | Beschreibung |
|
||||
|-------|------|--------------|
|
||||
| 0 | P | Satzkennung |
|
||||
| 1 | A | Aktionskennung |
|
||||
| 2 | ArtNr | Artikelnummer |
|
||||
| 3 | PreisKz | Preiskennzeichen (2=Nettopreis) |
|
||||
| 4 | Preis | Materialpreis in **Cent** (für A-Satz PE-Einheit!) |
|
||||
| 5 | x | Unbekannt (immer 1 bei Sonepar) |
|
||||
| 6 | Zuschlag | **Metallzuschlag** in Cent (Kupfer/Aluminium) |
|
||||
| 7-10 | x | Weitere Felder (Flags) |
|
||||
|
||||
**Wichtig:** Der Preis in DATPREIS ist bereits für die PE-Einheit aus dem A-Satz angegeben! Keine Normalisierung nötig.
|
||||
|
||||
### Metallzuschlag (für Kabel)
|
||||
|
||||
Bei Kabeln und metallhaltigen Produkten gibt es oft zwei Preiskomponenten:
|
||||
- **Materialpreis** (Preis): Grundpreis des Produkts
|
||||
- **Metallzuschlag** (Zuschlag): Zusatzkosten für Kupfer/Aluminium
|
||||
|
||||
**Gesamtpreis = Materialpreis + Metallzuschlag**
|
||||
|
||||
**Beispiel Kabel NYM-J 5x1,5:**
|
||||
```
|
||||
P;A;0110350;2;2920;2;7629;0;1;0;...
|
||||
```
|
||||
- Materialpreis: 2920 Cent = 29,20 €/100m
|
||||
- Metallzuschlag: 7629 Cent = 76,29 €/100m
|
||||
- **Gesamtpreis: 105,49 €/100m = 1,05 €/m**
|
||||
|
||||
**Beispiel ohne Metallzuschlag:**
|
||||
```
|
||||
P;A;0480145;2;9997;1;0;1;0;1;0;0480146;2;20689;1;0;1;0;1;0;
|
||||
```
|
||||
- Artikel 0480145: Preis = 9997 Cent = 99,97 €
|
||||
|
||||
## Preisberechnung
|
||||
|
||||
### Formel für Stückpreis
|
||||
|
||||
```
|
||||
Stückpreis = Preis / PE_Divisor
|
||||
```
|
||||
|
||||
Wobei PE_Divisor aus dem PE-Code berechnet wird:
|
||||
- Code 0 → Divisor 1
|
||||
- Code 1 → Divisor 10
|
||||
- Code 2 → Divisor 100
|
||||
- Code 3 → Divisor 1000
|
||||
|
||||
### Beispiel
|
||||
|
||||
```
|
||||
Artikel: 0480145
|
||||
DATPREIS: 9997 (Cent) = 99,97 €
|
||||
A-Satz PE-Code: 2 → Divisor 100
|
||||
|
||||
Stückpreis = 99,97 € / 100 = 0,9997 €
|
||||
```
|
||||
|
||||
## Datenbankfelder
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|--------------|
|
||||
| `price` | DOUBLE | Materialpreis aus DATPREIS (in Euro) |
|
||||
| `price_unit` | INT | Konvertierter PE-Divisor (1, 10, 100, 1000) |
|
||||
| `price_unit_code` | TINYINT | Originaler PE-Code (0, 1, 2, 3) |
|
||||
| `price_type` | TINYINT | Preiskennzeichen (1=Brutto, 2=Netto) |
|
||||
| `metal_surcharge` | DOUBLE | Metallzuschlag (Kupfer/Aluminium) in Euro |
|
||||
| `vpe` | INT | VPE aus B-Satz (Verpackungseinheit) |
|
||||
| `action_code` | CHAR(1) | Aktionscode (N=Neu, A=Ändern, L=Löschen) |
|
||||
| `active` | TINYINT | Artikelstatus (1=aktiv, 0=gelöscht bei L) |
|
||||
|
||||
### Preisberechnung mit Metallzuschlag
|
||||
|
||||
```
|
||||
Gesamtpreis = price + metal_surcharge
|
||||
Stückpreis = Gesamtpreis / price_unit
|
||||
```
|
||||
|
||||
**Beispiel:**
|
||||
```
|
||||
price = 29.20 €
|
||||
metal_surcharge = 76.29 €
|
||||
price_unit = 100
|
||||
|
||||
Gesamtpreis = 29.20 + 76.29 = 105.49 €
|
||||
Stückpreis = 105.49 / 100 = 1.0549 €
|
||||
```
|
||||
|
||||
## Quellen
|
||||
|
||||
- [Datanorm Wikipedia](https://de.wikipedia.org/wiki/Datanorm)
|
||||
- [DATANORM.de](https://www.datanorm.de/)
|
||||
- [Comtech Hilfe](https://hilfe.comtech.at/ce/773/html/datanorm_datei.htm)
|
||||
|
||||
## Hinweis
|
||||
|
||||
Diese Dokumentation basiert auf der Analyse von Sonepar-Datanorm-Dateien und öffentlich verfügbaren Informationen. Für die vollständige offizielle Spezifikation wird das Datanorm-Taschenbuch vom Krammer Verlag empfohlen.
|
||||
|
|
@ -778,8 +778,9 @@ if ($action == 'createinvoice' && $id > 0) {
|
|||
$line->quantity,
|
||||
$line->fk_product,
|
||||
0, '', '',
|
||||
0, 0, '',
|
||||
'HT'
|
||||
0, 0,
|
||||
'HT', // price_base_type - Netto-Preise aus ZUGFeRD
|
||||
0 // type (0=product)
|
||||
);
|
||||
if ($res < 0) {
|
||||
$error++;
|
||||
|
|
|
|||
|
|
@ -344,3 +344,82 @@ NotifyTestSuccess = Die E-Mail-Konfiguration funktioniert einwandfrei!
|
|||
CurrentSettings = Aktuelle Einstellungen
|
||||
NotificationsNotEnabled = Benachrichtigungen sind nicht aktiviert oder keine E-Mail-Adresse konfiguriert
|
||||
NotifyEmail = Empfänger-E-Mail
|
||||
|
||||
#
|
||||
# Datanorm Massenaktualisierung
|
||||
#
|
||||
DatanormMassUpdate = Datanorm Massenaktualisierung
|
||||
SelectSupplier = Lieferant auswählen
|
||||
SelectASupplier = -- Lieferant wählen --
|
||||
SearchMode = Suchmodus
|
||||
SearchBySupplierProducts = Nach Lieferanten-Produkten suchen
|
||||
ManualSearch = Manuelle Suche
|
||||
SearchTerm = Suchbegriff
|
||||
ArticleNumberOrName = Artikelnummer oder Name
|
||||
AdditionalSearchOptions = Zusätzliche Suchoptionen
|
||||
AlsoSearchByName = Auch nach Name suchen
|
||||
AlsoSearchByEAN = Auch nach EAN suchen
|
||||
AlsoSearchByRef = Auch nach Artikelref. suchen
|
||||
FieldsToCompare = Felder zum Vergleichen
|
||||
OnlyShowDifferences = Nur Unterschiede anzeigen
|
||||
CurrentPrice = Aktueller Preis
|
||||
DatanormPrice = Datanorm Preis
|
||||
CurrentDescription = Aktuelle Beschreibung
|
||||
DatanormDescription = Datanorm Beschreibung
|
||||
CurrentLabel = Aktueller Name
|
||||
DatanormLabel = Datanorm Name
|
||||
DatanormArticle = Datanorm Artikel
|
||||
ProductNotInDatabase = Produkt nicht in Datenbank
|
||||
ApplyChanges = Änderungen übernehmen
|
||||
AddToPending = Zur Liste hinzufügen
|
||||
Pending = Ausstehend
|
||||
PendingChanges = Ausstehende Änderungen
|
||||
NoChanges = Keine Änderungen
|
||||
ApplyAllPendingChanges = Alle ausstehenden Änderungen übernehmen
|
||||
ClearPendingChanges = Ausstehende Änderungen löschen
|
||||
AddedToPendingChanges = Zur Liste hinzugefügt
|
||||
PendingChangesCleared = Ausstehende Änderungen gelöscht
|
||||
ConfirmMassUpdate = Massenaktualisierung bestätigen
|
||||
FollowingProductsWillBeUpdated = Folgende Produkte werden aktualisiert
|
||||
Changes = Änderungen
|
||||
DatanormMassUpdateComplete = Massenaktualisierung abgeschlossen: %s erfolgreich, %s Fehler
|
||||
ProductUpdated = Produkt aktualisiert
|
||||
ErrorUpdatingProduct = Fehler beim Aktualisieren des Produkts
|
||||
NoResultsFound = Keine Ergebnisse gefunden
|
||||
Results = Ergebnisse
|
||||
WithDifferences = mit Unterschieden
|
||||
OnlyShowingDifferences = Nur Unterschiede werden angezeigt
|
||||
|
||||
#
|
||||
# Änderungsprotokoll
|
||||
#
|
||||
DatanormChangeLog = Änderungsprotokoll
|
||||
ChangeHistory = Änderungsverlauf
|
||||
FieldChanged = Geändertes Feld
|
||||
OldValue = Alter Wert
|
||||
NewValue = Neuer Wert
|
||||
DateChange = Änderungsdatum
|
||||
ChangedBy = Geändert von
|
||||
BatchUpdate = Stapelaktualisierung
|
||||
ViewChangeLog = Änderungsprotokoll anzeigen
|
||||
NoChangesRecorded = Keine Änderungen protokolliert
|
||||
PriceChange = Preisänderung
|
||||
DescriptionChange = Beschreibungsänderung
|
||||
LabelChange = Namensänderung
|
||||
|
||||
#
|
||||
# Kupferzuschlag / Metallzuschlag
|
||||
#
|
||||
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)
|
||||
MetalSurchargeDetected = Metallzuschlag erkannt
|
||||
MetalSurchargeUpdated = Kupferzuschlag aktualisiert auf %s €/Einheit
|
||||
AddAllWithDifferences = Alle mit Unterschieden hinzufügen
|
||||
AddedAllToPendingChanges = %s Produkte zur Aktualisierungsliste hinzugefügt
|
||||
ConfirmAddAllToPending = Alle Produkte mit Unterschieden zur Aktualisierungsliste hinzufügen?
|
||||
Kupfergehalt = Kupfergehalt (kg/km)
|
||||
KupfergehaltHelp = Kupfergewicht pro Kilometer Kabel (konstant je Kabeltyp)
|
||||
CopperSurchargeFromInvoice = Kupferzuschlag aus Rechnung
|
||||
CopperSurchargePerUnit = Kupferzuschlag/Einheit
|
||||
|
|
|
|||
|
|
@ -344,3 +344,13 @@ NotifyTestSuccess = The email configuration is working properly!
|
|||
CurrentSettings = Current settings
|
||||
NotificationsNotEnabled = Notifications are not enabled or no email address configured
|
||||
NotifyEmail = Recipient email
|
||||
|
||||
#
|
||||
# Metal Surcharge
|
||||
#
|
||||
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)
|
||||
MetalSurchargeDetected = Metal surcharge detected
|
||||
MetalSurchargeUpdated = Metal surcharge updated to %s €/unit
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@ CREATE TABLE llx_importzugferd_datanorm (
|
|||
manufacturer_ref varchar(128), -- Hersteller-Artikelnummer (Typ A Feld 15)
|
||||
manufacturer_name varchar(128), -- Herstellername (Typ A Feld 16)
|
||||
unit_code varchar(8), -- Mengeneinheit (Typ A Feld 6)
|
||||
price double(24,8) DEFAULT 0, -- Listenpreis (Typ P)
|
||||
price_unit integer DEFAULT 1, -- Preiseinheit (Stück pro Preis)
|
||||
price double(24,8) DEFAULT 0, -- Listenpreis/Materialpreis (Typ P)
|
||||
price_unit integer DEFAULT 1, -- Preiseinheit (Stück pro Preis) - konvertiert aus PE-Code
|
||||
price_unit_code tinyint DEFAULT 0, -- Original PE-Code (0=1, 1=10, 2=100, 3=1000)
|
||||
price_type tinyint DEFAULT 1, -- Preiskennzeichen (1=Brutto, 2=Netto)
|
||||
metal_surcharge double(24,8) DEFAULT 0, -- Metallzuschlag/Kupferzuschlag (Typ P)
|
||||
vpe integer DEFAULT NULL, -- VPE aus B-Satz (Verpackungseinheit)
|
||||
discount_group varchar(32), -- Rabattgruppe (Typ A Feld 8)
|
||||
product_group varchar(64), -- Warengruppe (Typ A Feld 9)
|
||||
alt_unit varchar(8), -- Alternative Mengeneinheit
|
||||
|
|
@ -24,8 +28,9 @@ CREATE TABLE llx_importzugferd_datanorm (
|
|||
weight double(10,4), -- Gewicht in kg
|
||||
matchcode varchar(128), -- Matchcode für Suche (Typ A Feld 3)
|
||||
datanorm_version varchar(8), -- Datanorm Version (4.0, 5.0)
|
||||
action_code char(1) DEFAULT 'N', -- Aktionscode (N=Neu, A=Ändern, L=Löschen)
|
||||
import_date datetime NOT NULL, -- Importzeitpunkt
|
||||
active tinyint DEFAULT 1, -- Aktiv/Inaktiv
|
||||
active tinyint DEFAULT 1, -- Aktiv/Inaktiv (0 bei action_code='L')
|
||||
date_creation datetime NOT NULL,
|
||||
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
fk_user_creat integer,
|
||||
|
|
|
|||
8
sql/llx_importzugferd_datanorm_log.key.sql
Normal file
8
sql/llx_importzugferd_datanorm_log.key.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- 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.
|
||||
|
||||
-- No additional keys needed, all defined in main SQL file
|
||||
25
sql/llx_importzugferd_datanorm_log.sql
Normal file
25
sql/llx_importzugferd_datanorm_log.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- 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.
|
||||
|
||||
CREATE TABLE llx_importzugferd_datanorm_log (
|
||||
rowid integer AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_product integer NOT NULL,
|
||||
fk_soc integer NOT NULL,
|
||||
fk_user integer NOT NULL,
|
||||
datanorm_ref varchar(100),
|
||||
field_changed varchar(50) NOT NULL,
|
||||
old_value text,
|
||||
new_value text,
|
||||
date_change datetime NOT NULL,
|
||||
batch_id varchar(50),
|
||||
entity integer DEFAULT 1 NOT NULL
|
||||
) ENGINE=innodb;
|
||||
|
||||
ALTER TABLE llx_importzugferd_datanorm_log ADD INDEX idx_datanorm_log_product (fk_product);
|
||||
ALTER TABLE llx_importzugferd_datanorm_log ADD INDEX idx_datanorm_log_soc (fk_soc);
|
||||
ALTER TABLE llx_importzugferd_datanorm_log ADD INDEX idx_datanorm_log_date (date_change);
|
||||
ALTER TABLE llx_importzugferd_datanorm_log ADD INDEX idx_datanorm_log_batch (batch_id);
|
||||
Loading…
Reference in a new issue