Compare commits
No commits in common. "v3.3" and "main" have entirely different histories.
32 changed files with 2135 additions and 19586 deletions
Binary file not shown.
|
|
@ -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
145
CHANGELOG.md
Executable 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
|
||||||
128
ChangeLog.md
128
ChangeLog.md
|
|
@ -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
|
||||||
|
|
|
||||||
44
README.md
44
README.md
|
|
@ -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
BIN
bin/module_importzugferd-4.4.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-4.5.zip
Executable file
BIN
bin/module_importzugferd-4.5.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-4.6.zip
Executable file
BIN
bin/module_importzugferd-4.6.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-4.8.zip
Executable file
BIN
bin/module_importzugferd-4.8.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-4.9.zip
Executable file
BIN
bin/module_importzugferd-4.9.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.0.zip
Executable file
BIN
bin/module_importzugferd-5.0.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.1.zip
Executable file
BIN
bin/module_importzugferd-5.1.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.2.zip
Executable file
BIN
bin/module_importzugferd-5.2.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.3.zip
Executable file
BIN
bin/module_importzugferd-5.3.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.4.zip
Executable file
BIN
bin/module_importzugferd-5.4.zip
Executable file
Binary file not shown.
11
card.php
11
card.php
|
|
@ -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('/(<\/?)([\w:.-]+)/', '$1<span style="color:#2271b1;font-weight:bold;">$2</span>', $highlightedXml);
|
||||||
|
// Attribut-Namen und -Werte
|
||||||
|
$highlightedXml = preg_replace('/ ([\w:.-]+)(=)(")(.*?)(")/', ' <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>';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ' ';
|
||||||
|
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 ' ';
|
||||||
|
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
1189
import.php
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue