Compare commits
No commits in common. "main" and "v3.3" have entirely different histories.
32 changed files with 19585 additions and 2134 deletions
BIN
2026-01-27-07-40-37-Zugferd-ZUGFERD2493150.pdf
Executable file
BIN
2026-01-27-07-40-37-Zugferd-ZUGFERD2493150.pdf
Executable file
Binary file not shown.
40
.claude/settings.local.json
Executable file
40
.claude/settings.local.json
Executable file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"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:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
18971
2026-02-04 - Zugferd Rechnung - Sonepar - 9010548449 - 3581,33 EUR.pdf
Executable file
18971
2026-02-04 - Zugferd Rechnung - Sonepar - 9010548449 - 3581,33 EUR.pdf
Executable file
File diff suppressed because one or more lines are too long
145
CHANGELOG.md
145
CHANGELOG.md
|
|
@ -1,145 +0,0 @@
|
||||||
# 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,107 +1,43 @@
|
||||||
# Changelog
|
# CHANGELOG MODULE IMPORTZUGFERD FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
|
||||||
|
|
||||||
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
|
## 3.2
|
||||||
|
|
||||||
## [4.2] - 2026-03-02
|
### Neue Funktionen
|
||||||
|
- 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
|
||||||
|
|
||||||
### Behoben
|
### Bugfixes
|
||||||
- **PDF-Anhänge**: ZUGFeRD-PDFs werden jetzt korrekt an Lieferantenrechnungen angehängt
|
- Datanorm Import: Kluxen-Format (Preise im A-Record in Cent) wird jetzt korrekt verarbeitet
|
||||||
- Problem: PDF wurde nur ins Dateisystem kopiert, nicht in ECM-Datenbank registriert
|
- Datanorm Import: Preise aus A-Record werden von Cent in Euro umgerechnet (geteilt durch 100)
|
||||||
- Lösung: `EcmFiles`-Eintrag wird erstellt für korrekte Verknüpfung mit Rechnung
|
|
||||||
- Wichtig: Bei Rechnungsvalidierung wird PDF automatisch mitverschoben
|
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinweise
|
||||||
- **Bezeichnung in Rechnungsliste**: Teuerster Artikel wird als Bezeichnung der Lieferantenrechnung gesetzt
|
- Kluxen-Katalog enthaelt nur Listenpreise (UVP), keine Netto-Einkaufspreise
|
||||||
- Erleichtert schnelle Identifikation in der Rechnungsliste
|
- Cross-Katalog-Suche erfordert aktivierte Einstellung "In allen Lieferanten-Katalogen suchen"
|
||||||
- Spalte "Bezeichnung" muss in Liste aktiviert sein
|
|
||||||
|
|
||||||
## [4.0] - 2026-03-01
|
## 2.1
|
||||||
|
|
||||||
### Behoben
|
### Bugfixes
|
||||||
- **Verschachtelte HTML-Forms**: "Ausgewählte Preise hinzufügen" funktionierte nicht, weil Browser verschachtelte `<form>`-Elemente nicht unterstützen. Lösung: HTML5 `form`-Attribut
|
- Rechnungsimport: Preise wurden falsch als Brutto (TTC) statt Netto (HT) behandelt - korrigierte Parameterreihenfolge in addline()
|
||||||
- **Stückpreis-Anzeige**: Einkaufspreise zeigen jetzt den Stückpreis statt Gesamtpreis (z.B. 0,16 statt 16,32 bei 100 Stk.)
|
- Datanorm Massenaktualisierung: Lieferantenauswahl ging nach Aktionen verloren - Redirects hinzugefuegt
|
||||||
- **Preisvergleich**: Fehlende Lieferantenpreise werden korrekt auf Stückpreis-Basis verglichen
|
- Datanorm Massenaktualisierung: "Alle Aenderungen uebernehmen" Button war nicht sichtbar ohne Suchergebnisse
|
||||||
- **DATPREIS-Kommentare**: Feld korrekt als Rabattkennzeichen dokumentiert (war fälschlich als PE-Code beschrieben)
|
- Datanorm Massenaktualisierung: Filter-Auswahl (Preis/Beschreibung/Bezeichnung) wurde bei "Alle hinzufuegen" ignoriert
|
||||||
|
- ProductFournisseur::update_buyprice erwartet Societe-Objekt, nicht Integer-ID
|
||||||
|
|
||||||
### Verbessert
|
### Verbesserungen
|
||||||
- **Feedback bei Preishinzufügen**: Zeigt Erfolgs- und Fehlermeldungen nach dem Hinzufügen von Lieferantenpreisen
|
- Bestaetungsdialog fuer Massenaktionen verwendet jetzt Dolibarr jQuery UI Dialog statt JavaScript confirm()
|
||||||
- **Mengenkontext**: Bei Mengenstaffel wird zusätzlich der Gesamtpreis mit Stückzahl angezeigt (z.B. "0,16 (16,32/100Stk.)")
|
- Manuelles Metallzuschlag-Eingabefeld entfernt (nicht mehr benoetigt - Kupferzuschlag wird aus ZUGFeRD XML extrahiert)
|
||||||
|
- Ausstehende Aenderungen werden immer angezeigt wenn vorhanden, unabhaengig von Suchergebnissen
|
||||||
|
|
||||||
### Hinweis
|
## 2.0
|
||||||
- `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
|
- Datanorm 4.0/5.0 Katalog-Import
|
||||||
|
- Kupferzuschlag-Extraktion aus ZUGFeRD XML (AllowanceCharge)
|
||||||
|
- Automatischer Preisvergleich zwischen Datanorm und aktuellen Einkaufspreisen
|
||||||
|
- Massenaktualisierung von Produktpreisen und Beschreibungen
|
||||||
|
- Aenderungsprotokoll fuer Preisanpassungen
|
||||||
|
|
||||||
### Hinzugefügt
|
## 1.0
|
||||||
- **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
|
Initial version
|
||||||
- **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,39 +102,23 @@ Available in:
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
|
### 1.1
|
||||||
|
- New persistent import workflow with database storage
|
||||||
### 5.7 (Current)
|
- Manual product assignment via dropdown
|
||||||
- Fixed PDF path for Cron/Batch imports - now correctly saved to `/imports/{id}/{filename}`
|
- Product removal/reassignment
|
||||||
- Added fallback for old PDF paths when attaching to supplier invoices
|
- Status "Pending" for imports requiring manual intervention
|
||||||
|
- Pending imports overview on upload page
|
||||||
### 5.5
|
- UN/ECE unit code translation (C62 → Stk., MTR → m, etc.)
|
||||||
- Fixed copper surcharge scaling in mass update (different quantities between Dolibarr and Datanorm)
|
- Batch import from folder or IMAP mailbox
|
||||||
- Fixed VAT rate preservation when updating prices
|
- IMAP connection test with folder selection
|
||||||
- New filters for price direction (up/down) in mass update
|
- Product template feature (duplicate existing product)
|
||||||
- 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
|
||||||
|
|
||||||
|
|
|
||||||
BIN
bin/module_importzugferd-2.2.zip
Executable file
BIN
bin/module_importzugferd-2.2.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-2.3.zip
Executable file
BIN
bin/module_importzugferd-2.3.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-2.4.zip
Executable file
BIN
bin/module_importzugferd-2.4.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-2.5.zip
Executable file
BIN
bin/module_importzugferd-2.5.zip
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
11
card.php
11
card.php
|
|
@ -244,13 +244,6 @@ 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">';
|
||||||
|
|
@ -261,8 +254,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: 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 '<pre style="max-height: 500px; overflow: auto; background: #f5f5f5; padding: 10px; font-size: 11px; white-space: pre-wrap; word-wrap: break-word;">';
|
||||||
print $highlightedXml;
|
print dol_escape_htmltag($formattedXml);
|
||||||
print '</pre>';
|
print '</pre>';
|
||||||
print '</div>';
|
print '</div>';
|
||||||
print '</td>';
|
print '</td>';
|
||||||
|
|
|
||||||
|
|
@ -316,32 +316,6 @@ 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,7 +14,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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');
|
||||||
|
|
@ -61,67 +60,6 @@ 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
|
||||||
*
|
*
|
||||||
|
|
@ -129,45 +67,7 @@ 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) ==========");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -227,16 +127,15 @@ class CronImportZugferd
|
||||||
*/
|
*/
|
||||||
public function runScheduledImport()
|
public function runScheduledImport()
|
||||||
{
|
{
|
||||||
global $langs;
|
global $conf, $user, $langs;
|
||||||
|
|
||||||
// Initialize timing and shutdown handler
|
|
||||||
$this->startTime = microtime(true);
|
|
||||||
register_shutdown_function(array($this, 'handleShutdown'));
|
|
||||||
|
|
||||||
$langs->load('importzugferd@importzugferd');
|
$langs->load('importzugferd@importzugferd');
|
||||||
|
|
||||||
$this->cronLog("========== CRON START ==========");
|
// Check if we should run based on frequency
|
||||||
$this->cronLog("PHP Version: ".PHP_VERSION.", Memory Limit: ".ini_get('memory_limit'));
|
if (!$this->shouldRunImport()) {
|
||||||
|
$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;
|
||||||
|
|
@ -244,69 +143,25 @@ class CronImportZugferd
|
||||||
$this->error_count = 0;
|
$this->error_count = 0;
|
||||||
$this->errors = array();
|
$this->errors = array();
|
||||||
|
|
||||||
try {
|
$folderResult = $this->importFromFolder();
|
||||||
$this->cronLog("Starting folder import...");
|
$mailboxResult = $this->fetchFromMailbox();
|
||||||
$this->importFromFolder();
|
|
||||||
$this->cronLog("Folder import completed");
|
|
||||||
|
|
||||||
// IMAP nur wenn konfiguriert
|
// Update last run time
|
||||||
if (!empty(getDolGlobalString('IMPORTZUGFERD_IMAP_HOST'))) {
|
$this->updateLastRunTime();
|
||||||
$this->cronLog("Starting IMAP import...");
|
|
||||||
$this->fetchFromMailbox();
|
|
||||||
$this->cronLog("IMAP import completed");
|
|
||||||
} else {
|
|
||||||
$this->cronLog("IMAP not configured - skipping");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last run time
|
// Build combined output
|
||||||
$this->updateLastRunTime();
|
$this->output = sprintf(
|
||||||
|
"Scheduled import complete. Imported: %d, Skipped: %d, Errors: %d",
|
||||||
|
$this->imported_count,
|
||||||
|
$this->skipped_count,
|
||||||
|
$this->error_count
|
||||||
|
);
|
||||||
|
|
||||||
// Build combined output
|
if ($this->error_count > 0 && !empty($this->errors)) {
|
||||||
$this->output = sprintf(
|
$this->output .= "\nErrors: " . implode(", ", array_slice($this->errors, 0, 5));
|
||||||
"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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -316,7 +171,7 @@ class CronImportZugferd
|
||||||
*/
|
*/
|
||||||
public function importFromFolder()
|
public function importFromFolder()
|
||||||
{
|
{
|
||||||
global $langs;
|
global $conf, $user, $langs;
|
||||||
|
|
||||||
$langs->load('importzugferd@importzugferd');
|
$langs->load('importzugferd@importzugferd');
|
||||||
|
|
||||||
|
|
@ -325,45 +180,24 @@ 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)) {
|
if (empty($watchFolder) || !is_dir($watchFolder)) {
|
||||||
$this->cronLog("Watch folder not configured - skipping");
|
$this->output = 'Watch folder not configured or not accessible';
|
||||||
$this->output = 'Watch folder not configured';
|
return 0; // Not an error, just 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');
|
||||||
$this->cronLog("Found ".count($files)." .pdf files");
|
$files = array_merge($files, glob($watchFolder . '/*.PDF'));
|
||||||
|
|
||||||
$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);
|
||||||
|
|
@ -375,22 +209,20 @@ 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, !empty($autoCreate));
|
$result = $import->importFromFile($admin_user, $file, $autoCreate);
|
||||||
|
|
||||||
if ($result > 0) {
|
if ($result > 0) {
|
||||||
$this->imported_count++;
|
$this->imported_count++;
|
||||||
$this->cronLog("Imported: ".basename($file)." -> ID {$result}");
|
dol_syslog("CronImportZugferd: Imported invoice from folder: " . basename($file), LOG_INFO);
|
||||||
|
|
||||||
// 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++;
|
||||||
$this->cronLog("Skipped (duplicate): ".basename($file));
|
dol_syslog("CronImportZugferd: Skipped duplicate invoice: " . basename($file), LOG_INFO);
|
||||||
|
|
||||||
// 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_')) {
|
||||||
|
|
@ -399,7 +231,7 @@ class CronImportZugferd
|
||||||
} else {
|
} else {
|
||||||
$this->error_count++;
|
$this->error_count++;
|
||||||
$this->errors[] = basename($file) . ': ' . $import->error;
|
$this->errors[] = basename($file) . ': ' . $import->error;
|
||||||
$this->cronLog("Error importing ".basename($file).": ".$import->error, 'ERROR');
|
dol_syslog("CronImportZugferd: Error importing: " . basename($file) . " - " . $import->error, LOG_WARNING);
|
||||||
|
|
||||||
// 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_')) {
|
||||||
|
|
@ -409,7 +241,6 @@ class CronImportZugferd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->cronLog("Folder import finished: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}");
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -666,17 +497,7 @@ class CronImportZugferd
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_dir($targetFolder) && is_writable($targetFolder)) {
|
if (is_dir($targetFolder) && is_writable($targetFolder)) {
|
||||||
// Originalen Dateinamen beibehalten, bei Namenskollision Zaehler anhaengen
|
$targetPath = $targetFolder . '/' . $prefix . date('Y-m-d_His') . '_' . basename($file);
|
||||||
$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);
|
||||||
|
|
@ -713,79 +534,4 @@ 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,22 +520,9 @@ 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 from Datanorm
|
// Store EAN and manufacturer_ref for cross-catalog search
|
||||||
$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) {
|
||||||
|
|
@ -544,15 +531,24 @@ class Datanorm extends CommonObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If searchAll is enabled and we found article with EAN,
|
// If searchAll is enabled and we found article with EAN/manufacturer_ref,
|
||||||
// search other catalogs using EAN ONLY (cross-catalog search)
|
// search other catalogs using these identifiers (cross-catalog search)
|
||||||
// Note: Artikelnummern-Vergleich macht keinen Sinn über Kataloge hinweg
|
if ($searchAll && $fk_soc > 0 && (!empty($foundEan) || !empty($foundManufacturerRef))) {
|
||||||
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 ean = '" . $this->db->escape($foundEan) . "'";
|
$sql .= " WHERE (";
|
||||||
|
|
||||||
|
$conditions = array();
|
||||||
|
if (!empty($foundEan)) {
|
||||||
|
$conditions[] = "ean = '" . $this->db->escape($foundEan) . "'";
|
||||||
|
}
|
||||||
|
if (!empty($foundManufacturerRef)) {
|
||||||
|
$conditions[] = "manufacturer_ref = '" . $this->db->escape($foundManufacturerRef) . "'";
|
||||||
|
}
|
||||||
|
$sql .= implode(' OR ', $conditions) . ")";
|
||||||
|
|
||||||
$sql .= " 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
|
||||||
|
|
@ -592,44 +588,55 @@ class Datanorm extends CommonObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Search by EXACT article number match for the specified supplier only
|
// Fallback: Search by partial match on article_number, ean, or manufacturer_ref
|
||||||
// No LIKE search - cross-catalog comparisons only work via EAN
|
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
|
||||||
if ($fk_soc > 0 && empty($results)) {
|
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
|
||||||
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
|
$sql .= " price, price_unit, discount_group, product_group, matchcode";
|
||||||
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
|
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||||
$sql .= " price, price_unit, discount_group, product_group, matchcode";
|
$sql .= " WHERE (article_number LIKE '" . $this->db->escape($article_number) . "%'";
|
||||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
$sql .= " OR ean = '" . $this->db->escape($article_number) . "'";
|
||||||
$sql .= " WHERE article_number = '" . $this->db->escape($article_number) . "'";
|
$sql .= " OR manufacturer_ref LIKE '" . $this->db->escape($article_number) . "%')";
|
||||||
$sql .= " AND fk_soc = " . (int) $fk_soc;
|
$sql .= " AND active = 1";
|
||||||
$sql .= " AND active = 1";
|
$sql .= " AND entity = " . (int) $conf->entity;
|
||||||
$sql .= " AND entity = " . (int) $conf->entity;
|
|
||||||
$sql .= " LIMIT 1";
|
|
||||||
|
|
||||||
$resql = $this->db->query($sql);
|
if ($fk_soc > 0 && !$searchAll) {
|
||||||
if ($resql) {
|
$sql .= " AND fk_soc = " . (int) $fk_soc;
|
||||||
while ($obj = $this->db->fetch_object($resql)) {
|
}
|
||||||
if (!isset($foundIds[$obj->rowid])) {
|
|
||||||
$results[] = array(
|
// ORDER BY clause
|
||||||
'id' => $obj->rowid,
|
if ($fk_soc > 0 && $searchAll) {
|
||||||
'fk_soc' => $obj->fk_soc,
|
// Order by matching supplier first, then by price
|
||||||
'article_number' => $obj->article_number,
|
$sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, price ASC";
|
||||||
'short_text1' => $obj->short_text1,
|
} else {
|
||||||
'short_text2' => $obj->short_text2,
|
$sql .= " ORDER BY article_number";
|
||||||
'ean' => $obj->ean,
|
}
|
||||||
'manufacturer_ref' => $obj->manufacturer_ref,
|
|
||||||
'manufacturer_name' => $obj->manufacturer_name,
|
$sql .= " LIMIT " . (int) $limit;
|
||||||
'unit_code' => $obj->unit_code,
|
|
||||||
'price' => $obj->price,
|
$resql = $this->db->query($sql);
|
||||||
'price_unit' => $obj->price_unit,
|
if ($resql) {
|
||||||
'discount_group' => $obj->discount_group,
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
'product_group' => $obj->product_group,
|
if (!isset($foundIds[$obj->rowid])) {
|
||||||
'matchcode' => $obj->matchcode,
|
$results[] = array(
|
||||||
);
|
'id' => $obj->rowid,
|
||||||
$foundIds[$obj->rowid] = true;
|
'fk_soc' => $obj->fk_soc,
|
||||||
}
|
'article_number' => $obj->article_number,
|
||||||
|
'short_text1' => $obj->short_text1,
|
||||||
|
'short_text2' => $obj->short_text2,
|
||||||
|
'ean' => $obj->ean,
|
||||||
|
'manufacturer_ref' => $obj->manufacturer_ref,
|
||||||
|
'manufacturer_name' => $obj->manufacturer_name,
|
||||||
|
'unit_code' => $obj->unit_code,
|
||||||
|
'price' => $obj->price,
|
||||||
|
'price_unit' => $obj->price_unit,
|
||||||
|
'discount_group' => $obj->discount_group,
|
||||||
|
'product_group' => $obj->product_group,
|
||||||
|
'matchcode' => $obj->matchcode,
|
||||||
|
);
|
||||||
|
$foundIds[$obj->rowid] = true;
|
||||||
}
|
}
|
||||||
$this->db->free($resql);
|
|
||||||
}
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
|
|
@ -975,13 +982,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;...
|
||||||
// Rabattkennzeichen aus DATPREIS (wird gespeichert aber nicht fuer price_unit verwendet)
|
// PE is the price unit code from DATPREIS (may differ from A-record!)
|
||||||
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'); // Rabattkennzeichen (nicht PE!)
|
$datpreisPeCode = (int)trim($parts[$i + 3] ?? '0'); // PE code from DATPREIS
|
||||||
$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
|
||||||
|
|
@ -1000,7 +1007,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'); // Rabattkennzeichen (nicht PE!)
|
$datpreisPeCode = (int)trim($parts[4] ?? '0'); // PE code if available
|
||||||
|
|
||||||
if (strpos($priceRaw, ',') === false && strpos($priceRaw, '.') === false) {
|
if (strpos($priceRaw, ',') === false && strpos($priceRaw, '.') === false) {
|
||||||
$price = (float)$priceRaw / 100;
|
$price = (float)$priceRaw / 100;
|
||||||
|
|
|
||||||
|
|
@ -624,32 +624,6 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -675,12 +649,12 @@ class ZugferdImport extends CommonObject
|
||||||
$this->status = self::STATUS_IMPORTED;
|
$this->status = self::STATUS_IMPORTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy PDF to documents (in subfolder by import ID)
|
// Copy PDF to documents
|
||||||
$destdir = $conf->importzugferd->dir_output . '/imports/' . $this->id;
|
$destdir = $conf->importzugferd->dir_output . '/imports';
|
||||||
if (!is_dir($destdir)) {
|
if (!is_dir($destdir)) {
|
||||||
dol_mkdir($destdir);
|
dol_mkdir($destdir);
|
||||||
}
|
}
|
||||||
$destfile = $destdir . '/' . $this->pdf_filename;
|
$destfile = $destdir . '/' . $this->ref . '_' . basename($file_path);
|
||||||
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 = '5.5';
|
$this->version = '3.3';
|
||||||
// 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,173 +573,113 @@ 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', // 1. attribute code
|
'supplier_customer_number', // attribute code
|
||||||
'SupplierCustomerNumber', // 2. label (translation key)
|
'SupplierCustomerNumber', // label (translation key)
|
||||||
'varchar', // 3. type
|
'varchar', // type
|
||||||
100, // 4. position
|
100, // position
|
||||||
64, // 5. size
|
64, // size
|
||||||
'thirdparty', // 6. element type
|
'thirdparty', // element type
|
||||||
0, // 7. unique
|
0, // unique
|
||||||
0, // 8. required
|
0, // required
|
||||||
'', // 9. default value
|
'', // default value
|
||||||
'', // 10. param
|
'', // param
|
||||||
1, // 11. always editable
|
1, // always editable
|
||||||
'', // 12. permission
|
'', // permission
|
||||||
1, // 13. list (show in list)
|
1, // list (show in list)
|
||||||
'', // 14. help
|
0, // printable
|
||||||
'', // 15. computed
|
'', // totalizable
|
||||||
'', // 16. entity
|
'', // langfile
|
||||||
'importzugferd@importzugferd', // 17. langfile
|
'importzugferd@importzugferd', // module
|
||||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
'isModEnabled("importzugferd")' // 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', // 1. attribute code
|
'kupferzuschlag', // attribute code
|
||||||
'Kupferzuschlag', // 2. label
|
'Kupferzuschlag', // label (translation key)
|
||||||
'price', // 3. type (price field)
|
'price', // type (price field)
|
||||||
110, // 4. position
|
110, // position
|
||||||
'24,8', // 5. size
|
'24,8', // size
|
||||||
'product_fournisseur_price', // 6. element type
|
'product_fournisseur_price', // element type
|
||||||
0, // 7. unique
|
0, // unique
|
||||||
0, // 8. required
|
0, // required
|
||||||
'', // 9. default value
|
'', // default value
|
||||||
'', // 10. param
|
'', // param
|
||||||
1, // 11. always editable
|
1, // always editable
|
||||||
'', // 12. permission
|
'', // permission
|
||||||
1, // 13. list (show in list)
|
1, // list (show in list)
|
||||||
'Metallzuschlag (Kupfer) für diesen Einkaufspreis', // 14. help
|
0, // printable
|
||||||
'', // 15. computed
|
'', // totalizable
|
||||||
'', // 16. entity
|
'', // langfile
|
||||||
'importzugferd@importzugferd', // 17. langfile
|
'importzugferd@importzugferd', // module
|
||||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
'isModEnabled("importzugferd")' // 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', // 1. attribute code
|
'preiseinheit', // attribute code
|
||||||
'Preiseinheit', // 2. label
|
'Preiseinheit', // label (translation key)
|
||||||
'int', // 3. type
|
'int', // type
|
||||||
120, // 4. position
|
120, // position
|
||||||
11, // 5. size
|
11, // size
|
||||||
'product_fournisseur_price', // 6. element type
|
'product_fournisseur_price', // element type
|
||||||
0, // 7. unique
|
0, // unique
|
||||||
0, // 8. required
|
0, // required
|
||||||
'1', // 9. default value
|
'1', // default value
|
||||||
'', // 10. param
|
'', // param
|
||||||
1, // 11. always editable
|
1, // always editable
|
||||||
'', // 12. permission
|
'', // permission
|
||||||
1, // 13. list (show in list)
|
1, // list (show in list)
|
||||||
'Preiseinheit aus Datanorm (z.B. 100 für Preis pro 100m)', // 14. help
|
0, // printable
|
||||||
'', // 15. computed
|
'', // totalizable
|
||||||
'', // 16. entity
|
'', // langfile
|
||||||
'importzugferd@importzugferd', // 17. langfile
|
'importzugferd@importzugferd', // module
|
||||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
'isModEnabled("importzugferd")' // 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', // 1. attribute code
|
'warengruppe', // attribute code
|
||||||
'Warengruppe', // 2. label
|
'Warengruppe', // label (translation key)
|
||||||
'varchar', // 3. type
|
'varchar', // type
|
||||||
125, // 4. position
|
125, // position
|
||||||
32, // 5. size
|
32, // size
|
||||||
'product_fournisseur_price', // 6. element type
|
'product_fournisseur_price', // element type
|
||||||
0, // 7. unique
|
0, // unique
|
||||||
0, // 8. required
|
0, // required
|
||||||
'', // 9. default value
|
'', // default value
|
||||||
'', // 10. param
|
'', // param
|
||||||
1, // 11. always editable
|
1, // always editable
|
||||||
'', // 12. permission
|
'', // permission
|
||||||
1, // 13. list (show in list)
|
1, // list (show in list)
|
||||||
'Datanorm-Warengruppe', // 14. help
|
0, // printable
|
||||||
'', // 15. computed
|
'', // totalizable
|
||||||
'', // 16. entity
|
'', // langfile
|
||||||
'importzugferd@importzugferd', // 17. langfile
|
'importzugferd@importzugferd', // module
|
||||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
'isModEnabled("importzugferd")' // 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', // 1. attribute code
|
'kupfergehalt', // attribute code
|
||||||
'Kupfergehalt', // 2. label
|
'Kupfergehalt', // label (translation key)
|
||||||
'double', // 3. type (decimal number)
|
'double', // type (decimal number)
|
||||||
130, // 4. position
|
130, // position
|
||||||
'24,4', // 5. size (precision,scale)
|
'24,4', // size (precision,scale)
|
||||||
'product', // 6. element type
|
'product', // element type
|
||||||
0, // 7. unique
|
0, // unique
|
||||||
0, // 8. required
|
0, // required
|
||||||
'', // 9. default value
|
'', // default value
|
||||||
'', // 10. param
|
'', // param
|
||||||
1, // 11. always editable
|
1, // always editable
|
||||||
'', // 12. permission
|
'', // permission
|
||||||
1, // 13. list (show in list)
|
1, // list (show in list)
|
||||||
'Kupfergehalt in kg/km (für Kupferzuschlag-Berechnung bei Kabeln)', // 14. help
|
0, // printable
|
||||||
'', // 15. computed
|
'', // totalizable
|
||||||
'', // 16. entity
|
'', // langfile
|
||||||
'importzugferd@importzugferd', // 17. langfile
|
'importzugferd@importzugferd', // module
|
||||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
'isModEnabled("importzugferd")' // enabled condition
|
||||||
0, // 19. totalizable
|
|
||||||
0 // 20. printable
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
|
|
|
||||||
|
|
@ -79,18 +79,12 @@ 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
|
||||||
|
|
@ -123,8 +117,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 (preserve all filters)
|
// Redirect to same page with same parameters
|
||||||
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');
|
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);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,8 +137,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 and filters
|
// Redirect back with same parameters to preserve supplier selection
|
||||||
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');
|
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');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,8 +192,8 @@ if ($action == 'add_all_pending') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back with same parameters to preserve supplier selection and filters
|
// Redirect back with same parameters to preserve supplier selection
|
||||||
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');
|
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');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,7 +322,8 @@ if ($obj->cnt == 0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search form
|
// Search form
|
||||||
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'" name="searchform">';
|
print '<form method="POST" 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">';
|
||||||
|
|
@ -419,21 +414,6 @@ 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>';
|
||||||
|
|
||||||
|
|
@ -609,45 +589,6 @@ 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']);
|
||||||
|
|
@ -682,72 +623,50 @@ 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) {
|
||||||
$dolibarr_total = isset($item['current_total_price']) ? $item['current_total_price'] : $item['current_price'];
|
print price($item['current_price']);
|
||||||
$dolibarr_qty = isset($item['current_quantity']) ? $item['current_quantity'] : 1;
|
// Show copper surcharge from invoice if available
|
||||||
$dolibarr_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0;
|
if (!empty($item['current_kupferzuschlag']) && $item['current_kupferzuschlag'] > 0) {
|
||||||
|
print '<br><span class="opacitymedium small" title="Kupferzuschlag aus Rechnung">';
|
||||||
// IMPORTANT: Dolibarr price already includes Cu! Show as info only
|
print '<i class="fas fa-plus-circle" style="color:#f0ad4e;"></i> '.price($item['current_kupferzuschlag']);
|
||||||
if ($dolibarr_cu > 0) {
|
print '</span>';
|
||||||
print '<span class="opacitymedium small">(davon '.price($dolibarr_cu).' Cu)</span><br>';
|
// Show total price (material + surcharge)
|
||||||
}
|
$totalWithSurcharge = $item['current_price'] + $item['current_kupferzuschlag'];
|
||||||
|
print '<br><span class="small" style="color:#337ab7;" title="Gesamt (Material+CU)">';
|
||||||
// Total price for minimum quantity (already includes Cu!)
|
print '<strong>='.price($totalWithSurcharge).'</strong>';
|
||||||
print '<strong>'.price($dolibarr_total);
|
print '</span>';
|
||||||
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.'">';
|
||||||
$datanorm_pe = isset($item['datanorm_price_unit']) ? $item['datanorm_price_unit'] : 1;
|
print price($item['datanorm_price']);
|
||||||
$datanorm_raw = isset($item['datanorm_price_raw']) ? $item['datanorm_price_raw'] : $item['datanorm_price'];
|
// Show original price and unit if price_unit > 1
|
||||||
$current_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0;
|
if (!empty($item['datanorm_price_unit']) && $item['datanorm_price_unit'] > 1) {
|
||||||
$effective_qty = isset($item['current_effective_quantity']) ? $item['current_effective_quantity'] : 1;
|
print '<br><span class="opacitymedium small">('.price($item['datanorm_price_raw']).'/'.$item['datanorm_price_unit'].')</span>';
|
||||||
|
|
||||||
// Scale Cu from Dolibarr's quantity to Datanorm's price_unit
|
|
||||||
// Example: Cu 254,55€ for 50m → for 100m = 509,10€
|
|
||||||
$cu_per_unit = ($current_cu > 0 && $effective_qty > 0) ? $current_cu / $effective_qty : 0;
|
|
||||||
$cu_for_pe = $cu_per_unit * $datanorm_pe;
|
|
||||||
|
|
||||||
// Show breakdown if copper exists
|
|
||||||
if ($current_cu > 0) {
|
|
||||||
print '<span class="opacitymedium small">'.price($datanorm_raw).' + '.price($cu_for_pe).' Cu</span><br>';
|
|
||||||
}
|
}
|
||||||
|
// 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';
|
||||||
|
|
||||||
// Total price for Datanorm price_unit (with scaled Cu)
|
print '<br><span class="opacitymedium small" title="Kupferzuschlag ('.$sourceLabel.')">';
|
||||||
$datanorm_total = $datanorm_raw + $cu_for_pe;
|
print '<i class="fas fa-plus-circle" style="color:'.$sourceColor.';"></i> '.price($item['effective_surcharge']);
|
||||||
print '<strong>'.price($datanorm_total);
|
print '</span>';
|
||||||
if ($datanorm_pe > 1) {
|
// Show total price with surcharge
|
||||||
print '/'.$datanorm_pe;
|
if (!empty($item['datanorm_price_with_surcharge'])) {
|
||||||
}
|
print '<br><span class="small" style="color:#337ab7;" title="Gesamt (Material+CU)">';
|
||||||
print '</strong>';
|
print '<strong>='.price($item['datanorm_price_with_surcharge']).'</strong>';
|
||||||
|
print '</span>';
|
||||||
// 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) {
|
||||||
// Calculate percentage difference using UNIT PRICE basis
|
$diff = $item['datanorm_price'] - $item['current_price'];
|
||||||
$current_total = isset($item['current_total_price']) ? $item['current_total_price'] : $item['current_price'];
|
$diffPercent = ($item['current_price'] > 0) ? ($diff / $item['current_price'] * 100) : 0;
|
||||||
|
|
||||||
// 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>';
|
||||||
|
|
@ -796,9 +715,6 @@ 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']) {
|
||||||
|
|
@ -1185,23 +1101,14 @@ 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_total_price = 0;
|
$current_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_total_price = $priceDetails['price'];
|
$current_price = $priceDetails['unitprice'];
|
||||||
$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;
|
||||||
|
|
@ -1211,43 +1118,14 @@ function searchDatanormProducts($db, $fk_soc, $search_term, $search_by_name = 0,
|
||||||
$surcharge_source = 'datanorm';
|
$surcharge_source = 'datanorm';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate prices for comparison - UNIT PRICE basis
|
// Calculate prices
|
||||||
// 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 + $cu_for_price_unit;
|
$total_price_with_surcharge = $datanorm->price + ($effective_surcharge * $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_unit_price,
|
'current_price' => $current_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 : '',
|
||||||
|
|
@ -1259,14 +1137,12 @@ 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_compare_price - $datanorm_compare_price) > 0.01,
|
'price_differs' => $product && abs($current_price - $datanorm_material_unit_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,
|
||||||
);
|
);
|
||||||
|
|
@ -1400,17 +1276,14 @@ 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 - ALWAYS load price + quantity, NOT unitprice alone!
|
// Get base price
|
||||||
$sql = "SELECT pf.rowid, pf.price, pf.quantity";
|
$sql = "SELECT pf.rowid, pf.unitprice, 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);
|
||||||
|
|
@ -1420,14 +1293,18 @@ 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 from price / quantity
|
// Calculate unit price
|
||||||
$result['unitprice'] = $result['quantity'] > 0 ? $result['price'] / $result['quantity'] : $result['price'];
|
if (!empty($obj->unitprice) && $obj->unitprice > 0) {
|
||||||
|
$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, Kaufmenge)
|
// Get extrafields (Kupferzuschlag, Preiseinheit)
|
||||||
$sql_extra = "SELECT kupferzuschlag, preiseinheit, kaufmenge";
|
$sql_extra = "SELECT kupferzuschlag, preiseinheit";
|
||||||
$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);
|
||||||
|
|
||||||
|
|
@ -1436,7 +1313,6 @@ 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1517,21 +1393,14 @@ 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, Kaufmenge)
|
// Get supplier price details including extrafields (Kupferzuschlag)
|
||||||
$priceDetails = getSupplierPriceDetails($db, $product->fk_product, $fk_soc);
|
$priceDetails = getSupplierPriceDetails($db, $product->fk_product, $fk_soc);
|
||||||
$current_total_price = $priceDetails['price'];
|
$current_price = $priceDetails['unitprice'];
|
||||||
$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
|
||||||
|
|
@ -1543,43 +1412,14 @@ function buildComparisonResult($product, $datanorm)
|
||||||
$surcharge_source = 'datanorm';
|
$surcharge_source = 'datanorm';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate prices for comparison
|
// Calculate prices
|
||||||
// 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 + $cu_for_price_unit;
|
$total_price_with_surcharge = $datanorm->price + ($effective_surcharge * $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_unit_price,
|
'current_price' => $current_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,
|
||||||
|
|
@ -1591,14 +1431,12 @@ 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_compare_price - $datanorm_compare_price) > 0.01,
|
'price_differs' => abs($current_price - $datanorm_material_unit_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,
|
||||||
);
|
);
|
||||||
|
|
@ -1626,31 +1464,9 @@ 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;
|
||||||
// Get existing supplier price details to get kupferzuschlag and quantity from extrafield
|
$total_price = $datanorm->price + $metal_surcharge;
|
||||||
$priceDetails = getSupplierPriceDetails($db, $product_id, $fk_soc);
|
$datanorm_unit_price = $total_price / $price_unit;
|
||||||
$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);
|
||||||
|
|
@ -1678,7 +1494,20 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
|
||||||
$updated = true;
|
$updated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update label only (description goes to supplier price desc_fourn below)
|
// Update description
|
||||||
|
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);
|
||||||
|
|
@ -1687,8 +1516,8 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update supplier price and/or description
|
// Update supplier price
|
||||||
if ($apply_price || $apply_description) {
|
if ($apply_price) {
|
||||||
$productFourn = new ProductFournisseur($db);
|
$productFourn = new ProductFournisseur($db);
|
||||||
$productFourn->fetch($product_id);
|
$productFourn->fetch($product_id);
|
||||||
|
|
||||||
|
|
@ -1697,7 +1526,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, desc_fourn FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
$sql = "SELECT rowid, quantity, price, unitprice 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";
|
||||||
|
|
@ -1705,66 +1534,54 @@ 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;
|
|
||||||
|
|
||||||
// Use effective_quantity (kaufmenge if set, otherwise quantity) for price comparison
|
// Get the actual unit price from Dolibarr (price per 1 piece)
|
||||||
// This ensures consistent comparison with buildComparisonResult()
|
$current_unit_price = (!empty($priceObj->unitprice) && $priceObj->unitprice > 0)
|
||||||
$effective_qty = ($priceDetails['kaufmenge'] > 0) ? $priceDetails['kaufmenge'] : max(1, $priceObj->quantity);
|
? $priceObj->unitprice
|
||||||
|
: ($priceObj->quantity > 0 ? $priceObj->price / $priceObj->quantity : $priceObj->price);
|
||||||
|
|
||||||
// Get the actual unit price from Dolibarr (price per 1 effective piece)
|
// Only update if unit price differs
|
||||||
$current_unit_price = $effective_qty > 0 ? $priceObj->price / $effective_qty : $priceObj->price;
|
if (abs($current_unit_price - $datanorm_unit_price) > 0.01) {
|
||||||
|
|
||||||
// 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
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Update only the fields that need changing (preserves all other fields!)
|
// Calculate total price for the quantity (Dolibarr expects total price, not unit price)
|
||||||
if ($price_changed || $new_desc_fourn !== null) {
|
// Dolibarr will calculate: unitprice = price / quantity
|
||||||
$update_fields = array();
|
$total_price_for_qty = $datanorm_unit_price * $priceObj->quantity;
|
||||||
|
|
||||||
if ($price_changed) {
|
// Update existing price - $supplier must be Societe object, not integer ID
|
||||||
// Calculate total price for the quantity, round to 2 decimals
|
$result = $productFourn->update_buyprice(
|
||||||
$total_price_for_qty = round($datanorm_unit_price * $priceObj->quantity, 2);
|
$priceObj->quantity,
|
||||||
$rounded_unit_price = round($datanorm_unit_price, 2);
|
$total_price_for_qty,
|
||||||
$update_fields[] = "price = ".((float)$total_price_for_qty);
|
$user,
|
||||||
$update_fields[] = "unitprice = ".((float)$rounded_unit_price);
|
'HT',
|
||||||
}
|
$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 ($new_desc_fourn !== null) {
|
if ($result < 0) {
|
||||||
$update_fields[] = "desc_fourn = '".$db->escape($new_desc_fourn)."'";
|
return -4;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1187
import.php
1187
import.php
File diff suppressed because it is too large
Load diff
|
|
@ -54,9 +54,6 @@ $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,8 +451,6 @@ 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
|
||||||
|
|
@ -488,10 +486,3 @@ 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,8 +389,6 @@ 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
|
||||||
|
|
@ -419,10 +417,3 @@ 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,5 +15,3 @@ 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