Compare commits

..

No commits in common. "v3.3" and "main" have entirely different histories.
v3.3 ... main

32 changed files with 2135 additions and 19586 deletions

View file

@ -1,40 +0,0 @@
{
"permissions": {
"allow": [
"Bash(pdfdetach:*)",
"Bash(python3:*)",
"Bash(xmllint:*)",
"Bash(php -r:*)",
"Bash(chmod:*)",
"Bash(cut:*)",
"Bash(grep:*)",
"Bash(mysql:*)",
"Bash(ls:*)",
"Bash(php:*)",
"Bash(for module in /srv/http/dolibarr/custom/*/core/modules/mod*.php)",
"Bash(do echo \"=== $module ===\" grep -A5 \"''js''\" \"$module\")",
"Bash(done)",
"Bash(find:*)",
"Bash(chown:*)",
"Bash(sudo chown:*)",
"Bash(composer show:*)",
"Bash(journalctl:*)",
"Bash(sudo tail:*)",
"Bash(strings:*)",
"Bash(qpdf --show-object=all:*)",
"Bash(pdftk:*)",
"Bash(pdfinfo:*)",
"Bash(exiftool:*)",
"Bash(pacman:*)",
"Bash(git init:*)",
"Bash(git remote add:*)",
"Bash(git branch:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git tag:*)",
"Bash(git push:*)",
"Bash(git pull:*)",
"Bash(git stash:*)"
]
}
}

File diff suppressed because one or more lines are too long

145
CHANGELOG.md Executable file
View file

@ -0,0 +1,145 @@
# Changelog
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
## [5.7] - 2026-03-04
### Behoben
- **PDF-Pfad bei Cron/Batch-Import**: PDFs werden jetzt korrekt nach `/imports/{id}/{filename}` gespeichert
- Problem: Cron speicherte nach `/imports/{ref}_{filename}`, aber beim Erstellen der Rechnung wurde nach `/imports/{id}/{filename}` gesucht
- Lösung: Einheitlicher Pfad für manuellen und Cron-Import
- **Fallback für alte PDF-Pfade**: Beim Anhängen an Lieferantenrechnung wird auch im alten Pfadformat gesucht
- Ermöglicht korrekte Verarbeitung von Imports die vor dem Fix erstellt wurden
- Sucht zuerst `/imports/{id}/{filename}`, dann `/imports/{ref}_{filename}`
## [5.5] - 2026-03-03
### Behoben
- **Kupferzuschlag-Skalierung in Massenaktualisierung**: Kupferzuschlag wird jetzt korrekt skaliert wenn Dolibarr-Mindestmenge von Datanorm-Preiseinheit abweicht
- Problem: Cu für 50m wurde direkt zu Datanorm-Preis für 100m addiert
- Lösung: Cu wird erst auf Stückpreis umgerechnet (`cu_per_unit = Cu / quantity`), dann auf Datanorm-PE skaliert
- **Steuersatz bei Preisübernahme**: `tva_tx` wird jetzt korrekt beibehalten statt auf 0 gesetzt
- Direktes SQL-UPDATE statt `update_buyprice()` um alle Felder zu erhalten
- **Preise auf 2 Dezimalstellen**: Gesamtpreis und Stückpreis werden auf 2 Nachkommastellen gerundet
### Hinzugefügt
- **Filter für Preisrichtung**: Neue Checkboxen "Preise rauf" und "Preise runter" in Massenaktualisierung
- Ermöglicht gezieltes Filtern nach Preiserhöhungen oder -senkungen
- **Filter-Persistenz**: Alle Filter (inkl. hide_cables, filter_price_up, filter_price_down) bleiben nach Preisübernahme erhalten
- **Alternative Datanorm-Preise verbessert**: Beim Import von Rechnungen mit alternativen Datanorm-Katalogen werden jetzt übernommen:
- Mindestmenge vom vorhandenen/Hauptpreis
- Verpackungseinheit vom vorhandenen/Hauptpreis
- Steuersatz vom vorhandenen/Hauptpreis
- kaufmenge-Extrafield (nur wenn numerisch und > 0)
- **Extrafield kaufmenge sichtbar**: Feld wird jetzt in Formularen angezeigt (`list = 1`)
### Geändert
- **Kupferzuschlag nicht automatisch gesetzt**: Bei Datanorm-Import wird kupferzuschlag NICHT mehr gesetzt - wird von separatem Modul berechnet
### Technisch
- Kupferzuschlag-Berechnung: `cu_for_price_unit = (kupferzuschlag / effective_quantity) * price_unit`
- kaufmenge-Validierung: `trim() !== '' && is_numeric() && (int) > 0`
## [4.2] - 2026-03-02
### Behoben
- **PDF-Anhänge**: ZUGFeRD-PDFs werden jetzt korrekt an Lieferantenrechnungen angehängt
- Problem: PDF wurde nur ins Dateisystem kopiert, nicht in ECM-Datenbank registriert
- Lösung: `EcmFiles`-Eintrag wird erstellt für korrekte Verknüpfung mit Rechnung
- Wichtig: Bei Rechnungsvalidierung wird PDF automatisch mitverschoben
### Hinzugefügt
- **Bezeichnung in Rechnungsliste**: Teuerster Artikel wird als Bezeichnung der Lieferantenrechnung gesetzt
- Erleichtert schnelle Identifikation in der Rechnungsliste
- Spalte "Bezeichnung" muss in Liste aktiviert sein
## [4.0] - 2026-03-01
### Behoben
- **Verschachtelte HTML-Forms**: "Ausgewählte Preise hinzufügen" funktionierte nicht, weil Browser verschachtelte `<form>`-Elemente nicht unterstützen. Lösung: HTML5 `form`-Attribut
- **Stückpreis-Anzeige**: Einkaufspreise zeigen jetzt den Stückpreis statt Gesamtpreis (z.B. 0,16 statt 16,32 bei 100 Stk.)
- **Preisvergleich**: Fehlende Lieferantenpreise werden korrekt auf Stückpreis-Basis verglichen
- **DATPREIS-Kommentare**: Feld korrekt als Rabattkennzeichen dokumentiert (war fälschlich als PE-Code beschrieben)
### Verbessert
- **Feedback bei Preishinzufügen**: Zeigt Erfolgs- und Fehlermeldungen nach dem Hinzufügen von Lieferantenpreisen
- **Mengenkontext**: Bei Mengenstaffel wird zusätzlich der Gesamtpreis mit Stückzahl angezeigt (z.B. "0,16 (16,32/100Stk.)")
### Hinweis
- `uk_product_barcode` UNIQUE KEY auf `product_fournisseur_price` muss entfernt werden falls vorhanden (mehrere Lieferanten dürfen gleichen EAN haben)
## [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
- **GlobalNotify Integration**: Benachrichtigungen über das zentrale GlobalNotify-Modul
- Import-Fehler: Warnung bei fehlgeschlagenen Importen
- Rechnungen zur Prüfung: Aktion wenn neue Rechnungen warten
- IMAP-Fehler: Warnung wenn E-Mail Postfach nicht erreichbar
- Exception/Fatal: Sofortige Benachrichtigung bei Abstürzen
- **Helper-Funktion**: `notify()` für sichere GlobalNotify-Nutzung mit Fallback
### Hinweis
GlobalNotify ist optional. Ohne das Modul werden Benachrichtigungen ins Dolibarr-Log geschrieben.
## [3.6] - 2026-02-23
### Behoben
- **Cron-Job Fix**: Fehlendes `require_once` für `admin.lib.php` hinzugefügt - verhinderte das Speichern des letzten Laufzeitpunkts
- Cron-Job lief in Endlosschleife weil `dolibarr_set_const()` nicht gefunden wurde
### Hinzugefügt
- **Dediziertes Cron-Logging**: Separate Log-Datei unter `/documents/importzugferd/logs/cron_importzugferd.log`
- **Shutdown Handler**: Fängt fatale PHP-Fehler ab und protokolliert sie
- **Detailliertes Logging**: Zeigt jeden Schritt des Import-Prozesses (Ordner-Zugriff, PDF-Scan, IMAP-Status)
### Verbessert
- Robustere Fehlerbehandlung mit try/catch für Exceptions und Throwables
- IMAP-Import wird nur ausgeführt wenn tatsächlich konfiguriert
## [3.5] - 2026-02-15
### Hinzugefügt
- Automatischer Cron-Import aus Watch-Folder
- IMAP-Mailbox-Unterstützung für E-Mail-Rechnungen
- Konfigurierbare Import-Frequenz (stündlich, täglich, wöchentlich)
- Archiv- und Fehler-Ordner für verarbeitete Dateien
## [3.0] - 2026-02-01
### Hinzugefügt
- ZUGFeRD/Factur-X PDF-Parsing
- Automatische Lieferanten-Erkennung
- Rechnungsvorschau vor Import
- Datanorm-Integration für Artikelpreise
## [2.0] - 2026-01-15
### Hinzugefügt
- Basis-Import von ZUGFeRD-Rechnungen
- Manuelle Datei-Auswahl
- Integration in Lieferantenrechnungen
## [1.0] - 2026-01-01
### Erste Version
- Grundlegende ZUGFeRD-Erkennung
- XML-Extraktion aus PDF

View file

@ -1,43 +1,107 @@
# CHANGELOG MODULE IMPORTZUGFERD FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) # Changelog
## 3.2 Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
### Neue Funktionen ## [4.2] - 2026-03-02
- Cross-Katalog-Suche: Artikel werden ueber EAN/Hersteller-Artikelnummer in allen Lieferanten-Katalogen gefunden
- Multi-Lieferanten-Anzeige: Bei Produktzuordnung werden alle verfuegbaren Lieferanten mit Preisen angezeigt
- Fehlende Lieferantenpreise: Bei zugeordneten Produkten werden fehlende EK-Preise anderer Lieferanten angeboten
- Preisvergleich mit Prozentangabe (guenstiger/teurer) fuer Lieferanten-Alternativen
### Bugfixes ### Behoben
- Datanorm Import: Kluxen-Format (Preise im A-Record in Cent) wird jetzt korrekt verarbeitet - **PDF-Anhänge**: ZUGFeRD-PDFs werden jetzt korrekt an Lieferantenrechnungen angehängt
- Datanorm Import: Preise aus A-Record werden von Cent in Euro umgerechnet (geteilt durch 100) - Problem: PDF wurde nur ins Dateisystem kopiert, nicht in ECM-Datenbank registriert
- Lösung: `EcmFiles`-Eintrag wird erstellt für korrekte Verknüpfung mit Rechnung
- Wichtig: Bei Rechnungsvalidierung wird PDF automatisch mitverschoben
### Hinweise ### Hinzugefügt
- Kluxen-Katalog enthaelt nur Listenpreise (UVP), keine Netto-Einkaufspreise - **Bezeichnung in Rechnungsliste**: Teuerster Artikel wird als Bezeichnung der Lieferantenrechnung gesetzt
- Cross-Katalog-Suche erfordert aktivierte Einstellung "In allen Lieferanten-Katalogen suchen" - Erleichtert schnelle Identifikation in der Rechnungsliste
- Spalte "Bezeichnung" muss in Liste aktiviert sein
## 2.1 ## [4.0] - 2026-03-01
### Bugfixes ### Behoben
- Rechnungsimport: Preise wurden falsch als Brutto (TTC) statt Netto (HT) behandelt - korrigierte Parameterreihenfolge in addline() - **Verschachtelte HTML-Forms**: "Ausgewählte Preise hinzufügen" funktionierte nicht, weil Browser verschachtelte `<form>`-Elemente nicht unterstützen. Lösung: HTML5 `form`-Attribut
- Datanorm Massenaktualisierung: Lieferantenauswahl ging nach Aktionen verloren - Redirects hinzugefuegt - **Stückpreis-Anzeige**: Einkaufspreise zeigen jetzt den Stückpreis statt Gesamtpreis (z.B. 0,16 statt 16,32 bei 100 Stk.)
- Datanorm Massenaktualisierung: "Alle Aenderungen uebernehmen" Button war nicht sichtbar ohne Suchergebnisse - **Preisvergleich**: Fehlende Lieferantenpreise werden korrekt auf Stückpreis-Basis verglichen
- Datanorm Massenaktualisierung: Filter-Auswahl (Preis/Beschreibung/Bezeichnung) wurde bei "Alle hinzufuegen" ignoriert - **DATPREIS-Kommentare**: Feld korrekt als Rabattkennzeichen dokumentiert (war fälschlich als PE-Code beschrieben)
- ProductFournisseur::update_buyprice erwartet Societe-Objekt, nicht Integer-ID
### Verbesserungen ### Verbessert
- Bestaetungsdialog fuer Massenaktionen verwendet jetzt Dolibarr jQuery UI Dialog statt JavaScript confirm() - **Feedback bei Preishinzufügen**: Zeigt Erfolgs- und Fehlermeldungen nach dem Hinzufügen von Lieferantenpreisen
- Manuelles Metallzuschlag-Eingabefeld entfernt (nicht mehr benoetigt - Kupferzuschlag wird aus ZUGFeRD XML extrahiert) - **Mengenkontext**: Bei Mengenstaffel wird zusätzlich der Gesamtpreis mit Stückzahl angezeigt (z.B. "0,16 (16,32/100Stk.)")
- Ausstehende Aenderungen werden immer angezeigt wenn vorhanden, unabhaengig von Suchergebnissen
## 2.0 ### Hinweis
- `uk_product_barcode` UNIQUE KEY auf `product_fournisseur_price` muss entfernt werden falls vorhanden (mehrere Lieferanten dürfen gleichen EAN haben)
- Datanorm 4.0/5.0 Katalog-Import ## [3.8] - 2026-02-25
- Kupferzuschlag-Extraktion aus ZUGFeRD XML (AllowanceCharge)
- Automatischer Preisvergleich zwischen Datanorm und aktuellen Einkaufspreisen
- Massenaktualisierung von Produktpreisen und Beschreibungen
- Aenderungsprotokoll fuer Preisanpassungen
## 1.0 ### 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
Initial version ### 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
- **GlobalNotify Integration**: Benachrichtigungen über das zentrale GlobalNotify-Modul
- Import-Fehler: Warnung bei fehlgeschlagenen Importen
- Rechnungen zur Prüfung: Aktion wenn neue Rechnungen warten
- IMAP-Fehler: Warnung wenn E-Mail Postfach nicht erreichbar
- Exception/Fatal: Sofortige Benachrichtigung bei Abstürzen
- **Helper-Funktion**: `notify()` für sichere GlobalNotify-Nutzung mit Fallback
### Hinweis
GlobalNotify ist optional. Ohne das Modul werden Benachrichtigungen ins Dolibarr-Log geschrieben.
## [3.6] - 2026-02-23
### Behoben
- **Cron-Job Fix**: Fehlendes `require_once` für `admin.lib.php` hinzugefügt - verhinderte das Speichern des letzten Laufzeitpunkts
- Cron-Job lief in Endlosschleife weil `dolibarr_set_const()` nicht gefunden wurde
### Hinzugefügt
- **Dediziertes Cron-Logging**: Separate Log-Datei unter `/documents/importzugferd/logs/cron_importzugferd.log`
- **Shutdown Handler**: Fängt fatale PHP-Fehler ab und protokolliert sie
- **Detailliertes Logging**: Zeigt jeden Schritt des Import-Prozesses (Ordner-Zugriff, PDF-Scan, IMAP-Status)
### Verbessert
- Robustere Fehlerbehandlung mit try/catch für Exceptions und Throwables
- IMAP-Import wird nur ausgeführt wenn tatsächlich konfiguriert
## [3.5] - 2026-02-15
### Hinzugefügt
- Automatischer Cron-Import aus Watch-Folder
- IMAP-Mailbox-Unterstützung für E-Mail-Rechnungen
- Konfigurierbare Import-Frequenz (stündlich, täglich, wöchentlich)
- Archiv- und Fehler-Ordner für verarbeitete Dateien
## [3.0] - 2026-02-01
### Hinzugefügt
- ZUGFeRD/Factur-X PDF-Parsing
- Automatische Lieferanten-Erkennung
- Rechnungsvorschau vor Import
- Datanorm-Integration für Artikelpreise
## [2.0] - 2026-01-15
### Hinzugefügt
- Basis-Import von ZUGFeRD-Rechnungen
- Manuelle Datei-Auswahl
- Integration in Lieferantenrechnungen
## [1.0] - 2026-01-01
### Erste Version
- Grundlegende ZUGFeRD-Erkennung
- XML-Extraktion aus PDF

View file

@ -102,23 +102,39 @@ Available in:
## Version History ## Version History
### 1.1 See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
- New persistent import workflow with database storage
- Manual product assignment via dropdown ### 5.7 (Current)
- Product removal/reassignment - Fixed PDF path for Cron/Batch imports - now correctly saved to `/imports/{id}/{filename}`
- Status "Pending" for imports requiring manual intervention - Added fallback for old PDF paths when attaching to supplier invoices
- Pending imports overview on upload page
- UN/ECE unit code translation (C62 → Stk., MTR → m, etc.) ### 5.5
- Batch import from folder or IMAP mailbox - Fixed copper surcharge scaling in mass update (different quantities between Dolibarr and Datanorm)
- IMAP connection test with folder selection - Fixed VAT rate preservation when updating prices
- Product template feature (duplicate existing product) - New filters for price direction (up/down) in mass update
- Alternative Datanorm prices inherit min quantity, packaging, VAT, and kaufmenge from existing prices
- Copper surcharge is NOT set by import - calculated by separate module
### 4.2
- PDF attachments properly linked to supplier invoices via ECM
- Most expensive item shown as invoice description
### 3.8
- 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
### 3.7
- GlobalNotify integration for import notifications
### 3.5
- Automatic cron import from watch folder and IMAP
### 3.0
- Datanorm integration for article prices
### 1.0 ### 1.0
- Initial release - Initial release
- Basic ZUGFeRD/Factur-X import
- Automatic product matching
- Supplier detection
- Duplicate detection
## License ## License

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
bin/module_importzugferd-4.4.zip Executable file

Binary file not shown.

BIN
bin/module_importzugferd-4.5.zip Executable file

Binary file not shown.

BIN
bin/module_importzugferd-4.6.zip Executable file

Binary file not shown.

BIN
bin/module_importzugferd-4.8.zip Executable file

Binary file not shown.

BIN
bin/module_importzugferd-4.9.zip Executable file

Binary file not shown.

BIN
bin/module_importzugferd-5.0.zip Executable file

Binary file not shown.

BIN
bin/module_importzugferd-5.1.zip Executable file

Binary file not shown.

BIN
bin/module_importzugferd-5.2.zip Executable file

Binary file not shown.

BIN
bin/module_importzugferd-5.3.zip Executable file

Binary file not shown.

BIN
bin/module_importzugferd-5.4.zip Executable file

Binary file not shown.

View file

@ -244,6 +244,13 @@ if (!empty($object->xml_content)) {
// Format XML for better readability using class method // Format XML for better readability using class method
$formattedXml = ZugferdImport::formatXmlForDisplay($object->xml_content); $formattedXml = ZugferdImport::formatXmlForDisplay($object->xml_content);
// XML Syntax-Highlighting
$highlightedXml = dol_escape_htmltag($formattedXml);
// Tag-Namen (oeffnend und schliessend)
$highlightedXml = preg_replace('/(&lt;\/?)([\w:.-]+)/', '$1<span style="color:#2271b1;font-weight:bold;">$2</span>', $highlightedXml);
// Attribut-Namen und -Werte
$highlightedXml = preg_replace('/ ([\w:.-]+)(=)(&quot;)(.*?)(&quot;)/', ' <span style="color:#a83a32;">$1</span>$2<span style="color:#2e7d32;">$3$4$5</span>', $highlightedXml);
print '<br>'; print '<br>';
print '<div class="div-table-responsive-no-min">'; print '<div class="div-table-responsive-no-min">';
print '<table class="noborder centpercent">'; print '<table class="noborder centpercent">';
@ -254,8 +261,8 @@ if (!empty($object->xml_content)) {
print '<td colspan="2">'; print '<td colspan="2">';
print '<a href="#" onclick="jQuery(\'#xmlcontent\').toggle(); return false;" class="butAction">'.$langs->trans('ClickToExpand').'</a>'; print '<a href="#" onclick="jQuery(\'#xmlcontent\').toggle(); return false;" class="butAction">'.$langs->trans('ClickToExpand').'</a>';
print '<div id="xmlcontent" style="display: none; margin-top: 10px;">'; print '<div id="xmlcontent" style="display: none; margin-top: 10px;">';
print '<pre style="max-height: 500px; overflow: auto; background: #f5f5f5; padding: 10px; font-size: 11px; white-space: pre-wrap; word-wrap: break-word;">'; print '<pre style="max-height: 600px; overflow: auto; background: #f8f9fa; padding: 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word;">';
print dol_escape_htmltag($formattedXml); print $highlightedXml;
print '</pre>'; print '</pre>';
print '</div>'; print '</div>';
print '</td>'; print '</td>';

View file

@ -316,6 +316,32 @@ class ActionsImportZugferd
$processed_line['product_ref'] = $product->ref; $processed_line['product_ref'] = $product->ref;
$processed_line['product_label'] = $product->label; $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 { } else {
$processed_line['needs_creation'] = true; $processed_line['needs_creation'] = true;
} }

View file

@ -14,6 +14,7 @@
*/ */
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
dol_include_once('/importzugferd/class/zugferdparser.class.php'); dol_include_once('/importzugferd/class/zugferdparser.class.php');
dol_include_once('/importzugferd/class/zugferdimport.class.php'); dol_include_once('/importzugferd/class/zugferdimport.class.php');
@ -60,6 +61,67 @@ class CronImportZugferd
*/ */
public $error_count = 0; public $error_count = 0;
/**
* @var string Path to cron log file
*/
private $cronLogFile = '';
/**
* @var float Start time of cron execution
*/
private $startTime = 0;
/**
* Send notification via GlobalNotify (if available)
*
* @param string $type 'error', 'warning', 'info', 'action'
* @param string $title Title
* @param string $message Message
* @param string $actionUrl URL for action button
* @param string $actionLabel Label for action button
* @return bool True if sent via GlobalNotify
*/
protected function notify($type, $title, $message, $actionUrl = '', $actionLabel = '')
{
if (!isModEnabled('globalnotify')) {
dol_syslog("ImportZugferd [{$type}]: {$title} - {$message}", LOG_WARNING);
return false;
}
$classFile = dol_buildpath('/globalnotify/class/globalnotify.class.php', 0);
if (!file_exists($classFile)) {
dol_syslog("ImportZugferd [{$type}]: {$title} - {$message}", LOG_WARNING);
return false;
}
require_once $classFile;
if (!class_exists('GlobalNotify')) {
dol_syslog("ImportZugferd [{$type}]: {$title} - {$message}", LOG_WARNING);
return false;
}
try {
switch ($type) {
case 'error':
GlobalNotify::error('importzugferd', $title, $message, $actionUrl, $actionLabel);
break;
case 'warning':
GlobalNotify::warning('importzugferd', $title, $message, $actionUrl, $actionLabel);
break;
case 'action':
GlobalNotify::actionRequired('importzugferd', $title, $message, $actionUrl, $actionLabel ?: 'Aktion erforderlich');
break;
default:
GlobalNotify::info('importzugferd', $title, $message, $actionUrl, $actionLabel);
}
return true;
} catch (Exception $e) {
dol_syslog("GlobalNotify error: ".$e->getMessage(), LOG_ERR);
return false;
}
}
/** /**
* Constructor * Constructor
* *
@ -67,7 +129,45 @@ class CronImportZugferd
*/ */
public function __construct($db) public function __construct($db)
{ {
global $conf;
$this->db = $db; $this->db = $db;
// Set up dedicated log file for cron jobs
$logDir = $conf->importzugferd->dir_output.'/logs';
if (!is_dir($logDir)) {
dol_mkdir($logDir);
}
$this->cronLogFile = $logDir.'/cron_importzugferd.log';
}
/**
* Write to dedicated cron log file
*
* @param string $message Log message
* @param string $level Log level (INFO, WARNING, ERROR, DEBUG)
* @return void
*/
private function cronLog($message, $level = 'INFO')
{
$timestamp = date('Y-m-d H:i:s');
$elapsed = $this->startTime > 0 ? round(microtime(true) - $this->startTime, 2).'s' : '0s';
$logLine = "[{$timestamp}] [{$level}] [{$elapsed}] {$message}\n";
@file_put_contents($this->cronLogFile, $logLine, FILE_APPEND | LOCK_EX);
dol_syslog("CronImportZugferd: ".$message, $level === 'ERROR' ? LOG_ERR : ($level === 'WARNING' ? LOG_WARNING : LOG_INFO));
}
/**
* Shutdown handler to catch fatal errors
*/
public function handleShutdown()
{
$error = error_get_last();
if ($error !== null && in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) {
$message = "FATAL SHUTDOWN: {$error['message']} in {$error['file']}:{$error['line']}";
$this->cronLog($message, 'ERROR');
$this->cronLog("========== CRON END (fatal shutdown) ==========");
}
} }
/** /**
@ -127,15 +227,16 @@ class CronImportZugferd
*/ */
public function runScheduledImport() public function runScheduledImport()
{ {
global $conf, $user, $langs; global $langs;
// Initialize timing and shutdown handler
$this->startTime = microtime(true);
register_shutdown_function(array($this, 'handleShutdown'));
$langs->load('importzugferd@importzugferd'); $langs->load('importzugferd@importzugferd');
// Check if we should run based on frequency $this->cronLog("========== CRON START ==========");
if (!$this->shouldRunImport()) { $this->cronLog("PHP Version: ".PHP_VERSION.", Memory Limit: ".ini_get('memory_limit'));
$this->output = 'Skipped - not scheduled to run (frequency: '.getDolGlobalString('IMPORTZUGFERD_IMPORT_FREQUENCY', 'manual').')';
return 0;
}
// Reset counters // Reset counters
$this->imported_count = 0; $this->imported_count = 0;
@ -143,25 +244,69 @@ class CronImportZugferd
$this->error_count = 0; $this->error_count = 0;
$this->errors = array(); $this->errors = array();
$folderResult = $this->importFromFolder(); try {
$mailboxResult = $this->fetchFromMailbox(); $this->cronLog("Starting folder import...");
$this->importFromFolder();
$this->cronLog("Folder import completed");
// Update last run time // IMAP nur wenn konfiguriert
$this->updateLastRunTime(); if (!empty(getDolGlobalString('IMPORTZUGFERD_IMAP_HOST'))) {
$this->cronLog("Starting IMAP import...");
$this->fetchFromMailbox();
$this->cronLog("IMAP import completed");
} else {
$this->cronLog("IMAP not configured - skipping");
}
// Build combined output // Update last run time
$this->output = sprintf( $this->updateLastRunTime();
"Scheduled import complete. Imported: %d, Skipped: %d, Errors: %d",
$this->imported_count,
$this->skipped_count,
$this->error_count
);
if ($this->error_count > 0 && !empty($this->errors)) { // Build combined output
$this->output .= "\nErrors: " . implode(", ", array_slice($this->errors, 0, 5)); $this->output = sprintf(
"Scheduled import complete. Imported: %d, Skipped: %d, Errors: %d",
$this->imported_count,
$this->skipped_count,
$this->error_count
);
if ($this->error_count > 0 && !empty($this->errors)) {
$this->output .= "\nErrors: " . implode(", ", array_slice($this->errors, 0, 5));
}
$duration = round(microtime(true) - $this->startTime, 2);
$this->cronLog("Completed: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}, duration={$duration}s");
$this->cronLog("========== CRON END (success) ==========");
// Send GlobalNotify notifications
$this->sendImportNotifications();
return ($this->error_count > 0) ? -1 : 0;
} catch (Exception $e) {
$this->error = 'Exception: '.$e->getMessage();
$this->cronLog("EXCEPTION: ".$e->getMessage()."\n".$e->getTraceAsString(), 'ERROR');
$this->cronLog("========== CRON END (exception) ==========");
$this->notify(
'error',
'ZUGFeRD Import fehlgeschlagen',
'Exception: '.$e->getMessage(),
dol_buildpath('/importzugferd/admin/setup.php', 1),
'Einstellungen prüfen'
);
return -1;
} catch (Throwable $t) {
$this->error = 'Fatal: '.$t->getMessage();
$this->cronLog("FATAL: ".$t->getMessage()."\n".$t->getTraceAsString(), 'ERROR');
$this->cronLog("========== CRON END (fatal) ==========");
$this->notify(
'error',
'ZUGFeRD Import Absturz',
'Fatal: '.$t->getMessage(),
dol_buildpath('/importzugferd/admin/setup.php', 1),
'Einstellungen prüfen'
);
return -1;
} }
return ($this->error_count > 0) ? -1 : 0;
} }
/** /**
@ -171,7 +316,7 @@ class CronImportZugferd
*/ */
public function importFromFolder() public function importFromFolder()
{ {
global $conf, $user, $langs; global $langs;
$langs->load('importzugferd@importzugferd'); $langs->load('importzugferd@importzugferd');
@ -180,24 +325,45 @@ class CronImportZugferd
$errorFolder = getDolGlobalString('IMPORTZUGFERD_ERROR_FOLDER'); $errorFolder = getDolGlobalString('IMPORTZUGFERD_ERROR_FOLDER');
$autoCreate = getDolGlobalString('IMPORTZUGFERD_AUTO_CREATE_INVOICE'); $autoCreate = getDolGlobalString('IMPORTZUGFERD_AUTO_CREATE_INVOICE');
$this->cronLog("Watch folder: {$watchFolder}");
// Validate settings // Validate settings
if (empty($watchFolder) || !is_dir($watchFolder)) { if (empty($watchFolder)) {
$this->output = 'Watch folder not configured or not accessible'; $this->cronLog("Watch folder not configured - skipping");
return 0; // Not an error, just not configured $this->output = 'Watch folder not configured';
return 0;
} }
if (!is_dir($watchFolder)) {
$this->cronLog("Watch folder not accessible: {$watchFolder}", 'WARNING');
$this->output = 'Watch folder not accessible';
return 0;
}
$this->cronLog("Watch folder accessible, scanning for PDFs...");
// Load admin user for import actions // Load admin user for import actions
$admin_user = new User($this->db); $admin_user = new User($this->db);
$admin_user->fetch(1); $admin_user->fetch(1);
// Find PDF files // Find PDF files
$this->cronLog("Running glob for *.pdf...");
$files = glob($watchFolder . '/*.pdf'); $files = glob($watchFolder . '/*.pdf');
$files = array_merge($files, glob($watchFolder . '/*.PDF')); $this->cronLog("Found ".count($files)." .pdf files");
$this->cronLog("Running glob for *.PDF...");
$filesUpper = glob($watchFolder . '/*.PDF');
$this->cronLog("Found ".count($filesUpper)." .PDF files");
$files = array_merge($files, $filesUpper);
if (empty($files)) { if (empty($files)) {
$this->cronLog("No PDF files found in watch folder");
return 0; return 0;
} }
$this->cronLog("Total ".count($files)." PDF files to process");
// Ensure archive folder exists if configured // Ensure archive folder exists if configured
if (!empty($archiveFolder) && !is_dir($archiveFolder)) { if (!empty($archiveFolder) && !is_dir($archiveFolder)) {
dol_mkdir($archiveFolder); dol_mkdir($archiveFolder);
@ -209,20 +375,22 @@ class CronImportZugferd
} }
foreach ($files as $file) { foreach ($files as $file) {
$this->cronLog("Processing: ".basename($file));
// Use ZugferdImport::importFromFile for consistent handling // Use ZugferdImport::importFromFile for consistent handling
$import = new ZugferdImport($this->db); $import = new ZugferdImport($this->db);
$result = $import->importFromFile($admin_user, $file, $autoCreate); $result = $import->importFromFile($admin_user, $file, !empty($autoCreate));
if ($result > 0) { if ($result > 0) {
$this->imported_count++; $this->imported_count++;
dol_syslog("CronImportZugferd: Imported invoice from folder: " . basename($file), LOG_INFO); $this->cronLog("Imported: ".basename($file)." -> ID {$result}");
// Archive the file // Archive the file
$this->moveFile($file, $archiveFolder, 'imported_'); $this->moveFile($file, $archiveFolder, 'imported_');
} elseif ($result == -2) { } elseif ($result == -2) {
// Duplicate - already imported // Duplicate - already imported
$this->skipped_count++; $this->skipped_count++;
dol_syslog("CronImportZugferd: Skipped duplicate invoice: " . basename($file), LOG_INFO); $this->cronLog("Skipped (duplicate): ".basename($file));
// Archive duplicates - delete if no archive folder // Archive duplicates - delete if no archive folder
if (!$this->moveFile($file, $archiveFolder, 'duplicate_')) { if (!$this->moveFile($file, $archiveFolder, 'duplicate_')) {
@ -231,7 +399,7 @@ class CronImportZugferd
} else { } else {
$this->error_count++; $this->error_count++;
$this->errors[] = basename($file) . ': ' . $import->error; $this->errors[] = basename($file) . ': ' . $import->error;
dol_syslog("CronImportZugferd: Error importing: " . basename($file) . " - " . $import->error, LOG_WARNING); $this->cronLog("Error importing ".basename($file).": ".$import->error, 'ERROR');
// Try error folder first, fall back to archive folder // Try error folder first, fall back to archive folder
if (!$this->moveFile($file, $errorFolder, 'error_')) { if (!$this->moveFile($file, $errorFolder, 'error_')) {
@ -241,6 +409,7 @@ class CronImportZugferd
} }
} }
$this->cronLog("Folder import finished: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}");
return 0; return 0;
} }
@ -497,7 +666,17 @@ class CronImportZugferd
} }
if (is_dir($targetFolder) && is_writable($targetFolder)) { if (is_dir($targetFolder) && is_writable($targetFolder)) {
$targetPath = $targetFolder . '/' . $prefix . date('Y-m-d_His') . '_' . basename($file); // Originalen Dateinamen beibehalten, bei Namenskollision Zaehler anhaengen
$baseName = basename($file);
$targetPath = $targetFolder . '/' . $baseName;
if (file_exists($targetPath)) {
$pathInfo = pathinfo($baseName);
$counter = 1;
do {
$targetPath = $targetFolder . '/' . $pathInfo['filename'] . '_' . $counter . '.' . $pathInfo['extension'];
$counter++;
} while (file_exists($targetPath));
}
if (@rename($file, $targetPath)) { if (@rename($file, $targetPath)) {
dol_syslog("CronImportZugferd: Moved file to: " . $targetPath, LOG_INFO); dol_syslog("CronImportZugferd: Moved file to: " . $targetPath, LOG_INFO);
@ -534,4 +713,79 @@ class CronImportZugferd
dol_syslog("CronImportZugferd: Keeping error file in watch folder: " . $file, LOG_INFO); dol_syslog("CronImportZugferd: Keeping error file in watch folder: " . $file, LOG_INFO);
return true; return true;
} }
/**
* Send notifications based on import results
*
* @return void
*/
protected function sendImportNotifications()
{
// Check for errors
if ($this->error_count > 0) {
$errorSummary = count($this->errors) > 0 ? implode(', ', array_slice($this->errors, 0, 3)) : 'Siehe Log';
$this->notify(
'warning',
$this->error_count.' ZUGFeRD Import-Fehler',
$errorSummary,
dol_buildpath('/importzugferd/list.php?status=error', 1),
'Fehler anzeigen'
);
}
// Check for imported invoices that need review
if ($this->imported_count > 0) {
// Count pending invoices (drafts needing approval)
$pendingCount = $this->countPendingInvoices();
if ($pendingCount > 0) {
$this->notify(
'action',
$this->imported_count.' ZUGFeRD Rechnungen importiert',
"{$pendingCount} Lieferantenrechnungen warten auf Prüfung und Freigabe",
dol_buildpath('/fourn/facture/list.php?search_status=0', 1),
'Rechnungen prüfen'
);
} else {
// All auto-created and validated
$this->notify(
'info',
$this->imported_count.' ZUGFeRD Rechnungen importiert',
'Alle Rechnungen wurden erfolgreich verarbeitet',
dol_buildpath('/fourn/facture/list.php', 1),
'Anzeigen'
);
}
}
// IMAP connection issues
if (strpos($this->error, 'IMAP connection failed') !== false) {
$this->notify(
'error',
'IMAP Verbindung fehlgeschlagen',
'E-Mail Postfach für ZUGFeRD-Import nicht erreichbar',
dol_buildpath('/importzugferd/admin/setup.php', 1),
'IMAP prüfen'
);
}
}
/**
* Count pending (draft) supplier invoices
*
* @return int Number of draft supplier invoices
*/
protected function countPendingInvoices()
{
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_fourn";
$sql .= " WHERE fk_statut = 0"; // Draft status
$sql .= " AND entity IN (".getEntity('facture_fourn').")";
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
return (int) $obj->cnt;
}
return 0;
}
} }

View file

@ -520,9 +520,22 @@ class Datanorm extends CommonObject
if ($result > 0) { if ($result > 0) {
$results[] = $this->toArray(); $results[] = $this->toArray();
$foundIds[$this->id] = true; $foundIds[$this->id] = true;
// Store EAN and manufacturer_ref for cross-catalog search // Store EAN from Datanorm
$foundEan = $this->ean; $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 not searching all catalogs, return immediately
if (!$searchAll) { if (!$searchAll) {
@ -531,24 +544,15 @@ class Datanorm extends CommonObject
} }
} }
// If searchAll is enabled and we found article with EAN/manufacturer_ref, // If searchAll is enabled and we found article with EAN,
// search other catalogs using these identifiers (cross-catalog search) // search other catalogs using EAN ONLY (cross-catalog search)
if ($searchAll && $fk_soc > 0 && (!empty($foundEan) || !empty($foundManufacturerRef))) { // 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 = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,"; $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
$sql .= " price, price_unit, discount_group, product_group, matchcode"; $sql .= " price, price_unit, discount_group, product_group, matchcode";
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element; $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
$sql .= " WHERE ("; $sql .= " WHERE ean = '" . $this->db->escape($foundEan) . "'";
$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 .= " AND active = 1"; $sql .= " AND active = 1";
$sql .= " AND entity = " . (int) $conf->entity; $sql .= " AND entity = " . (int) $conf->entity;
$sql .= " AND fk_soc != " . (int) $fk_soc; // Exclude already found supplier $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 // Fallback: Search by EXACT article number match for the specified supplier only
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,"; // No LIKE search - cross-catalog comparisons only work via EAN
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,"; if ($fk_soc > 0 && empty($results)) {
$sql .= " price, price_unit, discount_group, product_group, matchcode"; $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element; $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
$sql .= " WHERE (article_number LIKE '" . $this->db->escape($article_number) . "%'"; $sql .= " price, price_unit, discount_group, product_group, matchcode";
$sql .= " OR ean = '" . $this->db->escape($article_number) . "'"; $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
$sql .= " OR manufacturer_ref LIKE '" . $this->db->escape($article_number) . "%')"; $sql .= " WHERE article_number = '" . $this->db->escape($article_number) . "'";
$sql .= " AND active = 1";
$sql .= " AND entity = " . (int) $conf->entity;
if ($fk_soc > 0 && !$searchAll) {
$sql .= " AND fk_soc = " . (int) $fk_soc; $sql .= " AND fk_soc = " . (int) $fk_soc;
} $sql .= " AND active = 1";
$sql .= " AND entity = " . (int) $conf->entity;
$sql .= " LIMIT 1";
// ORDER BY clause $resql = $this->db->query($sql);
if ($fk_soc > 0 && $searchAll) { if ($resql) {
// Order by matching supplier first, then by price while ($obj = $this->db->fetch_object($resql)) {
$sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, price ASC"; if (!isset($foundIds[$obj->rowid])) {
} else { $results[] = array(
$sql .= " ORDER BY article_number"; 'id' => $obj->rowid,
} 'fk_soc' => $obj->fk_soc,
'article_number' => $obj->article_number,
$sql .= " LIMIT " . (int) $limit; 'short_text1' => $obj->short_text1,
'short_text2' => $obj->short_text2,
$resql = $this->db->query($sql); 'ean' => $obj->ean,
if ($resql) { 'manufacturer_ref' => $obj->manufacturer_ref,
while ($obj = $this->db->fetch_object($resql)) { 'manufacturer_name' => $obj->manufacturer_name,
if (!isset($foundIds[$obj->rowid])) { 'unit_code' => $obj->unit_code,
$results[] = array( 'price' => $obj->price,
'id' => $obj->rowid, 'price_unit' => $obj->price_unit,
'fk_soc' => $obj->fk_soc, 'discount_group' => $obj->discount_group,
'article_number' => $obj->article_number, 'product_group' => $obj->product_group,
'short_text1' => $obj->short_text1, 'matchcode' => $obj->matchcode,
'short_text2' => $obj->short_text2, );
'ean' => $obj->ean, $foundIds[$obj->rowid] = true;
'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; return $results;
@ -982,13 +975,13 @@ class Datanorm extends CommonObject
// P;A format - multiple articles per line // P;A format - multiple articles per line
// Format: P;A;ArtNr;PreisKz;Preis;PE;Zuschlag;x;x;x;ArtNr2;... // 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!) // Rabattkennzeichen aus DATPREIS (wird gespeichert aber nicht fuer price_unit verwendet)
if ($recordType === 'P' && isset($parts[1]) && $parts[1] === 'A') { if ($recordType === 'P' && isset($parts[1]) && $parts[1] === 'A') {
$i = 2; $i = 2;
while ($i < count($parts) - 2) { while ($i < count($parts) - 2) {
$articleNumber = trim($parts[$i] ?? ''); $articleNumber = trim($parts[$i] ?? '');
$priceRaw = trim($parts[$i + 2] ?? '0'); $priceRaw = trim($parts[$i + 2] ?? '0');
$datpreisPeCode = (int)trim($parts[$i + 3] ?? '0'); // PE code from DATPREIS $datpreisPeCode = (int)trim($parts[$i + 3] ?? '0'); // Rabattkennzeichen (nicht PE!)
$metalSurchargeRaw = trim($parts[$i + 4] ?? '0'); $metalSurchargeRaw = trim($parts[$i + 4] ?? '0');
$price = (float)$priceRaw / 100; // Convert cents to euros $price = (float)$priceRaw / 100; // Convert cents to euros
$metalSurcharge = (float)$metalSurchargeRaw / 100; // Convert cents to euros $metalSurcharge = (float)$metalSurchargeRaw / 100; // Convert cents to euros
@ -1007,7 +1000,7 @@ class Datanorm extends CommonObject
// Simple format: P;ArtNr;PreisKz;Preis;PE;... // Simple format: P;ArtNr;PreisKz;Preis;PE;...
$articleNumber = trim($parts[1] ?? ''); $articleNumber = trim($parts[1] ?? '');
$priceRaw = trim($parts[3] ?? '0'); $priceRaw = trim($parts[3] ?? '0');
$datpreisPeCode = (int)trim($parts[4] ?? '0'); // PE code if available $datpreisPeCode = (int)trim($parts[4] ?? '0'); // Rabattkennzeichen (nicht PE!)
if (strpos($priceRaw, ',') === false && strpos($priceRaw, '.') === false) { if (strpos($priceRaw, ',') === false && strpos($priceRaw, '.') === false) {
$price = (float)$priceRaw / 100; $price = (float)$priceRaw / 100;

View file

@ -624,6 +624,32 @@ class ZugferdImport extends CommonObject
if (!empty($match) && $match['fk_product'] > 0) { if (!empty($match) && $match['fk_product'] > 0) {
$fk_product = $match['fk_product']; $fk_product = $match['fk_product'];
$match_method = $match['method']; $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);
}
} }
} }
@ -649,12 +675,12 @@ class ZugferdImport extends CommonObject
$this->status = self::STATUS_IMPORTED; $this->status = self::STATUS_IMPORTED;
} }
// Copy PDF to documents // Copy PDF to documents (in subfolder by import ID)
$destdir = $conf->importzugferd->dir_output . '/imports'; $destdir = $conf->importzugferd->dir_output . '/imports/' . $this->id;
if (!is_dir($destdir)) { if (!is_dir($destdir)) {
dol_mkdir($destdir); dol_mkdir($destdir);
} }
$destfile = $destdir . '/' . $this->ref . '_' . basename($file_path); $destfile = $destdir . '/' . $this->pdf_filename;
copy($file_path, $destfile); copy($file_path, $destfile);
// Update status // Update status

View file

@ -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' $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' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '3.3'; $this->version = '5.5';
// Url to the file with your last numberversion of this module // Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt'; //$this->url_last_version = 'http://www.example.com/versionmodule.txt';
@ -573,113 +573,173 @@ class modImportZugferd extends DolibarrModules
$extrafields = new ExtraFields($this->db); $extrafields = new ExtraFields($this->db);
// Add extrafield for supplier customer number (our customer ID at the supplier) // Add extrafield for supplier customer number (our customer ID at the supplier)
// Signature: addExtraField($attrname, $label, $type, $pos, $size, $elementtype, $unique, $required, $default_value, $param, $alwayseditable, $perms, $list, $help, $computed, $entity, $langfile, $enabled, $totalizable, $printable)
$extrafields->addExtraField( $extrafields->addExtraField(
'supplier_customer_number', // attribute code 'supplier_customer_number', // 1. attribute code
'SupplierCustomerNumber', // label (translation key) 'SupplierCustomerNumber', // 2. label (translation key)
'varchar', // type 'varchar', // 3. type
100, // position 100, // 4. position
64, // size 64, // 5. size
'thirdparty', // element type 'thirdparty', // 6. element type
0, // unique 0, // 7. unique
0, // required 0, // 8. required
'', // default value '', // 9. default value
'', // param '', // 10. param
1, // always editable 1, // 11. always editable
'', // permission '', // 12. permission
1, // list (show in list) 1, // 13. list (show in list)
0, // printable '', // 14. help
'', // totalizable '', // 15. computed
'', // langfile '', // 16. entity
'importzugferd@importzugferd', // module 'importzugferd@importzugferd', // 17. langfile
'isModEnabled("importzugferd")' // enabled condition 'isModEnabled("importzugferd")', // 18. enabled condition
0, // 19. totalizable
0 // 20. printable
); );
// Add extrafield for metal surcharge (Kupferzuschlag) on supplier prices // Add extrafield for metal surcharge (Kupferzuschlag) on supplier prices
$extrafields->addExtraField( $extrafields->addExtraField(
'kupferzuschlag', // attribute code 'kupferzuschlag', // 1. attribute code
'Kupferzuschlag', // label (translation key) 'Kupferzuschlag', // 2. label
'price', // type (price field) 'price', // 3. type (price field)
110, // position 110, // 4. position
'24,8', // size '24,8', // 5. size
'product_fournisseur_price', // element type 'product_fournisseur_price', // 6. element type
0, // unique 0, // 7. unique
0, // required 0, // 8. required
'', // default value '', // 9. default value
'', // param '', // 10. param
1, // always editable 1, // 11. always editable
'', // permission '', // 12. permission
1, // list (show in list) 1, // 13. list (show in list)
0, // printable 'Metallzuschlag (Kupfer) für diesen Einkaufspreis', // 14. help
'', // totalizable '', // 15. computed
'', // langfile '', // 16. entity
'importzugferd@importzugferd', // module 'importzugferd@importzugferd', // 17. langfile
'isModEnabled("importzugferd")' // enabled condition 'isModEnabled("importzugferd")', // 18. enabled condition
0, // 19. totalizable
0 // 20. printable
);
// Add extrafield for product price without copper surcharge (only for cables)
$extrafields->addExtraField(
'produktpreis', // 1. attribute code
'Produktpreis', // 2. label
'price', // 3. type (price field)
115, // 4. position
'24,8', // 5. size
'product_fournisseur_price', // 6. element type
0, // 7. unique
0, // 8. required
'', // 9. default value
'', // 10. param
1, // 11. always editable
'', // 12. permission
1, // 13. list (show in list)
'Materialpreis ohne Kupferzuschlag (nur bei Kabeln)', // 14. help
'', // 15. computed
'', // 16. entity
'importzugferd@importzugferd', // 17. langfile
'isModEnabled("importzugferd")', // 18. enabled condition
0, // 19. totalizable
0 // 20. printable
); );
// Add extrafield for price unit (Preiseinheit) on supplier prices // Add extrafield for price unit (Preiseinheit) on supplier prices
$extrafields->addExtraField( $extrafields->addExtraField(
'preiseinheit', // attribute code 'preiseinheit', // 1. attribute code
'Preiseinheit', // label (translation key) 'Preiseinheit', // 2. label
'int', // type 'int', // 3. type
120, // position 120, // 4. position
11, // size 11, // 5. size
'product_fournisseur_price', // element type 'product_fournisseur_price', // 6. element type
0, // unique 0, // 7. unique
0, // required 0, // 8. required
'1', // default value '1', // 9. default value
'', // param '', // 10. param
1, // always editable 1, // 11. always editable
'', // permission '', // 12. permission
1, // list (show in list) 1, // 13. list (show in list)
0, // printable 'Preiseinheit aus Datanorm (z.B. 100 für Preis pro 100m)', // 14. help
'', // totalizable '', // 15. computed
'', // langfile '', // 16. entity
'importzugferd@importzugferd', // module 'importzugferd@importzugferd', // 17. langfile
'isModEnabled("importzugferd")' // enabled condition 'isModEnabled("importzugferd")', // 18. enabled condition
0, // 19. totalizable
0 // 20. printable
); );
// Add extrafield for product group (Warengruppe) on supplier prices // Add extrafield for product group (Warengruppe) on supplier prices
$extrafields->addExtraField( $extrafields->addExtraField(
'warengruppe', // attribute code 'warengruppe', // 1. attribute code
'Warengruppe', // label (translation key) 'Warengruppe', // 2. label
'varchar', // type 'varchar', // 3. type
125, // position 125, // 4. position
32, // size 32, // 5. size
'product_fournisseur_price', // element type 'product_fournisseur_price', // 6. element type
0, // unique 0, // 7. unique
0, // required 0, // 8. required
'', // default value '', // 9. default value
'', // param '', // 10. param
1, // always editable 1, // 11. always editable
'', // permission '', // 12. permission
1, // list (show in list) 1, // 13. list (show in list)
0, // printable 'Datanorm-Warengruppe', // 14. help
'', // totalizable '', // 15. computed
'', // langfile '', // 16. entity
'importzugferd@importzugferd', // module 'importzugferd@importzugferd', // 17. langfile
'isModEnabled("importzugferd")' // enabled condition 'isModEnabled("importzugferd")', // 18. enabled condition
0, // 19. totalizable
0 // 20. printable
);
// Add extrafield for purchase quantity (Kaufmenge) on supplier prices
// Signature: addExtraField($attrname, $label, $type, $pos, $size, $elementtype, $unique, $required, $default_value, $param, $alwayseditable, $perms, $list, $help, $computed, $entity, $langfile, $enabled, $totalizable, $printable)
$extrafields->addExtraField(
'kaufmenge', // 1. attribute code
'Kaufmenge (Datanorm-Vergleich)', // 2. label
'int', // 3. type
127, // 4. position
11, // 5. size
'product_fournisseur_price', // 6. element type
0, // 7. unique
0, // 8. required
'', // 9. default value
'', // 10. param (empty)
1, // 11. always editable
'', // 12. permission
1, // 13. list (show in forms)
'Tatsächliche Kaufmenge für Preisvergleiche. Beispiele: Lüsterklemme mit 12 Positionen = 1, Kabel 100m = leer lassen.', // 14. help
'', // 15. computed
'', // 16. entity
'importzugferd@importzugferd', // 17. langfile
'isModEnabled("importzugferd")', // 18. enabled condition
0, // 19. totalizable
0 // 20. printable
); );
// Add extrafield for copper content (Kupfergehalt) on product - kg per km for cables // Add extrafield for copper content (Kupfergehalt) on product - kg per km for cables
$extrafields->addExtraField( $extrafields->addExtraField(
'kupfergehalt', // attribute code 'kupfergehalt', // 1. attribute code
'Kupfergehalt', // label (translation key) 'Kupfergehalt', // 2. label
'double', // type (decimal number) 'double', // 3. type (decimal number)
130, // position 130, // 4. position
'24,4', // size (precision,scale) '24,4', // 5. size (precision,scale)
'product', // element type 'product', // 6. element type
0, // unique 0, // 7. unique
0, // required 0, // 8. required
'', // default value '', // 9. default value
'', // param '', // 10. param
1, // always editable 1, // 11. always editable
'', // permission '', // 12. permission
1, // list (show in list) 1, // 13. list (show in list)
0, // printable 'Kupfergehalt in kg/km (für Kupferzuschlag-Berechnung bei Kabeln)', // 14. help
'', // totalizable '', // 15. computed
'', // langfile '', // 16. entity
'importzugferd@importzugferd', // module 'importzugferd@importzugferd', // 17. langfile
'isModEnabled("importzugferd")' // enabled condition 'isModEnabled("importzugferd")', // 18. enabled condition
0, // 19. totalizable
0 // 20. printable
); );
// Permissions // Permissions

View file

@ -79,12 +79,18 @@ if ($isFormSubmitted) {
$filter_description = GETPOSTINT('filter_description'); $filter_description = GETPOSTINT('filter_description');
$filter_label = GETPOSTINT('filter_label'); $filter_label = GETPOSTINT('filter_label');
$only_differences = GETPOSTINT('only_differences'); $only_differences = GETPOSTINT('only_differences');
$hide_cables = GETPOSTINT('hide_cables');
$filter_price_up = GETPOSTINT('filter_price_up');
$filter_price_down = GETPOSTINT('filter_price_down');
} else { } else {
// Defaults for first page load // Defaults for first page load
$filter_price = 1; $filter_price = 1;
$filter_description = 1; $filter_description = 1;
$filter_label = 0; $filter_label = 0;
$only_differences = 0; $only_differences = 0;
$hide_cables = 0;
$filter_price_up = 0;
$filter_price_down = 0;
} }
// Initialize objects // Initialize objects
@ -117,8 +123,8 @@ if ($action == 'apply_single' && GETPOSTINT('product_id') && GETPOST('datanorm_k
setEventMessages($langs->trans('ErrorUpdatingProduct'), null, 'errors'); setEventMessages($langs->trans('ErrorUpdatingProduct'), null, 'errors');
} }
// Redirect to same page with same parameters // Redirect to same page with same parameters (preserve all filters)
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences); header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&hide_cables='.$hide_cables.'&filter_price_up='.$filter_price_up.'&filter_price_down='.$filter_price_down.'&action=search');
exit; exit;
} }
@ -137,8 +143,8 @@ if ($action == 'add_pending') {
setEventMessages($langs->trans('AddedToPendingChanges'), null, 'mesgs'); setEventMessages($langs->trans('AddedToPendingChanges'), null, 'mesgs');
} }
// Redirect back with same parameters to preserve supplier selection // Redirect back with same parameters to preserve supplier selection and filters
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&action=search'); header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&hide_cables='.$hide_cables.'&filter_price_up='.$filter_price_up.'&filter_price_down='.$filter_price_down.'&action=search');
exit; exit;
} }
@ -192,8 +198,8 @@ if ($action == 'add_all_pending') {
} }
} }
// Redirect back with same parameters to preserve supplier selection // Redirect back with same parameters to preserve supplier selection and filters
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&action=search'); header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&hide_cables='.$hide_cables.'&filter_price_up='.$filter_price_up.'&filter_price_down='.$filter_price_down.'&action=search');
exit; exit;
} }
@ -322,8 +328,7 @@ if ($obj->cnt == 0) {
} }
// Search form // Search form
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" name="searchform">'; print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'" name="searchform">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="search">'; print '<input type="hidden" name="action" value="search">';
print '<div class="fichecenter">'; print '<div class="fichecenter">';
@ -414,6 +419,21 @@ print '<td>'.$langs->trans('Display').'</td>';
print '<td colspan="3">'; print '<td colspan="3">';
print '<input type="checkbox" name="only_differences" value="1" id="only_differences" '.($only_differences ? 'checked' : '').'>'; print '<input type="checkbox" name="only_differences" value="1" id="only_differences" '.($only_differences ? 'checked' : '').'>';
print '<label for="only_differences"> '.$langs->trans('OnlyShowDifferences').'</label>'; print '<label for="only_differences"> '.$langs->trans('OnlyShowDifferences').'</label>';
print ' &nbsp;&nbsp;&nbsp; ';
print '<input type="checkbox" name="hide_cables" value="1" id="hide_cables" '.($hide_cables ? 'checked' : '').'>';
print '<label for="hide_cables"> <i class="fas fa-eye-slash"></i> Keine Kabel anzeigen</label>';
print '</td>';
print '</tr>';
// Price direction filter
print '<tr class="oddeven">';
print '<td>Preisfilter</td>';
print '<td colspan="3">';
print '<input type="checkbox" name="filter_price_up" value="1" id="filter_price_up" '.($filter_price_up ? 'checked' : '').'>';
print '<label for="filter_price_up"> <i class="fas fa-arrow-up" style="color: #d9534f;"></i> Nur Preise rauf</label>';
print ' &nbsp;&nbsp;&nbsp; ';
print '<input type="checkbox" name="filter_price_down" value="1" id="filter_price_down" '.($filter_price_down ? 'checked' : '').'>';
print '<label for="filter_price_down"> <i class="fas fa-arrow-down" style="color: #5cb85c;"></i> Nur Preise runter</label>';
print '</td>'; print '</td>';
print '</tr>'; print '</tr>';
@ -589,6 +609,45 @@ if ($fk_soc > 0 && ($action == 'search' || GETPOST('search_mode'))) {
print '</tr>'; print '</tr>';
foreach ($comparison_results as $item) { foreach ($comparison_results as $item) {
// Filter cables if requested
// Datanorm groups: 01-19, 101-119, 202, 205 are cables/wires
if ($hide_cables && !empty($item['datanorm_product_group'])) {
$pg = (int)$item['datanorm_product_group'];
if (($pg >= 1 && $pg <= 19) || ($pg >= 101 && $pg <= 119) || $pg == 202 || $pg == 205) {
continue; // Skip cables
}
}
// Calculate price difference for filtering
$price_diff = 0;
if ($item['product_id'] > 0 && $item['price_differs']) {
$datanorm_pe = isset($item['datanorm_price_unit']) ? $item['datanorm_price_unit'] : 1;
$datanorm_raw = isset($item['datanorm_price_raw']) ? $item['datanorm_price_raw'] : 0;
$current_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0;
$effective_qty = isset($item['current_effective_quantity']) ? $item['current_effective_quantity'] : 1;
$current_total = isset($item['current_total_price']) ? $item['current_total_price'] : 0;
// Scale Cu to Datanorm's price_unit
$cu_per_unit = ($current_cu > 0 && $effective_qty > 0) ? $current_cu / $effective_qty : 0;
$cu_for_pe = $cu_per_unit * $datanorm_pe;
$datanorm_total = $datanorm_raw + $cu_for_pe;
$current_unit = $effective_qty > 0 ? $current_total / $effective_qty : $current_total;
$datanorm_unit = $datanorm_total / $datanorm_pe;
$price_diff = $datanorm_unit - $current_unit;
}
// Filter by price direction
if ($filter_price_up && !$filter_price_down && $price_diff <= 0) {
continue; // Only show price increases
}
if ($filter_price_down && !$filter_price_up && $price_diff >= 0) {
continue; // Only show price decreases
}
if ($filter_price_up && $filter_price_down && $price_diff == 0) {
continue; // Both checked: show any change, skip unchanged
}
$has_difference = ($filter_price && $item['price_differs']) || $has_difference = ($filter_price && $item['price_differs']) ||
($filter_description && $item['description_differs']) || ($filter_description && $item['description_differs']) ||
($filter_label && $item['label_differs']); ($filter_label && $item['label_differs']);
@ -623,50 +682,72 @@ if ($fk_soc > 0 && ($action == 'search' || GETPOST('search_mode'))) {
$priceStyle = $item['price_differs'] ? 'background-color: #fcf8e3;' : ''; $priceStyle = $item['price_differs'] ? 'background-color: #fcf8e3;' : '';
print '<td class="right nowraponall" style="'.$priceStyle.'">'; print '<td class="right nowraponall" style="'.$priceStyle.'">';
if ($item['product_id'] > 0) { if ($item['product_id'] > 0) {
print price($item['current_price']); $dolibarr_total = isset($item['current_total_price']) ? $item['current_total_price'] : $item['current_price'];
// Show copper surcharge from invoice if available $dolibarr_qty = isset($item['current_quantity']) ? $item['current_quantity'] : 1;
if (!empty($item['current_kupferzuschlag']) && $item['current_kupferzuschlag'] > 0) { $dolibarr_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0;
print '<br><span class="opacitymedium small" title="Kupferzuschlag aus Rechnung">';
print '<i class="fas fa-plus-circle" style="color:#f0ad4e;"></i> '.price($item['current_kupferzuschlag']); // IMPORTANT: Dolibarr price already includes Cu! Show as info only
print '</span>'; if ($dolibarr_cu > 0) {
// Show total price (material + surcharge) print '<span class="opacitymedium small">(davon '.price($dolibarr_cu).' Cu)</span><br>';
$totalWithSurcharge = $item['current_price'] + $item['current_kupferzuschlag']; }
print '<br><span class="small" style="color:#337ab7;" title="Gesamt (Material+CU)">';
print '<strong>='.price($totalWithSurcharge).'</strong>'; // Total price for minimum quantity (already includes Cu!)
print '</span>'; print '<strong>'.price($dolibarr_total);
if ($dolibarr_qty > 1) {
print '/'.$dolibarr_qty;
}
print '</strong>';
// Unit price as secondary info
if ($dolibarr_qty > 1) {
$dolibarr_unit = $dolibarr_total / $dolibarr_qty;
print '<br><span class="opacitymedium small">('.price($dolibarr_unit).'/Stk.)</span>';
} }
} else { } else {
print '-'; print '-';
} }
print '</td>'; print '</td>';
print '<td class="right nowraponall" style="'.$priceStyle.'">'; print '<td class="right nowraponall" style="'.$priceStyle.'">';
print price($item['datanorm_price']); $datanorm_pe = isset($item['datanorm_price_unit']) ? $item['datanorm_price_unit'] : 1;
// Show original price and unit if price_unit > 1 $datanorm_raw = isset($item['datanorm_price_raw']) ? $item['datanorm_price_raw'] : $item['datanorm_price'];
if (!empty($item['datanorm_price_unit']) && $item['datanorm_price_unit'] > 1) { $current_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0;
print '<br><span class="opacitymedium small">('.price($item['datanorm_price_raw']).'/'.$item['datanorm_price_unit'].')</span>'; $effective_qty = isset($item['current_effective_quantity']) ? $item['current_effective_quantity'] : 1;
}
// Show effective surcharge (from invoice/datanorm)
if (!empty($item['effective_surcharge']) && $item['effective_surcharge'] > 0) {
// Determine surcharge source
$surchargeSource = isset($item['surcharge_source']) ? $item['surcharge_source'] : 'datanorm';
$sourceLabels = array('invoice' => 'Rechnung', 'datanorm' => 'Datanorm');
$sourceColors = array('invoice' => '#f0ad4e', 'datanorm' => '#95a5a6');
$sourceLabel = isset($sourceLabels[$surchargeSource]) ? $sourceLabels[$surchargeSource] : $surchargeSource;
$sourceColor = isset($sourceColors[$surchargeSource]) ? $sourceColors[$surchargeSource] : '#f0ad4e';
print '<br><span class="opacitymedium small" title="Kupferzuschlag ('.$sourceLabel.')">'; // Scale Cu from Dolibarr's quantity to Datanorm's price_unit
print '<i class="fas fa-plus-circle" style="color:'.$sourceColor.';"></i> '.price($item['effective_surcharge']); // Example: Cu 254,55€ for 50m → for 100m = 509,10€
print '</span>'; $cu_per_unit = ($current_cu > 0 && $effective_qty > 0) ? $current_cu / $effective_qty : 0;
// Show total price with surcharge $cu_for_pe = $cu_per_unit * $datanorm_pe;
if (!empty($item['datanorm_price_with_surcharge'])) {
print '<br><span class="small" style="color:#337ab7;" title="Gesamt (Material+CU)">'; // Show breakdown if copper exists
print '<strong>='.price($item['datanorm_price_with_surcharge']).'</strong>'; if ($current_cu > 0) {
print '</span>'; print '<span class="opacitymedium small">'.price($datanorm_raw).' + '.price($cu_for_pe).' Cu</span><br>';
} }
// Total price for Datanorm price_unit (with scaled Cu)
$datanorm_total = $datanorm_raw + $cu_for_pe;
print '<strong>'.price($datanorm_total);
if ($datanorm_pe > 1) {
print '/'.$datanorm_pe;
}
print '</strong>';
// Unit price as secondary info
if ($datanorm_pe > 1) {
$datanorm_unit = $datanorm_total / $datanorm_pe;
print '<br><span class="opacitymedium small">('.price($datanorm_unit).'/Stk.)</span>';
} }
if ($item['price_differs'] && $item['product_id'] > 0) { if ($item['price_differs'] && $item['product_id'] > 0) {
$diff = $item['datanorm_price'] - $item['current_price']; // Calculate percentage difference using UNIT PRICE basis
$diffPercent = ($item['current_price'] > 0) ? ($diff / $item['current_price'] * 100) : 0; $current_total = isset($item['current_total_price']) ? $item['current_total_price'] : $item['current_price'];
// Dolibarr: unit price (already includes Cu)
$current_compare = $effective_qty > 0 ? $current_total / $effective_qty : $current_total;
// Datanorm: unit price (material + scaled Cu)
$datanorm_compare = $datanorm_total / $datanorm_pe;
$diff = $datanorm_compare - $current_compare;
$diffPercent = ($current_compare > 0) ? ($diff / $current_compare * 100) : 0;
print '<br>'; print '<br>';
if ($diff > 0) { if ($diff > 0) {
print '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($diffPercent, 1).'%</span>'; print '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($diffPercent, 1).'%</span>';
@ -715,6 +796,9 @@ if ($fk_soc > 0 && ($action == 'search' || GETPOST('search_mode'))) {
print '<input type="hidden" name="filter_description" value="'.$filter_description.'">'; print '<input type="hidden" name="filter_description" value="'.$filter_description.'">';
print '<input type="hidden" name="filter_label" value="'.$filter_label.'">'; print '<input type="hidden" name="filter_label" value="'.$filter_label.'">';
print '<input type="hidden" name="only_differences" value="'.$only_differences.'">'; print '<input type="hidden" name="only_differences" value="'.$only_differences.'">';
print '<input type="hidden" name="hide_cables" value="'.$hide_cables.'">';
print '<input type="hidden" name="filter_price_up" value="'.$filter_price_up.'">';
print '<input type="hidden" name="filter_price_down" value="'.$filter_price_down.'">';
// Checkboxes for what to apply // Checkboxes for what to apply
if ($filter_price && $item['price_differs']) { if ($filter_price && $item['price_differs']) {
@ -1101,14 +1185,23 @@ function searchDatanormProducts($db, $fk_soc, $search_term, $search_by_name = 0,
$datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0; $datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
// Get current price and copper surcharge from extrafield // Get current price and copper surcharge from extrafield
$current_price = 0; $current_total_price = 0;
$current_quantity = 1;
$current_kaufmenge = 0;
$current_kupferzuschlag = 0; $current_kupferzuschlag = 0;
$datanorm_price_unit_code = isset($datanorm->price_unit_code) ? $datanorm->price_unit_code : -1;
if ($product) { if ($product) {
$priceDetails = getSupplierPriceDetails($db, $product->rowid, $fk_soc); $priceDetails = getSupplierPriceDetails($db, $product->rowid, $fk_soc);
$current_price = $priceDetails['unitprice']; $current_total_price = $priceDetails['price'];
$current_quantity = $priceDetails['quantity'];
$current_kaufmenge = $priceDetails['kaufmenge'];
$current_kupferzuschlag = $priceDetails['kupferzuschlag']; $current_kupferzuschlag = $priceDetails['kupferzuschlag'];
} }
// Use kaufmenge if set, otherwise fall back to quantity
$effective_quantity = ($current_kaufmenge > 0) ? $current_kaufmenge : $current_quantity;
// Priority for surcharge: 1) Invoice extrafield, 2) Datanorm // Priority for surcharge: 1) Invoice extrafield, 2) Datanorm
if ($current_kupferzuschlag > 0) { if ($current_kupferzuschlag > 0) {
$effective_surcharge = $current_kupferzuschlag; $effective_surcharge = $current_kupferzuschlag;
@ -1118,14 +1211,43 @@ function searchDatanormProducts($db, $fk_soc, $search_term, $search_by_name = 0,
$surcharge_source = 'datanorm'; $surcharge_source = 'datanorm';
} }
// Calculate prices // Calculate prices for comparison - UNIT PRICE basis
// IMPORTANT: Dolibarr price already INCLUDES kupferzuschlag! Don't add it again!
// Datanorm price is WITHOUT kupferzuschlag, so add SCALED Cu for comparison
//
// Example: Kabel NYM-J 5x10
// - Dolibarr: 331,27€ for 50m (includes 254,55€ Cu for 50m) → 6,63€/m
// - Datanorm: 168,50€ for 100m (PE=100)
// - Cu per unit: 254,55€ / 50m = 5,09€/m → for 100m = 509,10€
// - Datanorm total: 168,50€ + 509,10€ = 677,60€ → 6,78€/m
// Calculate Cu per unit (from Dolibarr's quantity basis)
$cu_per_unit = ($current_kupferzuschlag > 0 && $effective_quantity > 0)
? $current_kupferzuschlag / $effective_quantity
: 0;
// Scale Cu to Datanorm's price_unit basis
$cu_for_price_unit = $cu_per_unit * $price_unit;
// Dolibarr: unit price (already includes Cu)
$current_compare_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price;
// Datanorm: material price + scaled Cu, then to unit price
$datanorm_compare_price = ($datanorm->price + $cu_for_price_unit) / $price_unit;
// For display: always show unit prices
$datanorm_material_unit_price = $datanorm->price / $price_unit; $datanorm_material_unit_price = $datanorm->price / $price_unit;
$total_price_with_surcharge = $datanorm->price + ($effective_surcharge * $price_unit); $total_price_with_surcharge = $datanorm->price + $cu_for_price_unit;
$datanorm_total_unit_price = $total_price_with_surcharge / $price_unit; $datanorm_total_unit_price = $total_price_with_surcharge / $price_unit;
$current_unit_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price;
$results[] = array( $results[] = array(
'product_id' => $product ? $product->rowid : 0, 'product_id' => $product ? $product->rowid : 0,
'current_price' => $current_price, 'current_price' => $current_unit_price,
'current_total_price' => $current_total_price,
'current_quantity' => $current_quantity,
'current_kaufmenge' => $current_kaufmenge,
'current_effective_quantity' => $effective_quantity,
'current_kupferzuschlag' => $current_kupferzuschlag, 'current_kupferzuschlag' => $current_kupferzuschlag,
'current_description' => $product ? $product->description : '', 'current_description' => $product ? $product->description : '',
'current_label' => $product ? $product->label : '', 'current_label' => $product ? $product->label : '',
@ -1137,12 +1259,14 @@ function searchDatanormProducts($db, $fk_soc, $search_term, $search_by_name = 0,
'datanorm_price_raw' => $datanorm->price, 'datanorm_price_raw' => $datanorm->price,
'datanorm_material_price' => $datanorm->price, 'datanorm_material_price' => $datanorm->price,
'datanorm_metal_surcharge' => $datanorm_metal_surcharge, 'datanorm_metal_surcharge' => $datanorm_metal_surcharge,
'datanorm_price_unit_code' => $datanorm_price_unit_code,
'effective_surcharge' => $effective_surcharge, 'effective_surcharge' => $effective_surcharge,
'surcharge_source' => $surcharge_source, 'surcharge_source' => $surcharge_source,
'datanorm_price_unit' => $price_unit, 'datanorm_price_unit' => $price_unit,
'datanorm_product_group' => isset($datanorm->product_group) ? $datanorm->product_group : '',
'datanorm_description' => trim($datanorm->short_text1.' '.$datanorm->short_text2), 'datanorm_description' => trim($datanorm->short_text1.' '.$datanorm->short_text2),
'datanorm_label' => $datanorm->short_text1, 'datanorm_label' => $datanorm->short_text1,
'price_differs' => $product && abs($current_price - $datanorm_material_unit_price) > 0.01, 'price_differs' => $product && abs($current_compare_price - $datanorm_compare_price) > 0.01,
'description_differs' => $product && $product->description != trim($datanorm->short_text1.' '.$datanorm->short_text2), 'description_differs' => $product && $product->description != trim($datanorm->short_text1.' '.$datanorm->short_text2),
'label_differs' => $product && $product->label != $datanorm->short_text1, 'label_differs' => $product && $product->label != $datanorm->short_text1,
); );
@ -1276,14 +1400,17 @@ function getSupplierPrice($db, $product_id, $fk_soc)
function getSupplierPriceDetails($db, $product_id, $fk_soc) function getSupplierPriceDetails($db, $product_id, $fk_soc)
{ {
$result = array( $result = array(
'price' => 0,
'quantity' => 1,
'kaufmenge' => 0,
'unitprice' => 0, 'unitprice' => 0,
'kupferzuschlag' => 0, 'kupferzuschlag' => 0,
'preiseinheit' => 1, 'preiseinheit' => 1,
'price_id' => 0, 'price_id' => 0,
); );
// Get base price // Get base price - ALWAYS load price + quantity, NOT unitprice alone!
$sql = "SELECT pf.rowid, pf.unitprice, pf.price, pf.quantity"; $sql = "SELECT pf.rowid, pf.price, pf.quantity";
$sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price pf"; $sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price pf";
$sql .= " WHERE pf.fk_product = ".((int)$product_id); $sql .= " WHERE pf.fk_product = ".((int)$product_id);
$sql .= " AND pf.fk_soc = ".((int)$fk_soc); $sql .= " AND pf.fk_soc = ".((int)$fk_soc);
@ -1293,18 +1420,14 @@ function getSupplierPriceDetails($db, $product_id, $fk_soc)
if ($resql && $db->num_rows($resql) > 0) { if ($resql && $db->num_rows($resql) > 0) {
$obj = $db->fetch_object($resql); $obj = $db->fetch_object($resql);
$result['price_id'] = $obj->rowid; $result['price_id'] = $obj->rowid;
$result['price'] = (float)$obj->price;
$result['quantity'] = max(1, (int)$obj->quantity);
// Calculate unit price // Calculate unit price from price / quantity
if (!empty($obj->unitprice) && $obj->unitprice > 0) { $result['unitprice'] = $result['quantity'] > 0 ? $result['price'] / $result['quantity'] : $result['price'];
$result['unitprice'] = $obj->unitprice;
} elseif (!empty($obj->quantity) && $obj->quantity > 0) {
$result['unitprice'] = $obj->price / $obj->quantity;
} else {
$result['unitprice'] = $obj->price;
}
// Get extrafields (Kupferzuschlag, Preiseinheit) // Get extrafields (Kupferzuschlag, Preiseinheit, Kaufmenge)
$sql_extra = "SELECT kupferzuschlag, preiseinheit"; $sql_extra = "SELECT kupferzuschlag, preiseinheit, kaufmenge";
$sql_extra .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields"; $sql_extra .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
$sql_extra .= " WHERE fk_object = ".((int)$obj->rowid); $sql_extra .= " WHERE fk_object = ".((int)$obj->rowid);
@ -1313,6 +1436,7 @@ function getSupplierPriceDetails($db, $product_id, $fk_soc)
$extra = $db->fetch_object($res_extra); $extra = $db->fetch_object($res_extra);
$result['kupferzuschlag'] = !empty($extra->kupferzuschlag) ? (float)$extra->kupferzuschlag : 0; $result['kupferzuschlag'] = !empty($extra->kupferzuschlag) ? (float)$extra->kupferzuschlag : 0;
$result['preiseinheit'] = !empty($extra->preiseinheit) ? (int)$extra->preiseinheit : 1; $result['preiseinheit'] = !empty($extra->preiseinheit) ? (int)$extra->preiseinheit : 1;
$result['kaufmenge'] = !empty($extra->kaufmenge) ? (int)$extra->kaufmenge : 0;
} }
} }
@ -1393,14 +1517,21 @@ function buildComparisonResult($product, $datanorm)
global $db; global $db;
$fk_soc = $datanorm->fk_soc; $fk_soc = $datanorm->fk_soc;
// Get supplier price details including extrafields (Kupferzuschlag) // Get supplier price details including extrafields (Kupferzuschlag, Kaufmenge)
$priceDetails = getSupplierPriceDetails($db, $product->fk_product, $fk_soc); $priceDetails = getSupplierPriceDetails($db, $product->fk_product, $fk_soc);
$current_price = $priceDetails['unitprice']; $current_total_price = $priceDetails['price'];
$current_quantity = $priceDetails['quantity'];
$current_kaufmenge = $priceDetails['kaufmenge']; // Actual purchase quantity (if set)
$current_kupferzuschlag = $priceDetails['kupferzuschlag']; $current_kupferzuschlag = $priceDetails['kupferzuschlag'];
// Use kaufmenge if set, otherwise fall back to quantity
$effective_quantity = ($current_kaufmenge > 0) ? $current_kaufmenge : $current_quantity;
$current_unit_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price;
// Calculate unit price (Datanorm price may be per price_unit pieces) // Calculate unit price (Datanorm price may be per price_unit pieces)
// Datanorm metal_surcharge is usually 0 for Sonepar - use extrafield from invoice instead // Datanorm metal_surcharge is usually 0 for Sonepar - use extrafield from invoice instead
$price_unit = (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) ? $datanorm->price_unit : 1; $price_unit = (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) ? $datanorm->price_unit : 1;
$datanorm_price_unit_code = isset($datanorm->price_unit_code) ? $datanorm->price_unit_code : -1;
$datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0; $datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
// Priority for surcharge: 1) Invoice extrafield, 2) Datanorm // Priority for surcharge: 1) Invoice extrafield, 2) Datanorm
@ -1412,14 +1543,43 @@ function buildComparisonResult($product, $datanorm)
$surcharge_source = 'datanorm'; $surcharge_source = 'datanorm';
} }
// Calculate prices // Calculate prices for comparison
// IMPORTANT: Dolibarr price already INCLUDES kupferzuschlag! Don't add it again!
// Datanorm price is WITHOUT kupferzuschlag, so add it for comparison
// Compare on UNIT PRICE basis (per 1 piece/meter)
//
// Example: Kabel NYM-J 5x10
// - Dolibarr: 331,27€ for 50m (includes 254,55€ Cu for 50m) → 6,63€/m
// - Datanorm: 168,50€ for 100m (PE=100) + Cu must be scaled to 100m
// - Cu per unit: 254,55€ / 50m = 5,09€/m → for 100m = 509,10€
// - Datanorm total for 100m: 168,50€ + 509,10€ = 677,60€ → 6,78€/m
// Calculate Cu per unit (from Dolibarr's quantity basis)
$cu_per_unit = ($current_kupferzuschlag > 0 && $effective_quantity > 0)
? $current_kupferzuschlag / $effective_quantity
: 0;
// Scale Cu to Datanorm's price_unit basis
$cu_for_price_unit = $cu_per_unit * $price_unit;
// Dolibarr: unit price (already includes Cu)
$current_compare_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price;
// Datanorm: material price + scaled Cu, then to unit price
$datanorm_compare_price = ($datanorm->price + $cu_for_price_unit) / $price_unit;
// For display: always show unit prices
$datanorm_material_unit_price = $datanorm->price / $price_unit; $datanorm_material_unit_price = $datanorm->price / $price_unit;
$total_price_with_surcharge = $datanorm->price + ($effective_surcharge * $price_unit); $total_price_with_surcharge = $datanorm->price + $cu_for_price_unit;
$datanorm_total_unit_price = $total_price_with_surcharge / $price_unit; $datanorm_total_unit_price = $total_price_with_surcharge / $price_unit;
return array( return array(
'product_id' => $product->fk_product, 'product_id' => $product->fk_product,
'current_price' => $current_price, 'current_price' => $current_unit_price,
'current_total_price' => $current_total_price,
'current_quantity' => $current_quantity,
'current_kaufmenge' => $current_kaufmenge,
'current_effective_quantity' => $effective_quantity,
'current_kupferzuschlag' => $current_kupferzuschlag, 'current_kupferzuschlag' => $current_kupferzuschlag,
'current_description' => $product->description, 'current_description' => $product->description,
'current_label' => $product->label, 'current_label' => $product->label,
@ -1431,12 +1591,14 @@ function buildComparisonResult($product, $datanorm)
'datanorm_price_raw' => $datanorm->price, // Raw price from DATPREIS 'datanorm_price_raw' => $datanorm->price, // Raw price from DATPREIS
'datanorm_material_price' => $datanorm->price, 'datanorm_material_price' => $datanorm->price,
'datanorm_metal_surcharge' => $datanorm_metal_surcharge, // From Datanorm (usually 0) 'datanorm_metal_surcharge' => $datanorm_metal_surcharge, // From Datanorm (usually 0)
'datanorm_price_unit_code' => $datanorm_price_unit_code,
'effective_surcharge' => $effective_surcharge, // From invoice or Datanorm 'effective_surcharge' => $effective_surcharge, // From invoice or Datanorm
'surcharge_source' => $surcharge_source, // Source of surcharge (invoice/datanorm) 'surcharge_source' => $surcharge_source, // Source of surcharge (invoice/datanorm)
'datanorm_price_unit' => $price_unit, 'datanorm_price_unit' => $price_unit,
'datanorm_product_group' => isset($datanorm->product_group) ? $datanorm->product_group : '',
'datanorm_description' => trim($datanorm->short_text1.' '.$datanorm->short_text2), 'datanorm_description' => trim($datanorm->short_text1.' '.$datanorm->short_text2),
'datanorm_label' => $datanorm->short_text1, 'datanorm_label' => $datanorm->short_text1,
'price_differs' => abs($current_price - $datanorm_material_unit_price) > 0.01, 'price_differs' => abs($current_compare_price - $datanorm_compare_price) > 0.01,
'description_differs' => $product->description != trim($datanorm->short_text1.' '.$datanorm->short_text2), 'description_differs' => $product->description != trim($datanorm->short_text1.' '.$datanorm->short_text2),
'label_differs' => $product->label != $datanorm->short_text1, 'label_differs' => $product->label != $datanorm->short_text1,
); );
@ -1464,9 +1626,31 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
// Calculate unit price (Datanorm price may be per price_unit pieces) // Calculate unit price (Datanorm price may be per price_unit pieces)
// Total price = material price + metal surcharge (for cables) // Total price = material price + metal surcharge (for cables)
$price_unit = (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) ? $datanorm->price_unit : 1; $price_unit = (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) ? $datanorm->price_unit : 1;
$metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
$total_price = $datanorm->price + $metal_surcharge; // Get existing supplier price details to get kupferzuschlag and quantity from extrafield
$datanorm_unit_price = $total_price / $price_unit; $priceDetails = getSupplierPriceDetails($db, $product_id, $fk_soc);
$current_kupferzuschlag = $priceDetails['kupferzuschlag'];
$current_quantity = $priceDetails['quantity'];
$current_kaufmenge = $priceDetails['kaufmenge'];
$effective_quantity = ($current_kaufmenge > 0) ? $current_kaufmenge : max(1, $current_quantity);
// Priority for surcharge: 1) Dolibarr extrafield (from invoice), 2) Datanorm metal_surcharge
$datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
// Scale Cu from Dolibarr's quantity to Datanorm's price_unit
// Example: Cu 152,73€ for 50m → per meter = 3,05€ → for 100m = 305,46€
$cu_per_unit = ($current_kupferzuschlag > 0 && $effective_quantity > 0)
? $current_kupferzuschlag / $effective_quantity
: 0;
$cu_for_price_unit = $cu_per_unit * $price_unit;
// Use scaled Cu, or fallback to Datanorm metal_surcharge
$effective_surcharge = ($cu_for_price_unit > 0) ? $cu_for_price_unit : $datanorm_metal_surcharge;
// Total price for price_unit includes scaled surcharge
$total_price_for_pe = $datanorm->price + $effective_surcharge;
// Unit price (per 1 piece/meter)
$datanorm_unit_price = $total_price_for_pe / $price_unit;
// Load product // Load product
$product = new Product($db); $product = new Product($db);
@ -1494,20 +1678,7 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
$updated = true; $updated = true;
} }
// Update description // Update label only (description goes to supplier price desc_fourn below)
if ($apply_description) {
$new_desc = trim($datanorm->short_text1.' '.$datanorm->short_text2);
if ($product->description != $new_desc) {
$changes[] = array(
'field' => 'description',
'old' => $old_description,
'new' => $new_desc
);
$product->description = $new_desc;
$updated = true;
}
}
// Save product changes // Save product changes
if ($updated) { if ($updated) {
$result = $product->update($product->id, $user); $result = $product->update($product->id, $user);
@ -1516,8 +1687,8 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
} }
} }
// Update supplier price // Update supplier price and/or description
if ($apply_price) { if ($apply_price || $apply_description) {
$productFourn = new ProductFournisseur($db); $productFourn = new ProductFournisseur($db);
$productFourn->fetch($product_id); $productFourn->fetch($product_id);
@ -1526,7 +1697,7 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
$supplier->fetch($fk_soc); $supplier->fetch($fk_soc);
// Find existing supplier price // Find existing supplier price
$sql = "SELECT rowid, quantity, price, unitprice FROM ".MAIN_DB_PREFIX."product_fournisseur_price"; $sql = "SELECT rowid, quantity, price, unitprice, desc_fourn FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sql .= " WHERE fk_product = ".((int)$product_id); $sql .= " WHERE fk_product = ".((int)$product_id);
$sql .= " AND fk_soc = ".((int)$fk_soc); $sql .= " AND fk_soc = ".((int)$fk_soc);
$sql .= " ORDER BY rowid DESC LIMIT 1"; $sql .= " ORDER BY rowid DESC LIMIT 1";
@ -1534,54 +1705,66 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
$resql = $db->query($sql); $resql = $db->query($sql);
if ($resql && $db->num_rows($resql) > 0) { if ($resql && $db->num_rows($resql) > 0) {
$priceObj = $db->fetch_object($resql); $priceObj = $db->fetch_object($resql);
$price_rowid = $priceObj->rowid;
// Get the actual unit price from Dolibarr (price per 1 piece) // Use effective_quantity (kaufmenge if set, otherwise quantity) for price comparison
$current_unit_price = (!empty($priceObj->unitprice) && $priceObj->unitprice > 0) // This ensures consistent comparison with buildComparisonResult()
? $priceObj->unitprice $effective_qty = ($priceDetails['kaufmenge'] > 0) ? $priceDetails['kaufmenge'] : max(1, $priceObj->quantity);
: ($priceObj->quantity > 0 ? $priceObj->price / $priceObj->quantity : $priceObj->price);
// Only update if unit price differs // Get the actual unit price from Dolibarr (price per 1 effective piece)
if (abs($current_unit_price - $datanorm_unit_price) > 0.01) { $current_unit_price = $effective_qty > 0 ? $priceObj->price / $effective_qty : $priceObj->price;
// Prepare new description if requested
$new_desc_fourn = null;
if ($apply_description) {
$new_desc_fourn = trim($datanorm->short_text1.' '.$datanorm->short_text2);
if ($priceObj->desc_fourn != $new_desc_fourn) {
$changes[] = array(
'field' => 'desc_fourn',
'old' => $priceObj->desc_fourn,
'new' => $new_desc_fourn
);
} else {
$new_desc_fourn = null; // No change needed
}
}
// Check if price needs update
$price_changed = $apply_price && (abs($current_unit_price - $datanorm_unit_price) > 0.01);
if ($price_changed) {
$changes[] = array( $changes[] = array(
'field' => 'price', 'field' => 'price',
'old' => $current_unit_price, 'old' => $current_unit_price,
'new' => $datanorm_unit_price 'new' => $datanorm_unit_price
); );
}
// Calculate total price for the quantity (Dolibarr expects total price, not unit price) // Update only the fields that need changing (preserves all other fields!)
// Dolibarr will calculate: unitprice = price / quantity if ($price_changed || $new_desc_fourn !== null) {
$total_price_for_qty = $datanorm_unit_price * $priceObj->quantity; $update_fields = array();
// Update existing price - $supplier must be Societe object, not integer ID if ($price_changed) {
$result = $productFourn->update_buyprice( // Calculate total price for the quantity, round to 2 decimals
$priceObj->quantity, $total_price_for_qty = round($datanorm_unit_price * $priceObj->quantity, 2);
$total_price_for_qty, $rounded_unit_price = round($datanorm_unit_price, 2);
$user, $update_fields[] = "price = ".((float)$total_price_for_qty);
'HT', $update_fields[] = "unitprice = ".((float)$rounded_unit_price);
$supplier, // Societe object, not integer }
0, // availability
$datanorm->article_number, // ref_fourn
0, // tva_tx
0, // charges
0, // remise_percent
0, // remise
0, // newnpr
0, // delivery_time_days
'', // supplier_reputation
array(), // localtaxes
'', // newdefaultvatcode
0, // multicurrency_buyprice
'', // multicurrency_price_base_type
0, // multicurrency_tx
'', // multicurrency_code
'', // desc_fourn
'', // barcode
0, // fk_barcode_type
array() // options
);
if ($result < 0) { if ($new_desc_fourn !== null) {
return -4; $update_fields[] = "desc_fourn = '".$db->escape($new_desc_fourn)."'";
}
if (!empty($update_fields)) {
$sql_update = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price";
$sql_update .= " SET ".implode(", ", $update_fields);
$sql_update .= " WHERE rowid = ".(int)$price_rowid;
$result = $db->query($sql_update);
if (!$result) {
return -4;
}
} }
} }
} }

1189
import.php

File diff suppressed because it is too large Load diff

View file

@ -54,6 +54,9 @@ $langs->loadLangs(array("importzugferd@importzugferd"));
if (!isModEnabled('importzugferd')) { if (!isModEnabled('importzugferd')) {
accessforbidden('Module not enabled'); accessforbidden('Module not enabled');
} }
if (!$user->hasRight('importzugferd', 'import', 'read')) {
accessforbidden();
}
/* /*
* View * View

View file

@ -451,6 +451,8 @@ LabelChange = Namensänderung
# #
Kupferzuschlag = Kupferzuschlag Kupferzuschlag = Kupferzuschlag
KupferzuschlagHelp = Metallzuschlag pro Einheit (wird aus Rechnungen extrahiert) KupferzuschlagHelp = Metallzuschlag pro Einheit (wird aus Rechnungen extrahiert)
Produktpreis = Produktpreis
ProduktpreisHelp = Reiner Materialpreis ohne Kupferzuschlag (nur bei Kabeln)
Preiseinheit = Preiseinheit Preiseinheit = Preiseinheit
PreiseinheitHelp = Anzahl Einheiten pro Preis (z.B. 100 = Preis pro 100 Stück) PreiseinheitHelp = Anzahl Einheiten pro Preis (z.B. 100 = Preis pro 100 Stück)
Warengruppe = Warengruppe Warengruppe = Warengruppe
@ -486,3 +488,10 @@ AddSelectedPrices = Ausgewählte hinzufügen
SupplierPricesAdded = %s Lieferantenpreise hinzugefügt SupplierPricesAdded = %s Lieferantenpreise hinzugefügt
CheaperBy = %s%% günstiger CheaperBy = %s%% günstiger
MoreExpensiveBy = %s%% teurer 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

View file

@ -389,6 +389,8 @@ NotifyEmail = Recipient email
# #
Kupferzuschlag = Copper Surcharge Kupferzuschlag = Copper Surcharge
KupferzuschlagHelp = Metal surcharge per unit (extracted from invoices) KupferzuschlagHelp = Metal surcharge per unit (extracted from invoices)
Produktpreis = Material Price
ProduktpreisHelp = Material price without copper surcharge (cables only)
Preiseinheit = Price Unit Preiseinheit = Price Unit
PreiseinheitHelp = Number of units per price (e.g. 100 = price per 100 pieces) PreiseinheitHelp = Number of units per price (e.g. 100 = price per 100 pieces)
Warengruppe = Product Group Warengruppe = Product Group
@ -417,3 +419,10 @@ AddSelectedPrices = Add Selected
SupplierPricesAdded = %s supplier prices added SupplierPricesAdded = %s supplier prices added
CheaperBy = %s%% cheaper CheaperBy = %s%% cheaper
MoreExpensiveBy = %s%% more expensive 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

View file

@ -15,3 +15,5 @@ ALTER TABLE llx_importzugferd_datanorm ADD COLUMN price_type tinyint DEFAULT 1 A
ALTER TABLE llx_importzugferd_datanorm ADD COLUMN metal_surcharge double(24,8) DEFAULT 0 AFTER price_type; ALTER TABLE llx_importzugferd_datanorm ADD COLUMN metal_surcharge double(24,8) DEFAULT 0 AFTER price_type;
ALTER TABLE llx_importzugferd_datanorm ADD COLUMN vpe integer DEFAULT NULL AFTER metal_surcharge; ALTER TABLE llx_importzugferd_datanorm ADD COLUMN vpe integer DEFAULT NULL AFTER metal_surcharge;
ALTER TABLE llx_importzugferd_datanorm ADD COLUMN action_code char(1) DEFAULT 'N' AFTER datanorm_version; ALTER TABLE llx_importzugferd_datanorm ADD COLUMN action_code char(1) DEFAULT 'N' AFTER datanorm_version;
-- Note: kaufmenge extrafield is created programmatically in modImportZugferd.class.php init()