feat: Kabel-Preislogik für verschiedene Lieferanten-Formate (v3.8)
- Neue zentrale Funktion calculateCablePricing() für einheitliche Preislogik - Unterschiedliche Lieferanten-Formate: Sonepar (price_unit=1, Ring im Namen) vs Kluxen/Witte (price_unit=100) - Ringgröße-Erkennung: Ri100, Tr500, Fol.25m, "Ring 100m", "Trommel 500m" - Cross-Catalog-Suche nur noch über EAN (verhindert Fehlzuordnungen) - EAN-Auto-Update aus ZUGFeRD mit automatischer Barcode-Typ-Erkennung (EAN8/13/UPC-A) - Neues Extrafield "produktpreis" für Materialpreis ohne Kupferzuschlag - Kupfergehalt-Berechnung: Aderanzahl × Querschnitt × 8.9 - Division durch Null abgesichert - Besseres Error-Handling für Extrafields Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8b0d1830a3
commit
745fc68fc9
9 changed files with 782 additions and 256 deletions
19
CHANGELOG.md
Normal file → Executable file
19
CHANGELOG.md
Normal file → Executable file
|
|
@ -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
|
||||
|
|
|
|||
34
README.md
34
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
782
import.php
782
import.php
File diff suppressed because it is too large
Load diff
|
|
@ -451,6 +451,8 @@ LabelChange = Namensänderung
|
|||
#
|
||||
Kupferzuschlag = Kupferzuschlag
|
||||
KupferzuschlagHelp = Metallzuschlag pro Einheit (wird aus Rechnungen extrahiert)
|
||||
Produktpreis = Produktpreis
|
||||
ProduktpreisHelp = Reiner Materialpreis ohne Kupferzuschlag (nur bei Kabeln)
|
||||
Preiseinheit = Preiseinheit
|
||||
PreiseinheitHelp = Anzahl Einheiten pro Preis (z.B. 100 = Preis pro 100 Stück)
|
||||
Warengruppe = Warengruppe
|
||||
|
|
@ -489,3 +491,7 @@ MoreExpensiveBy = %s%% teurer
|
|||
RefreshProductListHelp = Produktlisten neu laden (nach Anlage neuer Produkte)
|
||||
SelectAll = Alle auswählen
|
||||
DeselectAll = Keine auswählen
|
||||
|
||||
# UI Buttons
|
||||
ExpandAll = Alle aufklappen
|
||||
CollapseAll = Alle zuklappen
|
||||
|
|
|
|||
|
|
@ -389,6 +389,8 @@ NotifyEmail = Recipient email
|
|||
#
|
||||
Kupferzuschlag = Copper Surcharge
|
||||
KupferzuschlagHelp = Metal surcharge per unit (extracted from invoices)
|
||||
Produktpreis = Material Price
|
||||
ProduktpreisHelp = Material price without copper surcharge (cables only)
|
||||
Preiseinheit = Price Unit
|
||||
PreiseinheitHelp = Number of units per price (e.g. 100 = price per 100 pieces)
|
||||
Warengruppe = Product Group
|
||||
|
|
@ -420,3 +422,7 @@ MoreExpensiveBy = %s%% more expensive
|
|||
RefreshProductListHelp = Refresh product lists (after creating new products)
|
||||
SelectAll = Select all
|
||||
DeselectAll = Deselect all
|
||||
|
||||
# UI Buttons
|
||||
ExpandAll = Expand all
|
||||
CollapseAll = Collapse all
|
||||
|
|
|
|||
Loading…
Reference in a new issue