diff --git a/CHANGELOG.md b/CHANGELOG.md
old mode 100644
new mode 100755
index 70ce32b..da68b11
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,25 @@
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
+## [3.8] - 2026-02-25
+
+### Hinzugefügt
+- **Kabel-Preisberechnung**: Zentrale Funktion `calculateCablePricing()` für einheitliche Preislogik
+- **Kupfergehalt-Berechnung**: Automatische Berechnung aus Aderanzahl × Querschnitt × 8.9
+- **Ringgröße-Erkennung**: Unterstützt Ri100, Tr500, Fol.25m, "Ring 100m", "Trommel 500m"
+- **Extrafield "produktpreis"**: Speichert reinen Materialpreis ohne Kupferzuschlag (nur Kabel)
+- **EAN-Auto-Update**: Barcodes aus ZUGFeRD-Rechnungen werden automatisch in Lieferantenpreise übernommen
+
+### Verbessert
+- **Lieferanten-Formate**: Korrekte Unterscheidung zwischen Sonepar (price_unit=1, Ring im Namen) und Kluxen/Witte (price_unit=100)
+- **Cross-Catalog-Suche**: Nur noch über EAN, nicht mehr über Artikelnummern (verhindert Fehlzuordnungen)
+- **EAN-Barcode-Typ**: Automatische Erkennung (EAN8, EAN13, UPC-A) statt hardcoded EAN13
+- **Error-Handling**: Besseres Logging bei Extrafield-Fehlern
+
+### Behoben
+- Division durch Null bei Preisberechnung abgesichert
+- Mindestbestellmenge und Verpackungseinheit werden von existierenden Lieferantenpreisen übernommen
+
## [3.7] - 2026-02-23
### Hinzugefügt
diff --git a/README.md b/README.md
index 19f0cd5..c051d58 100755
--- a/README.md
+++ b/README.md
@@ -102,23 +102,29 @@ Available in:
## Version History
-### 1.1
-- New persistent import workflow with database storage
-- Manual product assignment via dropdown
-- Product removal/reassignment
-- Status "Pending" for imports requiring manual intervention
-- Pending imports overview on upload page
-- UN/ECE unit code translation (C62 → Stk., MTR → m, etc.)
-- Batch import from folder or IMAP mailbox
-- IMAP connection test with folder selection
-- Product template feature (duplicate existing product)
+See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
+
+### 3.8 (Current)
+- Improved cable pricing for different supplier formats (Sonepar vs Kluxen/Witte)
+- Automatic ring size detection from product names (Ri100, Tr500, etc.)
+- EAN auto-update from ZUGFeRD invoices with automatic barcode type detection
+- New extrafield "produktpreis" for material price without copper surcharge
+- Cross-catalog search now EAN-only (prevents mismatches)
+
+### 3.7
+- GlobalNotify integration for import notifications
+
+### 3.6
+- Cron job stability fixes and dedicated logging
+
+### 3.5
+- Automatic cron import from watch folder and IMAP
+
+### 3.0
+- Datanorm integration for article prices
### 1.0
- Initial release
-- Basic ZUGFeRD/Factur-X import
-- Automatic product matching
-- Supplier detection
-- Duplicate detection
## License
diff --git a/class/actions_importzugferd.class.php b/class/actions_importzugferd.class.php
index 8243f7d..fb77881 100755
--- a/class/actions_importzugferd.class.php
+++ b/class/actions_importzugferd.class.php
@@ -316,6 +316,32 @@ class ActionsImportZugferd
$processed_line['product_ref'] = $product->ref;
$processed_line['product_label'] = $product->label;
}
+
+ // Update supplier price with EAN from invoice if empty
+ $invoiceEan = !empty($line['product']['global_id']) ? trim($line['product']['global_id']) : '';
+ $supplierRef = !empty($line['product']['seller_id']) ? $line['product']['seller_id'] : '';
+ if (!empty($invoiceEan) && !empty($supplierRef) && ctype_digit($invoiceEan)) {
+ // Barcode-Typ basierend auf Länge bestimmen
+ $eanLen = strlen($invoiceEan);
+ if ($eanLen == 13) {
+ $barcodeType = 2; // EAN13
+ } elseif ($eanLen == 8) {
+ $barcodeType = 1; // EAN8
+ } elseif ($eanLen == 12) {
+ $barcodeType = 3; // UPC-A
+ } else {
+ $barcodeType = 0; // Unbekannt
+ }
+
+ $sqlEan = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
+ $sqlEan .= " SET barcode = '" . $this->db->escape($invoiceEan) . "'";
+ $sqlEan .= ", fk_barcode_type = " . (int)$barcodeType;
+ $sqlEan .= " WHERE fk_product = " . (int)$match['fk_product'];
+ $sqlEan .= " AND fk_soc = " . (int)$supplier_id;
+ $sqlEan .= " AND ref_fourn = '" . $this->db->escape($supplierRef) . "'";
+ $sqlEan .= " AND (barcode IS NULL OR barcode = '')";
+ $this->db->query($sqlEan);
+ }
} else {
$processed_line['needs_creation'] = true;
}
diff --git a/class/datanorm.class.php b/class/datanorm.class.php
index 28806f6..50c1148 100755
--- a/class/datanorm.class.php
+++ b/class/datanorm.class.php
@@ -520,9 +520,22 @@ class Datanorm extends CommonObject
if ($result > 0) {
$results[] = $this->toArray();
$foundIds[$this->id] = true;
- // Store EAN and manufacturer_ref for cross-catalog search
+ // Store EAN from Datanorm
$foundEan = $this->ean;
- $foundManufacturerRef = $this->manufacturer_ref;
+
+ // If Datanorm has no EAN, try to get it from supplier price (barcode field)
+ if (empty($foundEan)) {
+ $sqlEan = "SELECT barcode FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
+ $sqlEan .= " WHERE fk_soc = " . (int)$fk_soc;
+ $sqlEan .= " AND ref_fourn = '" . $this->db->escape($article_number) . "'";
+ $sqlEan .= " AND barcode IS NOT NULL AND barcode != ''";
+ $sqlEan .= " LIMIT 1";
+ $resEan = $this->db->query($sqlEan);
+ if ($resEan && $this->db->num_rows($resEan) > 0) {
+ $objEan = $this->db->fetch_object($resEan);
+ $foundEan = $objEan->barcode;
+ }
+ }
// If not searching all catalogs, return immediately
if (!$searchAll) {
@@ -531,24 +544,15 @@ class Datanorm extends CommonObject
}
}
- // If searchAll is enabled and we found article with EAN/manufacturer_ref,
- // search other catalogs using these identifiers (cross-catalog search)
- if ($searchAll && $fk_soc > 0 && (!empty($foundEan) || !empty($foundManufacturerRef))) {
+ // If searchAll is enabled and we found article with EAN,
+ // search other catalogs using EAN ONLY (cross-catalog search)
+ // Note: Artikelnummern-Vergleich macht keinen Sinn über Kataloge hinweg
+ if ($searchAll && $fk_soc > 0 && !empty($foundEan)) {
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
$sql .= " price, price_unit, discount_group, product_group, matchcode";
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
- $sql .= " WHERE (";
-
- $conditions = array();
- if (!empty($foundEan)) {
- $conditions[] = "ean = '" . $this->db->escape($foundEan) . "'";
- }
- if (!empty($foundManufacturerRef)) {
- $conditions[] = "manufacturer_ref = '" . $this->db->escape($foundManufacturerRef) . "'";
- }
- $sql .= implode(' OR ', $conditions) . ")";
-
+ $sql .= " WHERE ean = '" . $this->db->escape($foundEan) . "'";
$sql .= " AND active = 1";
$sql .= " AND entity = " . (int) $conf->entity;
$sql .= " AND fk_soc != " . (int) $fk_soc; // Exclude already found supplier
@@ -588,55 +592,44 @@ class Datanorm extends CommonObject
}
}
- // Fallback: Search by partial match on article_number, ean, or manufacturer_ref
- $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
- $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
- $sql .= " price, price_unit, discount_group, product_group, matchcode";
- $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
- $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) . "%')";
- $sql .= " AND active = 1";
- $sql .= " AND entity = " . (int) $conf->entity;
-
- if ($fk_soc > 0 && !$searchAll) {
+ // Fallback: Search by EXACT article number match for the specified supplier only
+ // No LIKE search - cross-catalog comparisons only work via EAN
+ if ($fk_soc > 0 && empty($results)) {
+ $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
+ $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
+ $sql .= " price, price_unit, discount_group, product_group, matchcode";
+ $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
+ $sql .= " WHERE article_number = '" . $this->db->escape($article_number) . "'";
$sql .= " AND fk_soc = " . (int) $fk_soc;
- }
+ $sql .= " AND active = 1";
+ $sql .= " AND entity = " . (int) $conf->entity;
+ $sql .= " LIMIT 1";
- // ORDER BY clause
- if ($fk_soc > 0 && $searchAll) {
- // Order by matching supplier first, then by price
- $sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, price ASC";
- } else {
- $sql .= " ORDER BY article_number";
- }
-
- $sql .= " LIMIT " . (int) $limit;
-
- $resql = $this->db->query($sql);
- if ($resql) {
- while ($obj = $this->db->fetch_object($resql)) {
- if (!isset($foundIds[$obj->rowid])) {
- $results[] = array(
- 'id' => $obj->rowid,
- 'fk_soc' => $obj->fk_soc,
- 'article_number' => $obj->article_number,
- 'short_text1' => $obj->short_text1,
- 'short_text2' => $obj->short_text2,
- 'ean' => $obj->ean,
- 'manufacturer_ref' => $obj->manufacturer_ref,
- 'manufacturer_name' => $obj->manufacturer_name,
- 'unit_code' => $obj->unit_code,
- 'price' => $obj->price,
- 'price_unit' => $obj->price_unit,
- 'discount_group' => $obj->discount_group,
- 'product_group' => $obj->product_group,
- 'matchcode' => $obj->matchcode,
- );
- $foundIds[$obj->rowid] = true;
+ $resql = $this->db->query($sql);
+ if ($resql) {
+ while ($obj = $this->db->fetch_object($resql)) {
+ if (!isset($foundIds[$obj->rowid])) {
+ $results[] = array(
+ 'id' => $obj->rowid,
+ 'fk_soc' => $obj->fk_soc,
+ 'article_number' => $obj->article_number,
+ 'short_text1' => $obj->short_text1,
+ 'short_text2' => $obj->short_text2,
+ 'ean' => $obj->ean,
+ 'manufacturer_ref' => $obj->manufacturer_ref,
+ 'manufacturer_name' => $obj->manufacturer_name,
+ 'unit_code' => $obj->unit_code,
+ 'price' => $obj->price,
+ 'price_unit' => $obj->price_unit,
+ 'discount_group' => $obj->discount_group,
+ 'product_group' => $obj->product_group,
+ 'matchcode' => $obj->matchcode,
+ );
+ $foundIds[$obj->rowid] = true;
+ }
}
+ $this->db->free($resql);
}
- $this->db->free($resql);
}
return $results;
diff --git a/class/zugferdimport.class.php b/class/zugferdimport.class.php
index 100e849..bfdda55 100755
--- a/class/zugferdimport.class.php
+++ b/class/zugferdimport.class.php
@@ -624,6 +624,32 @@ class ZugferdImport extends CommonObject
if (!empty($match) && $match['fk_product'] > 0) {
$fk_product = $match['fk_product'];
$match_method = $match['method'];
+
+ // Update supplier price with EAN from invoice if empty
+ $invoiceEan = !empty($line_data['product']['global_id']) ? trim($line_data['product']['global_id']) : '';
+ $supplierRef = !empty($line_data['product']['seller_id']) ? $line_data['product']['seller_id'] : '';
+ if (!empty($invoiceEan) && !empty($supplierRef) && ctype_digit($invoiceEan)) {
+ // Barcode-Typ basierend auf Länge bestimmen
+ $eanLen = strlen($invoiceEan);
+ if ($eanLen == 13) {
+ $barcodeType = 2; // EAN13
+ } elseif ($eanLen == 8) {
+ $barcodeType = 1; // EAN8
+ } elseif ($eanLen == 12) {
+ $barcodeType = 3; // UPC-A
+ } else {
+ $barcodeType = 0; // Unbekannt
+ }
+
+ $sqlEan = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
+ $sqlEan .= " SET barcode = '" . $this->db->escape($invoiceEan) . "'";
+ $sqlEan .= ", fk_barcode_type = " . (int)$barcodeType;
+ $sqlEan .= " WHERE fk_product = " . (int)$fk_product;
+ $sqlEan .= " AND fk_soc = " . (int)$supplier_id;
+ $sqlEan .= " AND ref_fourn = '" . $this->db->escape($supplierRef) . "'";
+ $sqlEan .= " AND (barcode IS NULL OR barcode = '')";
+ $this->db->query($sqlEan);
+ }
}
}
diff --git a/core/modules/modImportZugferd.class.php b/core/modules/modImportZugferd.class.php
index 836585c..bc1da66 100755
--- a/core/modules/modImportZugferd.class.php
+++ b/core/modules/modImportZugferd.class.php
@@ -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 = '3.7';
+ $this->version = '3.8';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
@@ -616,6 +616,28 @@ class modImportZugferd extends DolibarrModules
'isModEnabled("importzugferd")' // enabled condition
);
+ // Add extrafield for product price without copper surcharge (only for cables)
+ $extrafields->addExtraField(
+ 'produktpreis', // attribute code
+ 'Produktpreis', // label (translation key)
+ 'price', // type (price field)
+ 115, // 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
diff --git a/import.php b/import.php
index 3cf83be..39ced3f 100755
--- a/import.php
+++ b/import.php
@@ -100,6 +100,241 @@ $message = '';
* Helper-Funktionen (DRY)
*/
+/**
+ * Parse Aderanzahl und Querschnitt aus Kabelbezeichnung
+ * Erkennt Formate wie: NYM-J 3x2,5 / NYM-J 5x1.5 / H07V-K 1x4 / J-Y(ST)Y 2x2x0,8 etc.
+ *
+ * @param string $text Kabelbezeichnung (z.B. "NYM-J 3x2,5 Eca Ri100")
+ * @return array|null Array mit 'aderanzahl', 'querschnitt' oder null wenn kein Kabel
+ */
+function parseCableSpecsFromText($text)
+{
+ // Spezialfall: Fernmeldekabel wie J-Y(ST)Y 2x2x0,8 (Paare x Adern pro Paar x Querschnitt)
+ // Pattern: Zahl x Zahl x Zahl (z.B. 2x2x0,8 = 4 Adern mit 0,8mm²)
+ if (preg_match('/(\d+)\s*[xX]\s*(\d+)\s*[xX]\s*(\d+(?:[,\.]\d+)?)/', $text, $matches)) {
+ $paare = (int) $matches[1];
+ $adernProPaar = (int) $matches[2];
+ $querschnitt = (float) str_replace(',', '.', $matches[3]);
+ $aderanzahl = $paare * $adernProPaar;
+
+ // Plausibilitätsprüfung
+ if ($aderanzahl >= 1 && $aderanzahl <= 200 && $querschnitt >= 0.14 && $querschnitt <= 400) {
+ return array(
+ 'aderanzahl' => $aderanzahl,
+ 'querschnitt' => $querschnitt
+ );
+ }
+ }
+
+ // Standard: NYM-J 3x2,5 (Adern x Querschnitt)
+ // Pattern: Zahl x Zahl (mit Komma oder Punkt als Dezimaltrenner)
+ if (preg_match('/(\d+)\s*[xX]\s*(\d+(?:[,\.]\d+)?)/', $text, $matches)) {
+ $aderanzahl = (int) $matches[1];
+ $querschnitt = (float) str_replace(',', '.', $matches[2]);
+
+ // Plausibilitätsprüfung
+ if ($aderanzahl >= 1 && $aderanzahl <= 100 && $querschnitt >= 0.5 && $querschnitt <= 400) {
+ return array(
+ 'aderanzahl' => $aderanzahl,
+ 'querschnitt' => $querschnitt
+ );
+ }
+ }
+ return null;
+}
+
+/**
+ * Berechne Kupfergehalt aus Aderanzahl und Querschnitt
+ * Formel: Aderanzahl × Querschnitt × 8.9 (Dichte Kupfer) = kg/km
+ *
+ * @param int $aderanzahl Anzahl der Adern
+ * @param float $querschnitt Querschnitt in mm²
+ * @return float Kupfergehalt in kg/km
+ */
+function calculateKupfergehalt($aderanzahl, $querschnitt)
+{
+ // Kupferdichte: 8.9 g/cm³ = 8.9 kg/dm³
+ // 1 mm² × 1 km = 1 mm² × 1000m = 1000 mm³ = 1 cm³
+ // Also: 1 mm² Querschnitt × 1 km Länge = 1000 cm³ = 1 dm³ = 8.9 kg
+ return $aderanzahl * $querschnitt * 8.9;
+}
+
+/**
+ * Hole aktuellen Kupferpreis aus Metallzuschlag-Modul
+ *
+ * @param DoliDB $db Datenbank
+ * @param int $supplierId Lieferanten-ID (optional, für lieferantenspezifischen Preis)
+ * @return float CU-Notiz in EUR/100kg oder 0 wenn nicht verfügbar
+ */
+function getCurrentCopperPrice($db, $supplierId = 0)
+{
+ // Erst prüfen ob Metallzuschlag-Modul aktiv ist
+ if (!isModEnabled('metallzuschlag')) {
+ return 0;
+ }
+
+ // Lieferanten-spezifischer CU-Wert (aus societe_extrafields)
+ if ($supplierId > 0) {
+ $sql = "SELECT metallzuschlag_cu FROM ".MAIN_DB_PREFIX."societe_extrafields";
+ $sql .= " WHERE fk_object = ".(int)$supplierId;
+ $resql = $db->query($sql);
+ if ($resql && $db->num_rows($resql) > 0) {
+ $obj = $db->fetch_object($resql);
+ if (!empty($obj->metallzuschlag_cu) && (float)$obj->metallzuschlag_cu > 0) {
+ return (float)$obj->metallzuschlag_cu;
+ }
+ }
+ }
+
+ // Fallback: Aktuellster CU-Wert aus History
+ $sql = "SELECT value FROM ".MAIN_DB_PREFIX."metallzuschlag_history";
+ $sql .= " WHERE metal = 'CU' ORDER BY date_notiz DESC LIMIT 1";
+ $resql = $db->query($sql);
+ if ($resql && $db->num_rows($resql) > 0) {
+ $obj = $db->fetch_object($resql);
+ return (float)$obj->value;
+ }
+
+ return 0;
+}
+
+/**
+ * Berechne Kupferzuschlag für eine bestimmte Menge
+ * Formel: Kupfergehalt (kg/km) × CU (EUR/100kg) / 100000 × Menge
+ *
+ * @param float $kupfergehalt Kupfergehalt in kg/km
+ * @param float $cuPrice CU-Notiz in EUR/100kg
+ * @param float $quantity Menge (z.B. 100 für 100m)
+ * @return float Kupferzuschlag in EUR
+ */
+function calculateKupferzuschlag($kupfergehalt, $cuPrice, $quantity = 1)
+{
+ if ($kupfergehalt <= 0 || $cuPrice <= 0) {
+ return 0;
+ }
+ // kg/km × EUR/100kg / 100000 × m = EUR
+ return round($kupfergehalt * $cuPrice / 100000 * $quantity, 2);
+}
+
+/**
+ * Prüft ob ein Produkt ein Kabel ist (basierend auf Warengruppe oder Bezeichnung)
+ *
+ * @param Datanorm $datanorm Datanorm-Objekt
+ * @return bool True wenn Kabel
+ */
+function isCableProduct($datanorm)
+{
+ // Warengruppen die typisch für Kabel sind
+ $cableGroups = array('KAB', 'KABEL', 'LEI', 'LEIT', 'LEITUNG');
+
+ if (!empty($datanorm->product_group)) {
+ $group = strtoupper(substr($datanorm->product_group, 0, 5));
+ foreach ($cableGroups as $cg) {
+ if (strpos($group, $cg) !== false) {
+ return true;
+ }
+ }
+ }
+
+ // Typische Kabelbezeichnungen
+ $cablePatterns = array(
+ '/NYM[-\s]?[JYOA]/i',
+ '/NYY[-\s]?[JO]/i',
+ '/H0[357]V[-\s]?[KUR]/i',
+ '/H0[357]RN[-\s]?F/i',
+ '/NHXH/i',
+ '/J[-\s]?Y\(ST\)Y/i',
+ '/LiYCY/i',
+ '/ÖLFLEX/i',
+ );
+
+ $text = $datanorm->short_text1 . ' ' . $datanorm->short_text2;
+ foreach ($cablePatterns as $pattern) {
+ if (preg_match($pattern, $text)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Ringgröße aus Kabel-Bezeichnung extrahieren
+ * Erkennt Muster wie: Ri100, Ri.50, Ri 100, Ring100, Tr500, Fol.25m, "Ring 100m", "Trommel 500m"
+ *
+ * WICHTIG: Nur verwenden wenn price_unit = 1!
+ * Bei price_unit > 1 ist das bereits die korrekte Preiseinheit (z.B. 100 für 100m)
+ *
+ * @param string $text Produktbezeichnung
+ * @return int Ringgröße in Metern (0 wenn nicht gefunden)
+ */
+function extractCableRingSize($text)
+{
+ // Muster für Ringgröße: Ri100, Ri.50, Ri 100, Ring100, Ring 50
+ if (preg_match('/Ri(?:ng)?[.\s]?(\d+)/i', $text, $matches)) {
+ return (int)$matches[1];
+ }
+ // Muster für "Ring 100m", "Ring 50 m"
+ if (preg_match('/Ring\s+(\d+)\s*m/i', $text, $matches)) {
+ return (int)$matches[1];
+ }
+ // Muster für Trommel: Tr500, Tr.500, Trommel500, "Trommel 500m"
+ if (preg_match('/Tr(?:ommel)?[.\s]?(\d+)/i', $text, $matches)) {
+ return (int)$matches[1];
+ }
+ if (preg_match('/Trommel\s+(\d+)\s*m/i', $text, $matches)) {
+ return (int)$matches[1];
+ }
+ // Muster für Folie/Rolle: Fol.25m, Fol25, Rol.50m
+ if (preg_match('/(?:Fol|Rol)[.\s]?(\d+)/i', $text, $matches)) {
+ return (int)$matches[1];
+ }
+ return 0;
+}
+
+/**
+ * Berechne Kabelpreis unter Berücksichtigung unterschiedlicher Lieferanten-Formate
+ *
+ * Logik:
+ * - Kluxen/Witte/eltric: price_unit > 1 (z.B. 100) → Preis ist für 100m
+ * - Sonepar: price_unit = 1 → Preis ist für kompletten Ring (Größe aus Name)
+ *
+ * @param Datanorm $datanorm Datanorm-Objekt
+ * @param float $minQty Mindestbestellmenge (default 1)
+ * @return array Array mit 'unitPrice', 'totalPrice', 'priceUnit'
+ */
+function calculateCablePricing($datanorm, $minQty = 1)
+{
+ $priceUnit = $datanorm->price_unit > 0 ? $datanorm->price_unit : 1;
+ $cableText = $datanorm->short_text1 . ' ' . $datanorm->short_text2;
+
+ if ($priceUnit > 1) {
+ // Kluxen/Witte-Format: price_unit gibt die Preiseinheit an (z.B. 100m)
+ $unitPrice = $datanorm->price / $priceUnit;
+ $effectivePriceUnit = $priceUnit;
+ } else {
+ // Sonepar-Format: price_unit = 1, aber Preis ist für kompletten Ring
+ $ringSize = extractCableRingSize($cableText);
+ if ($ringSize > 0) {
+ $unitPrice = $datanorm->price / $ringSize;
+ $effectivePriceUnit = $ringSize;
+ } else {
+ // Einzelstück
+ $unitPrice = $datanorm->price;
+ $effectivePriceUnit = 1;
+ }
+ }
+
+ // Schutz gegen Division durch Null
+ $effectivePriceUnit = max(1, $effectivePriceUnit);
+
+ return array(
+ 'unitPrice' => $unitPrice,
+ 'totalPrice' => $unitPrice * $minQty,
+ 'priceUnit' => $effectivePriceUnit
+ );
+}
+
/**
* Extrafields fuer Lieferantenpreis aus Datanorm-Daten zusammenstellen
*
@@ -110,12 +345,13 @@ $message = '';
function datanormBuildSupplierPriceExtrafields($datanorm, $lineObj = null)
{
$extrafields = array();
- // Kupferzuschlag
- if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
- $extrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge;
- } elseif ($lineObj && !empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) {
- $extrafields['options_kupferzuschlag'] = $lineObj->copper_surcharge;
+
+ // Produktpreis (reiner Materialpreis ohne Kupferzuschlag) - nur bei Kabeln mit Metallzuschlag
+ // Der Preis ist bereits auf Mindestmenge (price_unit) bezogen
+ if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0 && !empty($datanorm->price)) {
+ $extrafields['options_produktpreis'] = $datanorm->price;
}
+
// Preiseinheit
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
$extrafields['options_preiseinheit'] = $datanorm->price_unit;
@@ -153,14 +389,48 @@ function datanormAddSupplierPrice($db, $productId, $datanorm, $supplier, $user,
$supplierEanType = !empty($datanorm->ean) ? 2 : 0;
$description = trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : ''));
- return $prodfourn->update_buyprice(
- 1, $purchasePrice, $user, 'HT', $supplier, 0,
+ // Mindestbestellmenge und Verpackungseinheit vom bestehenden Lieferantenpreis übernehmen
+ // (gleiches Produkt = gleiche Mengen, nur anderer Lieferant)
+ $minQty = 1;
+ $packaging = null;
+
+ $sqlExisting = "SELECT quantity, packaging FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
+ $sqlExisting .= " WHERE fk_product = " . (int)$productId;
+ $sqlExisting .= " AND quantity > 0";
+ $sqlExisting .= " ORDER BY rowid ASC LIMIT 1";
+ $resExisting = $db->query($sqlExisting);
+ if ($resExisting && $db->num_rows($resExisting) > 0) {
+ $objExisting = $db->fetch_object($resExisting);
+ if ($objExisting->quantity > 0) {
+ $minQty = $objExisting->quantity;
+ }
+ if (!empty($objExisting->packaging)) {
+ $packaging = $objExisting->packaging;
+ }
+ }
+
+ // Preis berechnen mit zentraler Funktion
+ $pricing = calculateCablePricing($datanorm, $minQty);
+ $totalPrice = $pricing['totalPrice'];
+
+ $result = $prodfourn->update_buyprice(
+ $minQty, $totalPrice, $user, 'HT', $supplier, 0,
$datanorm->article_number, $taxPercent,
0, 0, 0, 0, 0, 0, array(), '',
0, 'HT', 1, '',
$description, $supplierEan, $supplierEanType,
$extrafields
);
+
+ // Verpackungseinheit nachträglich setzen (nicht in update_buyprice verfügbar)
+ if ($result > 0 && !empty($packaging)) {
+ $sqlPkg = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
+ $sqlPkg .= " SET packaging = " . (float)$packaging;
+ $sqlPkg .= " WHERE rowid = " . (int)$result;
+ $db->query($sqlPkg);
+ }
+
+ return $result;
}
/**
@@ -175,7 +445,7 @@ function datanormInsertPriceExtrafields($db, $priceId, $extrafields)
if (empty($priceId) || empty($extrafields)) {
return;
}
- $kupferzuschlag = !empty($extrafields['options_kupferzuschlag']) ? (float)$extrafields['options_kupferzuschlag'] : 'NULL';
+ $produktpreis = !empty($extrafields['options_produktpreis']) ? (float)$extrafields['options_produktpreis'] : 'NULL';
$preiseinheit = !empty($extrafields['options_preiseinheit']) ? (int)$extrafields['options_preiseinheit'] : 1;
$warengruppe = !empty($extrafields['options_warengruppe']) ? "'".$db->escape($extrafields['options_warengruppe'])."'" : 'NULL';
@@ -183,13 +453,22 @@ function datanormInsertPriceExtrafields($db, $priceId, $extrafields)
$sqlCheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields WHERE fk_object = ".(int)$priceId;
$resCheck = $db->query($sqlCheck);
if ($resCheck && $db->num_rows($resCheck) > 0) {
- return; // Bereits vorhanden
+ // Update statt Insert wenn bereits vorhanden
+ $sql = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields SET ";
+ $sql .= "produktpreis = ".($produktpreis === 'NULL' ? "NULL" : $produktpreis).", ";
+ $sql .= "preiseinheit = ".$preiseinheit.", ";
+ $sql .= "warengruppe = ".$warengruppe." ";
+ $sql .= "WHERE fk_object = ".(int)$priceId;
+ if (!$db->query($sql)) {
+ dol_syslog('ImportZugferd: Fehler beim Update der Extrafields: '.$db->lasterror(), LOG_ERR);
+ }
+ return;
}
$sql = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
- $sql .= " (fk_object, kupferzuschlag, preiseinheit, warengruppe) VALUES (";
+ $sql .= " (fk_object, produktpreis, preiseinheit, warengruppe) VALUES (";
$sql .= (int)$priceId.", ";
- $sql .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
+ $sql .= ($produktpreis === 'NULL' ? "NULL" : $produktpreis).", ";
$sql .= $preiseinheit.", ";
$sql .= $warengruppe.")";
if (!$db->query($sql)) {
@@ -701,33 +980,58 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
// Description
$newproduct->description = $datanorm->getFullDescription();
- // Prices
- $purchasePrice = $datanorm->price;
- if ($datanorm->price_unit > 1) {
- $purchasePrice = $datanorm->price / $datanorm->price_unit;
- }
+ // Preise und Kupferzuschlag
+ // Datanorm liefert den reinen Materialpreis (ohne Kupferzuschlag)
+ // WICHTIG: Bei Kabeln ist der Preis bereits für die Ringgröße (z.B. 49,20€ für 100m Ring)
+ $materialPrice = $datanorm->price;
+ $priceUnit = $datanorm->price_unit > 0 ? $datanorm->price_unit : 1;
- // 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;
+ // Prüfen ob es ein Kabel ist
+ $isCable = isCableProduct($datanorm);
+
+ // Preiseinheit bestimmen - unterschiedliche Logik je nach Lieferant-Datenformat:
+ // - Kluxen/Witte/eltric: price_unit > 1 (z.B. 100) → Preis ist für 100m
+ // - Sonepar: price_unit = 1, aber Preis ist für kompletten Ring → Größe aus Name extrahieren
+ $cableText = $datanorm->short_text1 . ' ' . $datanorm->short_text2;
+ if ($priceUnit == 1) {
+ // Sonepar-Format: Ringgröße aus Bezeichnung extrahieren
+ $ringSize = extractCableRingSize($cableText);
+ if ($ringSize > 0) {
+ $priceUnit = $ringSize; // z.B. 100 für Ri100
}
- } 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;
+ }
+ // Bei price_unit > 1 (Kluxen/Witte) bleibt priceUnit unverändert
+ $cableSpecs = null;
+ $kupfergehalt = 0;
+ $kupferzuschlag = 0;
+ $cuPrice = 0;
+
+ if ($isCable) {
+ // Parse Aderanzahl und Querschnitt aus Bezeichnung
+ $cableSpecs = parseCableSpecsFromText($datanorm->short_text1 . ' ' . $datanorm->short_text2);
+
+ if ($cableSpecs) {
+ // Kupfergehalt berechnen
+ $kupfergehalt = calculateKupfergehalt($cableSpecs['aderanzahl'], $cableSpecs['querschnitt']);
+
+ // Aktuellen Kupferpreis holen
+ $cuPrice = getCurrentCopperPrice($db, $import->fk_soc);
+
+ if ($cuPrice > 0 && $kupfergehalt > 0) {
+ // Kupferzuschlag für die Preiseinheit berechnen (z.B. 100m)
+ $kupferzuschlag = calculateKupferzuschlag($kupfergehalt, $cuPrice, $priceUnit);
+ }
}
}
- // Selling price with markup: (purchase price + copper surcharge) × (1 + markup%)
- $sellingPrice = ($purchasePrice + $copperSurchargeForPrice) * (1 + $markup / 100);
+ // Einkaufspreis = Materialpreis + Kupferzuschlag (für die Preiseinheit)
+ $totalPurchasePrice = $materialPrice + $kupferzuschlag;
+
+ // Stückpreis (pro 1 Einheit, z.B. pro Meter)
+ $purchasePricePerUnit = $totalPurchasePrice / $priceUnit;
+
+ // Verkaufspreis mit Aufschlag
+ $sellingPrice = $purchasePricePerUnit * (1 + $markup / 100);
$newproduct->price = $sellingPrice;
$newproduct->price_base_type = 'HT';
$newproduct->tva_tx = $lineObj->tax_percent ?: 19;
@@ -748,6 +1052,25 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
$result = $newproduct->create($user);
if ($result > 0) {
+ // Bei Kabeln: Produkt-Extrafields für Aderanzahl, Querschnitt und Kupfergehalt setzen
+ if ($isCable && $cableSpecs && $kupfergehalt > 0) {
+ $sqlProdExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_extrafields";
+ $sqlProdExtra .= " (fk_object, aderanzahl, querschnitt, kupfergehalt)";
+ $sqlProdExtra .= " VALUES (".(int)$newproduct->id.", ";
+ $sqlProdExtra .= (int)$cableSpecs['aderanzahl'].", ";
+ $sqlProdExtra .= (float)$cableSpecs['querschnitt'].", ";
+ $sqlProdExtra .= (float)$kupfergehalt.")";
+ $sqlProdExtra .= " ON DUPLICATE KEY UPDATE";
+ $sqlProdExtra .= " aderanzahl = ".(int)$cableSpecs['aderanzahl'].",";
+ $sqlProdExtra .= " querschnitt = ".(float)$cableSpecs['querschnitt'].",";
+ $sqlProdExtra .= " kupfergehalt = ".(float)$kupfergehalt;
+ if (!$db->query($sqlProdExtra)) {
+ dol_syslog("ImportZugferd: Fehler beim Setzen der Kabel-Extrafields: ".$db->lasterror(), LOG_WARNING);
+ } else {
+ dol_syslog("ImportZugferd: Kabel-Extrafields gesetzt - Adern: ".$cableSpecs['aderanzahl'].", Querschnitt: ".$cableSpecs['querschnitt'].", Kupfergehalt: ".$kupfergehalt." kg/km", LOG_INFO);
+ }
+ }
+
// Add supplier price
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
$prodfourn = new ProductFournisseur($db);
@@ -767,20 +1090,14 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
// 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;
+ // Produktpreis (reiner Materialpreis ohne Kupferzuschlag) - nur bei Kabeln
+ if ($isCable && $materialPrice > 0) {
+ $supplierPriceExtrafields['options_produktpreis'] = $materialPrice;
}
- // 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;
+ // Preiseinheit
+ if ($priceUnit > 1) {
+ $supplierPriceExtrafields['options_preiseinheit'] = $priceUnit;
} 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
@@ -788,10 +1105,10 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
$supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group;
}
- // Add supplier price entry with EAN and extrafields
+ // Lieferantenpreis speichern (Gesamtpreis inkl. Kupferzuschlag für die Preiseinheit)
$res = $prodfourn->update_buyprice(
- 1, // Quantity
- $purchasePrice, // Price
+ $priceUnit, // Quantity (Mindestmenge, z.B. 100 für 100m)
+ $totalPurchasePrice, // Price (Gesamtpreis für die Mindestmenge inkl. Kupfer)
$user,
'HT', // Price base
$supplier, // Supplier
@@ -813,9 +1130,11 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : '')), // Description from Datanorm
$supplierEan, // Barcode/EAN in supplier price
$supplierEanType, // Barcode type (EAN13)
- $supplierPriceExtrafields // Extra fields (kupferzuschlag, preiseinheit)
+ $supplierPriceExtrafields // Extra fields
);
+ dol_syslog("ImportZugferd: Lieferantenpreis - Material: ".$materialPrice.", Kupfer: ".$kupferzuschlag.", Gesamt: ".$totalPurchasePrice." (für ".$priceUnit." Einheiten)", LOG_INFO);
+
// Manually ensure extrafields record exists for supplier price
// (Dolibarr update_buyprice doesn't always create it properly)
$sqlGetPrice = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
@@ -831,21 +1150,36 @@ if ($action == 'createfromdatanorm' && $line_id > 0) {
$sqlCheckExtra = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sqlCheckExtra .= " WHERE fk_object = ".(int)$priceId;
$resCheckExtra = $db->query($sqlCheckExtra);
+
+ // Werte für Extrafields vorbereiten
+ $produktpreisVal = $isCable && $materialPrice > 0 ? (float)$materialPrice : 'NULL';
+ $kupferzuschlagVal = $kupferzuschlag > 0 ? (float)$kupferzuschlag : 'NULL';
+ $preiseinheitVal = $priceUnit > 1 ? (int)$priceUnit : 1;
+ $warengruppeVal = !empty($datanorm->product_group) ? "'".$db->escape($datanorm->product_group)."'" : 'NULL';
+
if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) {
// Insert extrafields record
- $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 .= " (fk_object, produktpreis, kupferzuschlag, preiseinheit, warengruppe) VALUES (";
$sqlInsertExtra .= (int)$priceId.", ";
- $sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
- $sqlInsertExtra .= $preiseinheit.", ";
- $sqlInsertExtra .= $warengruppe.")";
+ $sqlInsertExtra .= ($produktpreisVal === 'NULL' ? "NULL" : $produktpreisVal).", ";
+ $sqlInsertExtra .= ($kupferzuschlagVal === 'NULL' ? "NULL" : $kupferzuschlagVal).", ";
+ $sqlInsertExtra .= $preiseinheitVal.", ";
+ $sqlInsertExtra .= $warengruppeVal.")";
if (!$db->query($sqlInsertExtra)) {
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
}
+ } else {
+ // Update extrafields record
+ $sqlUpdateExtra = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields SET ";
+ $sqlUpdateExtra .= "produktpreis = ".($produktpreisVal === 'NULL' ? "NULL" : $produktpreisVal).", ";
+ $sqlUpdateExtra .= "kupferzuschlag = ".($kupferzuschlagVal === 'NULL' ? "NULL" : $kupferzuschlagVal).", ";
+ $sqlUpdateExtra .= "preiseinheit = ".$preiseinheitVal.", ";
+ $sqlUpdateExtra .= "warengruppe = ".$warengruppeVal." ";
+ $sqlUpdateExtra .= "WHERE fk_object = ".(int)$priceId;
+ if (!$db->query($sqlUpdateExtra)) {
+ dol_syslog('ImportZugferd: Fehler beim Update der Extrafields: '.$db->lasterror(), LOG_ERR);
+ }
}
}
@@ -1085,28 +1419,55 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
$altSupplier = new Societe($db);
$altSupplier->fetch($altSocId);
- $altPurchasePrice = $altDatanorm->price;
- if ($altDatanorm->price_unit > 1) {
- $altPurchasePrice = $altDatanorm->price / $altDatanorm->price_unit;
- }
-
// Prepare extrafields
$altExtrafields = array();
- if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0) {
- $altExtrafields['options_kupferzuschlag'] = $altDatanorm->metal_surcharge;
- }
- if (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) {
- $altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit;
+ if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0 && !empty($altDatanorm->price)) {
+ $altExtrafields['options_produktpreis'] = $altDatanorm->price;
}
if (!empty($altDatanorm->product_group)) {
$altExtrafields['options_warengruppe'] = $altDatanorm->product_group;
}
+ // Mindestbestellmenge und Verpackungseinheit vom bestehenden Preis übernehmen
+ $altMinQty = 1;
+ $altPackaging = null;
+ $sqlAltExisting = "SELECT quantity, packaging FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
+ $sqlAltExisting .= " WHERE fk_product = " . (int)$existingProductId;
+ $sqlAltExisting .= " AND quantity > 0 ORDER BY rowid ASC LIMIT 1";
+ $resAltExisting = $db->query($sqlAltExisting);
+ if ($resAltExisting && $db->num_rows($resAltExisting) > 0) {
+ $objAltExisting = $db->fetch_object($resAltExisting);
+ if ($objAltExisting->quantity > 0) {
+ $altMinQty = $objAltExisting->quantity;
+ }
+ if (!empty($objAltExisting->packaging)) {
+ $altPackaging = $objAltExisting->packaging;
+ }
+ }
+
+ // Preis berechnen - bei Kabeln ist Datanorm-Preis bereits Ringpreis!
+ $altCableText = $altDatanorm->short_text1 . ' ' . $altDatanorm->short_text2;
+ $altRingSize = extractCableRingSize($altCableText);
+ if ($altRingSize > 0) {
+ // Kabel: Stückpreis aus Ringpreis berechnen, dann auf minQty hochrechnen
+ $altUnitPrice = $altDatanorm->price / $altRingSize;
+ $altTotalPrice = $altUnitPrice * $altMinQty;
+ $altExtrafields['options_preiseinheit'] = $altRingSize;
+ } elseif (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) {
+ // Nicht-Kabel mit price_unit > 1
+ $altUnitPrice = $altDatanorm->price / $altDatanorm->price_unit;
+ $altTotalPrice = $altUnitPrice * $altMinQty;
+ $altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit;
+ } else {
+ // Einzelstück
+ $altTotalPrice = $altDatanorm->price * $altMinQty;
+ }
+
// Add supplier price
$altProdfourn = new ProductFournisseur($db);
$altProdfourn->id = $existingProductId;
- $altProdfourn->update_buyprice(
- 1, $altPurchasePrice, $user, 'HT', $altSupplier, 0,
+ $altResult = $altProdfourn->update_buyprice(
+ $altMinQty, $altTotalPrice, $user, 'HT', $altSupplier, 0,
$altDatanorm->article_number, $lineObj->tax_percent ?: 19,
0, 0, 0, 0, 0, 0, array(), '',
0, 'HT', 1, '',
@@ -1116,6 +1477,14 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
$altExtrafields
);
+ // Verpackungseinheit nachträglich setzen
+ if ($altResult > 0 && !empty($altPackaging)) {
+ $sqlAltPkg = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
+ $sqlAltPkg .= " SET packaging = " . (float)$altPackaging;
+ $sqlAltPkg .= " WHERE rowid = " . (int)$altResult;
+ $db->query($sqlAltPkg);
+ }
+
// Create product mapping
$altMapping = new ProductMapping($db);
$altMapping->fk_soc = $altSocId;
@@ -1186,26 +1555,37 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
// 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;
+ // Produktpreis (reiner Materialpreis) - nur bei Kabeln mit Metallzuschlag
+ if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0 && !empty($datanorm->price)) {
+ $supplierPriceExtrafields['options_produktpreis'] = $datanorm->price;
}
- // Preiseinheit - Priorität: 1. 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 - Priorität: 1. ZUGFeRD basis_quantity, 2. Datanorm price_unit
+ if (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
$supplierPriceExtrafields['options_preiseinheit'] = $lineObj->basis_quantity;
+ } elseif (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
+ $supplierPriceExtrafields['options_preiseinheit'] = $datanorm->price_unit;
}
// Warengruppe aus Datanorm
if (!empty($datanorm->product_group)) {
$supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group;
}
- $prodfourn->update_buyprice(
- 1, // Quantity
- $purchasePrice, // Price
+ // Mindestbestellmenge und Preis mit zentraler Funktion berechnen
+ $pricing = calculateCablePricing($datanorm, 1);
+ $newMinQty = $pricing['priceUnit'];
+ $newPackaging = $pricing['priceUnit'];
+ $newTotalPrice = $datanorm->price; // Originalpreis aus Datanorm
+
+ // Fallback auf ZUGFeRD basis_quantity wenn keine Ringgröße erkannt
+ if ($newMinQty == 1 && !empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) {
+ $newMinQty = $lineObj->basis_quantity;
+ $newPackaging = $lineObj->basis_quantity;
+ $newTotalPrice = $purchasePrice * $newMinQty;
+ }
+
+ $newPriceResult = $prodfourn->update_buyprice(
+ $newMinQty, // Quantity (Mindestbestellmenge)
+ $newTotalPrice, // Price (Gesamtpreis für die Mindestmenge)
$user,
'HT', // Price base
$supplier, // Supplier
@@ -1245,21 +1625,32 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
$sqlCheckExtra = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sqlCheckExtra .= " WHERE fk_object = ".(int)$priceId;
$resCheckExtra = $db->query($sqlCheckExtra);
+
+ $produktpreis = !empty($supplierPriceExtrafields['options_produktpreis']) ? (float)$supplierPriceExtrafields['options_produktpreis'] : 'NULL';
+ $preiseinheit = !empty($supplierPriceExtrafields['options_preiseinheit']) ? (int)$supplierPriceExtrafields['options_preiseinheit'] : 1;
+ $warengruppe = !empty($supplierPriceExtrafields['options_warengruppe']) ? "'".$db->escape($supplierPriceExtrafields['options_warengruppe'])."'" : 'NULL';
+
if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) {
// Insert extrafields record
- $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 .= " (fk_object, produktpreis, preiseinheit, warengruppe) VALUES (";
$sqlInsertExtra .= (int)$priceId.", ";
- $sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", ";
+ $sqlInsertExtra .= ($produktpreis === 'NULL' ? "NULL" : $produktpreis).", ";
$sqlInsertExtra .= $preiseinheit.", ";
$sqlInsertExtra .= $warengruppe.")";
if (!$db->query($sqlInsertExtra)) {
dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR);
}
+ } else {
+ // Update extrafields record
+ $sqlUpdateExtra = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields SET ";
+ $sqlUpdateExtra .= "produktpreis = ".($produktpreis === 'NULL' ? "NULL" : $produktpreis).", ";
+ $sqlUpdateExtra .= "preiseinheit = ".$preiseinheit.", ";
+ $sqlUpdateExtra .= "warengruppe = ".$warengruppe." ";
+ $sqlUpdateExtra .= "WHERE fk_object = ".(int)$priceId;
+ if (!$db->query($sqlUpdateExtra)) {
+ dol_syslog('ImportZugferd: Fehler beim Update der Extrafields: '.$db->lasterror(), LOG_ERR);
+ }
}
}
@@ -1295,8 +1686,8 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
// Prepare extrafields for alternative supplier price
$altExtrafields = array();
- if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0) {
- $altExtrafields['options_kupferzuschlag'] = $altDatanorm->metal_surcharge;
+ if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0 && !empty($altDatanorm->price)) {
+ $altExtrafields['options_produktpreis'] = $altDatanorm->price;
}
if (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) {
$altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit;
@@ -1479,8 +1870,28 @@ if ($action == 'previewdatanorm' && $id > 0) {
}
// Build supplier alternatives array
+ // Only show suppliers that don't already have a price for this product
$supplierAlternatives = array();
+ $existingPriceSuppliers = array();
+
+ // If product exists, load existing supplier prices
+ if ($existingProductId > 0) {
+ $sqlExisting = "SELECT fk_soc FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
+ $sqlExisting .= " WHERE fk_product = ".(int)$existingProductId;
+ $resExisting = $db->query($sqlExisting);
+ if ($resExisting) {
+ while ($objEx = $db->fetch_object($resExisting)) {
+ $existingPriceSuppliers[$objEx->fk_soc] = true;
+ }
+ }
+ }
+
foreach ($results as $altResult) {
+ // Skip if supplier already has a price for this product
+ if ($existingProductId > 0 && isset($existingPriceSuppliers[$altResult['fk_soc']])) {
+ continue;
+ }
+
$altSupplier = new Societe($db);
$altSupplier->fetch($altResult['fk_soc']);
@@ -1945,6 +2356,10 @@ if ($action == 'edit' && $import->id > 0) {
// Line items
print '
';
+ print '