Compare commits
No commits in common. "v3.3" and "main" have entirely different histories.
32 changed files with 2135 additions and 19586 deletions
Binary file not shown.
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pdfdetach:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(xmllint:*)",
|
||||
"Bash(php -r:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(cut:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(mysql:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(php:*)",
|
||||
"Bash(for module in /srv/http/dolibarr/custom/*/core/modules/mod*.php)",
|
||||
"Bash(do echo \"=== $module ===\" grep -A5 \"''js''\" \"$module\")",
|
||||
"Bash(done)",
|
||||
"Bash(find:*)",
|
||||
"Bash(chown:*)",
|
||||
"Bash(sudo chown:*)",
|
||||
"Bash(composer show:*)",
|
||||
"Bash(journalctl:*)",
|
||||
"Bash(sudo tail:*)",
|
||||
"Bash(strings:*)",
|
||||
"Bash(qpdf --show-object=all:*)",
|
||||
"Bash(pdftk:*)",
|
||||
"Bash(pdfinfo:*)",
|
||||
"Bash(exiftool:*)",
|
||||
"Bash(pacman:*)",
|
||||
"Bash(git init:*)",
|
||||
"Bash(git remote add:*)",
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git tag:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git stash:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
145
CHANGELOG.md
Executable file
145
CHANGELOG.md
Executable file
|
|
@ -0,0 +1,145 @@
|
|||
# Changelog
|
||||
|
||||
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
|
||||
|
||||
## [5.7] - 2026-03-04
|
||||
|
||||
### Behoben
|
||||
- **PDF-Pfad bei Cron/Batch-Import**: PDFs werden jetzt korrekt nach `/imports/{id}/{filename}` gespeichert
|
||||
- Problem: Cron speicherte nach `/imports/{ref}_{filename}`, aber beim Erstellen der Rechnung wurde nach `/imports/{id}/{filename}` gesucht
|
||||
- Lösung: Einheitlicher Pfad für manuellen und Cron-Import
|
||||
- **Fallback für alte PDF-Pfade**: Beim Anhängen an Lieferantenrechnung wird auch im alten Pfadformat gesucht
|
||||
- Ermöglicht korrekte Verarbeitung von Imports die vor dem Fix erstellt wurden
|
||||
- Sucht zuerst `/imports/{id}/{filename}`, dann `/imports/{ref}_{filename}`
|
||||
|
||||
## [5.5] - 2026-03-03
|
||||
|
||||
### Behoben
|
||||
- **Kupferzuschlag-Skalierung in Massenaktualisierung**: Kupferzuschlag wird jetzt korrekt skaliert wenn Dolibarr-Mindestmenge von Datanorm-Preiseinheit abweicht
|
||||
- Problem: Cu für 50m wurde direkt zu Datanorm-Preis für 100m addiert
|
||||
- Lösung: Cu wird erst auf Stückpreis umgerechnet (`cu_per_unit = Cu / quantity`), dann auf Datanorm-PE skaliert
|
||||
- **Steuersatz bei Preisübernahme**: `tva_tx` wird jetzt korrekt beibehalten statt auf 0 gesetzt
|
||||
- Direktes SQL-UPDATE statt `update_buyprice()` um alle Felder zu erhalten
|
||||
- **Preise auf 2 Dezimalstellen**: Gesamtpreis und Stückpreis werden auf 2 Nachkommastellen gerundet
|
||||
|
||||
### Hinzugefügt
|
||||
- **Filter für Preisrichtung**: Neue Checkboxen "Preise rauf" und "Preise runter" in Massenaktualisierung
|
||||
- Ermöglicht gezieltes Filtern nach Preiserhöhungen oder -senkungen
|
||||
- **Filter-Persistenz**: Alle Filter (inkl. hide_cables, filter_price_up, filter_price_down) bleiben nach Preisübernahme erhalten
|
||||
- **Alternative Datanorm-Preise verbessert**: Beim Import von Rechnungen mit alternativen Datanorm-Katalogen werden jetzt übernommen:
|
||||
- Mindestmenge vom vorhandenen/Hauptpreis
|
||||
- Verpackungseinheit vom vorhandenen/Hauptpreis
|
||||
- Steuersatz vom vorhandenen/Hauptpreis
|
||||
- kaufmenge-Extrafield (nur wenn numerisch und > 0)
|
||||
- **Extrafield kaufmenge sichtbar**: Feld wird jetzt in Formularen angezeigt (`list = 1`)
|
||||
|
||||
### Geändert
|
||||
- **Kupferzuschlag nicht automatisch gesetzt**: Bei Datanorm-Import wird kupferzuschlag NICHT mehr gesetzt - wird von separatem Modul berechnet
|
||||
|
||||
### Technisch
|
||||
- Kupferzuschlag-Berechnung: `cu_for_price_unit = (kupferzuschlag / effective_quantity) * price_unit`
|
||||
- kaufmenge-Validierung: `trim() !== '' && is_numeric() && (int) > 0`
|
||||
|
||||
## [4.2] - 2026-03-02
|
||||
|
||||
### Behoben
|
||||
- **PDF-Anhänge**: ZUGFeRD-PDFs werden jetzt korrekt an Lieferantenrechnungen angehängt
|
||||
- Problem: PDF wurde nur ins Dateisystem kopiert, nicht in ECM-Datenbank registriert
|
||||
- Lösung: `EcmFiles`-Eintrag wird erstellt für korrekte Verknüpfung mit Rechnung
|
||||
- Wichtig: Bei Rechnungsvalidierung wird PDF automatisch mitverschoben
|
||||
|
||||
### Hinzugefügt
|
||||
- **Bezeichnung in Rechnungsliste**: Teuerster Artikel wird als Bezeichnung der Lieferantenrechnung gesetzt
|
||||
- Erleichtert schnelle Identifikation in der Rechnungsliste
|
||||
- Spalte "Bezeichnung" muss in Liste aktiviert sein
|
||||
|
||||
## [4.0] - 2026-03-01
|
||||
|
||||
### Behoben
|
||||
- **Verschachtelte HTML-Forms**: "Ausgewählte Preise hinzufügen" funktionierte nicht, weil Browser verschachtelte `<form>`-Elemente nicht unterstützen. Lösung: HTML5 `form`-Attribut
|
||||
- **Stückpreis-Anzeige**: Einkaufspreise zeigen jetzt den Stückpreis statt Gesamtpreis (z.B. 0,16 statt 16,32 bei 100 Stk.)
|
||||
- **Preisvergleich**: Fehlende Lieferantenpreise werden korrekt auf Stückpreis-Basis verglichen
|
||||
- **DATPREIS-Kommentare**: Feld korrekt als Rabattkennzeichen dokumentiert (war fälschlich als PE-Code beschrieben)
|
||||
|
||||
### Verbessert
|
||||
- **Feedback bei Preishinzufügen**: Zeigt Erfolgs- und Fehlermeldungen nach dem Hinzufügen von Lieferantenpreisen
|
||||
- **Mengenkontext**: Bei Mengenstaffel wird zusätzlich der Gesamtpreis mit Stückzahl angezeigt (z.B. "0,16 (16,32/100Stk.)")
|
||||
|
||||
### Hinweis
|
||||
- `uk_product_barcode` UNIQUE KEY auf `product_fournisseur_price` muss entfernt werden falls vorhanden (mehrere Lieferanten dürfen gleichen EAN haben)
|
||||
|
||||
## [3.8] - 2026-02-25
|
||||
|
||||
### Hinzugefügt
|
||||
- **Kabel-Preisberechnung**: Zentrale Funktion `calculateCablePricing()` für einheitliche Preislogik
|
||||
- **Kupfergehalt-Berechnung**: Automatische Berechnung aus Aderanzahl × Querschnitt × 8.9
|
||||
- **Ringgröße-Erkennung**: Unterstützt Ri100, Tr500, Fol.25m, "Ring 100m", "Trommel 500m"
|
||||
- **Extrafield "produktpreis"**: Speichert reinen Materialpreis ohne Kupferzuschlag (nur Kabel)
|
||||
- **EAN-Auto-Update**: Barcodes aus ZUGFeRD-Rechnungen werden automatisch in Lieferantenpreise übernommen
|
||||
|
||||
### Verbessert
|
||||
- **Lieferanten-Formate**: Korrekte Unterscheidung zwischen Sonepar (price_unit=1, Ring im Namen) und Kluxen/Witte (price_unit=100)
|
||||
- **Cross-Catalog-Suche**: Nur noch über EAN, nicht mehr über Artikelnummern (verhindert Fehlzuordnungen)
|
||||
- **EAN-Barcode-Typ**: Automatische Erkennung (EAN8, EAN13, UPC-A) statt hardcoded EAN13
|
||||
- **Error-Handling**: Besseres Logging bei Extrafield-Fehlern
|
||||
|
||||
### Behoben
|
||||
- Division durch Null bei Preisberechnung abgesichert
|
||||
- Mindestbestellmenge und Verpackungseinheit werden von existierenden Lieferantenpreisen übernommen
|
||||
|
||||
## [3.7] - 2026-02-23
|
||||
|
||||
### Hinzugefügt
|
||||
- **GlobalNotify Integration**: Benachrichtigungen über das zentrale GlobalNotify-Modul
|
||||
- Import-Fehler: Warnung bei fehlgeschlagenen Importen
|
||||
- Rechnungen zur Prüfung: Aktion wenn neue Rechnungen warten
|
||||
- IMAP-Fehler: Warnung wenn E-Mail Postfach nicht erreichbar
|
||||
- Exception/Fatal: Sofortige Benachrichtigung bei Abstürzen
|
||||
- **Helper-Funktion**: `notify()` für sichere GlobalNotify-Nutzung mit Fallback
|
||||
|
||||
### Hinweis
|
||||
GlobalNotify ist optional. Ohne das Modul werden Benachrichtigungen ins Dolibarr-Log geschrieben.
|
||||
|
||||
## [3.6] - 2026-02-23
|
||||
|
||||
### Behoben
|
||||
- **Cron-Job Fix**: Fehlendes `require_once` für `admin.lib.php` hinzugefügt - verhinderte das Speichern des letzten Laufzeitpunkts
|
||||
- Cron-Job lief in Endlosschleife weil `dolibarr_set_const()` nicht gefunden wurde
|
||||
|
||||
### Hinzugefügt
|
||||
- **Dediziertes Cron-Logging**: Separate Log-Datei unter `/documents/importzugferd/logs/cron_importzugferd.log`
|
||||
- **Shutdown Handler**: Fängt fatale PHP-Fehler ab und protokolliert sie
|
||||
- **Detailliertes Logging**: Zeigt jeden Schritt des Import-Prozesses (Ordner-Zugriff, PDF-Scan, IMAP-Status)
|
||||
|
||||
### Verbessert
|
||||
- Robustere Fehlerbehandlung mit try/catch für Exceptions und Throwables
|
||||
- IMAP-Import wird nur ausgeführt wenn tatsächlich konfiguriert
|
||||
|
||||
## [3.5] - 2026-02-15
|
||||
|
||||
### Hinzugefügt
|
||||
- Automatischer Cron-Import aus Watch-Folder
|
||||
- IMAP-Mailbox-Unterstützung für E-Mail-Rechnungen
|
||||
- Konfigurierbare Import-Frequenz (stündlich, täglich, wöchentlich)
|
||||
- Archiv- und Fehler-Ordner für verarbeitete Dateien
|
||||
|
||||
## [3.0] - 2026-02-01
|
||||
|
||||
### Hinzugefügt
|
||||
- ZUGFeRD/Factur-X PDF-Parsing
|
||||
- Automatische Lieferanten-Erkennung
|
||||
- Rechnungsvorschau vor Import
|
||||
- Datanorm-Integration für Artikelpreise
|
||||
|
||||
## [2.0] - 2026-01-15
|
||||
|
||||
### Hinzugefügt
|
||||
- Basis-Import von ZUGFeRD-Rechnungen
|
||||
- Manuelle Datei-Auswahl
|
||||
- Integration in Lieferantenrechnungen
|
||||
|
||||
## [1.0] - 2026-01-01
|
||||
|
||||
### Erste Version
|
||||
- Grundlegende ZUGFeRD-Erkennung
|
||||
- XML-Extraktion aus PDF
|
||||
128
ChangeLog.md
128
ChangeLog.md
|
|
@ -1,43 +1,107 @@
|
|||
# CHANGELOG MODULE IMPORTZUGFERD FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
|
||||
# Changelog
|
||||
|
||||
## 3.2
|
||||
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
|
||||
|
||||
### Neue Funktionen
|
||||
- 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
|
||||
## [4.2] - 2026-03-02
|
||||
|
||||
### Bugfixes
|
||||
- Datanorm Import: Kluxen-Format (Preise im A-Record in Cent) wird jetzt korrekt verarbeitet
|
||||
- Datanorm Import: Preise aus A-Record werden von Cent in Euro umgerechnet (geteilt durch 100)
|
||||
### 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
|
||||
|
||||
### Hinweise
|
||||
- Kluxen-Katalog enthaelt nur Listenpreise (UVP), keine Netto-Einkaufspreise
|
||||
- Cross-Katalog-Suche erfordert aktivierte Einstellung "In allen Lieferanten-Katalogen suchen"
|
||||
### 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
|
||||
|
||||
## 2.1
|
||||
## [4.0] - 2026-03-01
|
||||
|
||||
### Bugfixes
|
||||
- Rechnungsimport: Preise wurden falsch als Brutto (TTC) statt Netto (HT) behandelt - korrigierte Parameterreihenfolge in addline()
|
||||
- Datanorm Massenaktualisierung: Lieferantenauswahl ging nach Aktionen verloren - Redirects hinzugefuegt
|
||||
- Datanorm Massenaktualisierung: "Alle Aenderungen uebernehmen" Button war nicht sichtbar ohne Suchergebnisse
|
||||
- Datanorm Massenaktualisierung: Filter-Auswahl (Preis/Beschreibung/Bezeichnung) wurde bei "Alle hinzufuegen" ignoriert
|
||||
- ProductFournisseur::update_buyprice erwartet Societe-Objekt, nicht Integer-ID
|
||||
### 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)
|
||||
|
||||
### Verbesserungen
|
||||
- Bestaetungsdialog fuer Massenaktionen verwendet jetzt Dolibarr jQuery UI Dialog statt JavaScript confirm()
|
||||
- Manuelles Metallzuschlag-Eingabefeld entfernt (nicht mehr benoetigt - Kupferzuschlag wird aus ZUGFeRD XML extrahiert)
|
||||
- Ausstehende Aenderungen werden immer angezeigt wenn vorhanden, unabhaengig von Suchergebnissen
|
||||
### 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.)")
|
||||
|
||||
## 2.0
|
||||
### Hinweis
|
||||
- `uk_product_barcode` UNIQUE KEY auf `product_fournisseur_price` muss entfernt werden falls vorhanden (mehrere Lieferanten dürfen gleichen EAN haben)
|
||||
|
||||
- Datanorm 4.0/5.0 Katalog-Import
|
||||
- Kupferzuschlag-Extraktion aus ZUGFeRD XML (AllowanceCharge)
|
||||
- Automatischer Preisvergleich zwischen Datanorm und aktuellen Einkaufspreisen
|
||||
- Massenaktualisierung von Produktpreisen und Beschreibungen
|
||||
- Aenderungsprotokoll fuer Preisanpassungen
|
||||
## [3.8] - 2026-02-25
|
||||
|
||||
## 1.0
|
||||
### Hinzugefügt
|
||||
- **Kabel-Preisberechnung**: Zentrale Funktion `calculateCablePricing()` für einheitliche Preislogik
|
||||
- **Kupfergehalt-Berechnung**: Automatische Berechnung aus Aderanzahl × Querschnitt × 8.9
|
||||
- **Ringgröße-Erkennung**: Unterstützt Ri100, Tr500, Fol.25m, "Ring 100m", "Trommel 500m"
|
||||
- **Extrafield "produktpreis"**: Speichert reinen Materialpreis ohne Kupferzuschlag (nur Kabel)
|
||||
- **EAN-Auto-Update**: Barcodes aus ZUGFeRD-Rechnungen werden automatisch in Lieferantenpreise übernommen
|
||||
|
||||
Initial version
|
||||
### Verbessert
|
||||
- **Lieferanten-Formate**: Korrekte Unterscheidung zwischen Sonepar (price_unit=1, Ring im Namen) und Kluxen/Witte (price_unit=100)
|
||||
- **Cross-Catalog-Suche**: Nur noch über EAN, nicht mehr über Artikelnummern (verhindert Fehlzuordnungen)
|
||||
- **EAN-Barcode-Typ**: Automatische Erkennung (EAN8, EAN13, UPC-A) statt hardcoded EAN13
|
||||
- **Error-Handling**: Besseres Logging bei Extrafield-Fehlern
|
||||
|
||||
### Behoben
|
||||
- Division durch Null bei Preisberechnung abgesichert
|
||||
- Mindestbestellmenge und Verpackungseinheit werden von existierenden Lieferantenpreisen übernommen
|
||||
|
||||
## [3.7] - 2026-02-23
|
||||
|
||||
### Hinzugefügt
|
||||
- **GlobalNotify Integration**: Benachrichtigungen über das zentrale GlobalNotify-Modul
|
||||
- Import-Fehler: Warnung bei fehlgeschlagenen Importen
|
||||
- Rechnungen zur Prüfung: Aktion wenn neue Rechnungen warten
|
||||
- IMAP-Fehler: Warnung wenn E-Mail Postfach nicht erreichbar
|
||||
- Exception/Fatal: Sofortige Benachrichtigung bei Abstürzen
|
||||
- **Helper-Funktion**: `notify()` für sichere GlobalNotify-Nutzung mit Fallback
|
||||
|
||||
### Hinweis
|
||||
GlobalNotify ist optional. Ohne das Modul werden Benachrichtigungen ins Dolibarr-Log geschrieben.
|
||||
|
||||
## [3.6] - 2026-02-23
|
||||
|
||||
### Behoben
|
||||
- **Cron-Job Fix**: Fehlendes `require_once` für `admin.lib.php` hinzugefügt - verhinderte das Speichern des letzten Laufzeitpunkts
|
||||
- Cron-Job lief in Endlosschleife weil `dolibarr_set_const()` nicht gefunden wurde
|
||||
|
||||
### Hinzugefügt
|
||||
- **Dediziertes Cron-Logging**: Separate Log-Datei unter `/documents/importzugferd/logs/cron_importzugferd.log`
|
||||
- **Shutdown Handler**: Fängt fatale PHP-Fehler ab und protokolliert sie
|
||||
- **Detailliertes Logging**: Zeigt jeden Schritt des Import-Prozesses (Ordner-Zugriff, PDF-Scan, IMAP-Status)
|
||||
|
||||
### Verbessert
|
||||
- Robustere Fehlerbehandlung mit try/catch für Exceptions und Throwables
|
||||
- IMAP-Import wird nur ausgeführt wenn tatsächlich konfiguriert
|
||||
|
||||
## [3.5] - 2026-02-15
|
||||
|
||||
### Hinzugefügt
|
||||
- Automatischer Cron-Import aus Watch-Folder
|
||||
- IMAP-Mailbox-Unterstützung für E-Mail-Rechnungen
|
||||
- Konfigurierbare Import-Frequenz (stündlich, täglich, wöchentlich)
|
||||
- Archiv- und Fehler-Ordner für verarbeitete Dateien
|
||||
|
||||
## [3.0] - 2026-02-01
|
||||
|
||||
### Hinzugefügt
|
||||
- ZUGFeRD/Factur-X PDF-Parsing
|
||||
- Automatische Lieferanten-Erkennung
|
||||
- Rechnungsvorschau vor Import
|
||||
- Datanorm-Integration für Artikelpreise
|
||||
|
||||
## [2.0] - 2026-01-15
|
||||
|
||||
### Hinzugefügt
|
||||
- Basis-Import von ZUGFeRD-Rechnungen
|
||||
- Manuelle Datei-Auswahl
|
||||
- Integration in Lieferantenrechnungen
|
||||
|
||||
## [1.0] - 2026-01-01
|
||||
|
||||
### Erste Version
|
||||
- Grundlegende ZUGFeRD-Erkennung
|
||||
- XML-Extraktion aus PDF
|
||||
|
|
|
|||
44
README.md
44
README.md
|
|
@ -102,23 +102,39 @@ Available in:
|
|||
|
||||
## Version History
|
||||
|
||||
### 1.1
|
||||
- New persistent import workflow with database storage
|
||||
- Manual product assignment via dropdown
|
||||
- Product removal/reassignment
|
||||
- Status "Pending" for imports requiring manual intervention
|
||||
- Pending imports overview on upload page
|
||||
- UN/ECE unit code translation (C62 → Stk., MTR → m, etc.)
|
||||
- Batch import from folder or IMAP mailbox
|
||||
- IMAP connection test with folder selection
|
||||
- Product template feature (duplicate existing product)
|
||||
See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
|
||||
|
||||
### 5.7 (Current)
|
||||
- Fixed PDF path for Cron/Batch imports - now correctly saved to `/imports/{id}/{filename}`
|
||||
- Added fallback for old PDF paths when attaching to supplier invoices
|
||||
|
||||
### 5.5
|
||||
- Fixed copper surcharge scaling in mass update (different quantities between Dolibarr and Datanorm)
|
||||
- Fixed VAT rate preservation when updating prices
|
||||
- New filters for price direction (up/down) in mass update
|
||||
- Alternative Datanorm prices inherit min quantity, packaging, VAT, and kaufmenge from existing prices
|
||||
- Copper surcharge is NOT set by import - calculated by separate module
|
||||
|
||||
### 4.2
|
||||
- PDF attachments properly linked to supplier invoices via ECM
|
||||
- Most expensive item shown as invoice description
|
||||
|
||||
### 3.8
|
||||
- Improved cable pricing for different supplier formats (Sonepar vs Kluxen/Witte)
|
||||
- Automatic ring size detection from product names (Ri100, Tr500, etc.)
|
||||
- EAN auto-update from ZUGFeRD invoices with automatic barcode type detection
|
||||
|
||||
### 3.7
|
||||
- GlobalNotify integration for import notifications
|
||||
|
||||
### 3.5
|
||||
- Automatic cron import from watch folder and IMAP
|
||||
|
||||
### 3.0
|
||||
- Datanorm integration for article prices
|
||||
|
||||
### 1.0
|
||||
- Initial release
|
||||
- Basic ZUGFeRD/Factur-X import
|
||||
- Automatic product matching
|
||||
- Supplier detection
|
||||
- Duplicate detection
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
bin/module_importzugferd-4.4.zip
Executable file
BIN
bin/module_importzugferd-4.4.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-4.5.zip
Executable file
BIN
bin/module_importzugferd-4.5.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-4.6.zip
Executable file
BIN
bin/module_importzugferd-4.6.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-4.8.zip
Executable file
BIN
bin/module_importzugferd-4.8.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-4.9.zip
Executable file
BIN
bin/module_importzugferd-4.9.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.0.zip
Executable file
BIN
bin/module_importzugferd-5.0.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.1.zip
Executable file
BIN
bin/module_importzugferd-5.1.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.2.zip
Executable file
BIN
bin/module_importzugferd-5.2.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.3.zip
Executable file
BIN
bin/module_importzugferd-5.3.zip
Executable file
Binary file not shown.
BIN
bin/module_importzugferd-5.4.zip
Executable file
BIN
bin/module_importzugferd-5.4.zip
Executable file
Binary file not shown.
11
card.php
11
card.php
|
|
@ -244,6 +244,13 @@ if (!empty($object->xml_content)) {
|
|||
// Format XML for better readability using class method
|
||||
$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 '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
|
|
@ -254,8 +261,8 @@ if (!empty($object->xml_content)) {
|
|||
print '<td colspan="2">';
|
||||
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 '<pre style="max-height: 500px; overflow: auto; background: #f5f5f5; padding: 10px; font-size: 11px; white-space: pre-wrap; word-wrap: break-word;">';
|
||||
print dol_escape_htmltag($formattedXml);
|
||||
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 $highlightedXml;
|
||||
print '</pre>';
|
||||
print '</div>';
|
||||
print '</td>';
|
||||
|
|
|
|||
|
|
@ -316,6 +316,32 @@ class ActionsImportZugferd
|
|||
$processed_line['product_ref'] = $product->ref;
|
||||
$processed_line['product_label'] = $product->label;
|
||||
}
|
||||
|
||||
// Update supplier price with EAN from invoice if empty
|
||||
$invoiceEan = !empty($line['product']['global_id']) ? trim($line['product']['global_id']) : '';
|
||||
$supplierRef = !empty($line['product']['seller_id']) ? $line['product']['seller_id'] : '';
|
||||
if (!empty($invoiceEan) && !empty($supplierRef) && ctype_digit($invoiceEan)) {
|
||||
// Barcode-Typ basierend auf Länge bestimmen
|
||||
$eanLen = strlen($invoiceEan);
|
||||
if ($eanLen == 13) {
|
||||
$barcodeType = 2; // EAN13
|
||||
} elseif ($eanLen == 8) {
|
||||
$barcodeType = 1; // EAN8
|
||||
} elseif ($eanLen == 12) {
|
||||
$barcodeType = 3; // UPC-A
|
||||
} else {
|
||||
$barcodeType = 0; // Unbekannt
|
||||
}
|
||||
|
||||
$sqlEan = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sqlEan .= " SET barcode = '" . $this->db->escape($invoiceEan) . "'";
|
||||
$sqlEan .= ", fk_barcode_type = " . (int)$barcodeType;
|
||||
$sqlEan .= " WHERE fk_product = " . (int)$match['fk_product'];
|
||||
$sqlEan .= " AND fk_soc = " . (int)$supplier_id;
|
||||
$sqlEan .= " AND ref_fourn = '" . $this->db->escape($supplierRef) . "'";
|
||||
$sqlEan .= " AND (barcode IS NULL OR barcode = '')";
|
||||
$this->db->query($sqlEan);
|
||||
}
|
||||
} else {
|
||||
$processed_line['needs_creation'] = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
*/
|
||||
|
||||
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/zugferdimport.class.php');
|
||||
|
|
@ -60,6 +61,67 @@ class CronImportZugferd
|
|||
*/
|
||||
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
|
||||
*
|
||||
|
|
@ -67,7 +129,45 @@ class CronImportZugferd
|
|||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
global $conf;
|
||||
$this->db = $db;
|
||||
|
||||
// Set up dedicated log file for cron jobs
|
||||
$logDir = $conf->importzugferd->dir_output.'/logs';
|
||||
if (!is_dir($logDir)) {
|
||||
dol_mkdir($logDir);
|
||||
}
|
||||
$this->cronLogFile = $logDir.'/cron_importzugferd.log';
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to dedicated cron log file
|
||||
*
|
||||
* @param string $message Log message
|
||||
* @param string $level Log level (INFO, WARNING, ERROR, DEBUG)
|
||||
* @return void
|
||||
*/
|
||||
private function cronLog($message, $level = 'INFO')
|
||||
{
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$elapsed = $this->startTime > 0 ? round(microtime(true) - $this->startTime, 2).'s' : '0s';
|
||||
$logLine = "[{$timestamp}] [{$level}] [{$elapsed}] {$message}\n";
|
||||
|
||||
@file_put_contents($this->cronLogFile, $logLine, FILE_APPEND | LOCK_EX);
|
||||
dol_syslog("CronImportZugferd: ".$message, $level === 'ERROR' ? LOG_ERR : ($level === 'WARNING' ? LOG_WARNING : LOG_INFO));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown handler to catch fatal errors
|
||||
*/
|
||||
public function handleShutdown()
|
||||
{
|
||||
$error = error_get_last();
|
||||
if ($error !== null && in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) {
|
||||
$message = "FATAL SHUTDOWN: {$error['message']} in {$error['file']}:{$error['line']}";
|
||||
$this->cronLog($message, 'ERROR');
|
||||
$this->cronLog("========== CRON END (fatal shutdown) ==========");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -127,15 +227,16 @@ class CronImportZugferd
|
|||
*/
|
||||
public function runScheduledImport()
|
||||
{
|
||||
global $conf, $user, $langs;
|
||||
global $langs;
|
||||
|
||||
// Initialize timing and shutdown handler
|
||||
$this->startTime = microtime(true);
|
||||
register_shutdown_function(array($this, 'handleShutdown'));
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
// Check if we should run based on frequency
|
||||
if (!$this->shouldRunImport()) {
|
||||
$this->output = 'Skipped - not scheduled to run (frequency: '.getDolGlobalString('IMPORTZUGFERD_IMPORT_FREQUENCY', 'manual').')';
|
||||
return 0;
|
||||
}
|
||||
$this->cronLog("========== CRON START ==========");
|
||||
$this->cronLog("PHP Version: ".PHP_VERSION.", Memory Limit: ".ini_get('memory_limit'));
|
||||
|
||||
// Reset counters
|
||||
$this->imported_count = 0;
|
||||
|
|
@ -143,25 +244,69 @@ class CronImportZugferd
|
|||
$this->error_count = 0;
|
||||
$this->errors = array();
|
||||
|
||||
$folderResult = $this->importFromFolder();
|
||||
$mailboxResult = $this->fetchFromMailbox();
|
||||
try {
|
||||
$this->cronLog("Starting folder import...");
|
||||
$this->importFromFolder();
|
||||
$this->cronLog("Folder import completed");
|
||||
|
||||
// Update last run time
|
||||
$this->updateLastRunTime();
|
||||
// IMAP nur wenn konfiguriert
|
||||
if (!empty(getDolGlobalString('IMPORTZUGFERD_IMAP_HOST'))) {
|
||||
$this->cronLog("Starting IMAP import...");
|
||||
$this->fetchFromMailbox();
|
||||
$this->cronLog("IMAP import completed");
|
||||
} else {
|
||||
$this->cronLog("IMAP not configured - skipping");
|
||||
}
|
||||
|
||||
// Build combined output
|
||||
$this->output = sprintf(
|
||||
"Scheduled import complete. Imported: %d, Skipped: %d, Errors: %d",
|
||||
$this->imported_count,
|
||||
$this->skipped_count,
|
||||
$this->error_count
|
||||
);
|
||||
// Update last run time
|
||||
$this->updateLastRunTime();
|
||||
|
||||
if ($this->error_count > 0 && !empty($this->errors)) {
|
||||
$this->output .= "\nErrors: " . implode(", ", array_slice($this->errors, 0, 5));
|
||||
// Build combined output
|
||||
$this->output = sprintf(
|
||||
"Scheduled import complete. Imported: %d, Skipped: %d, Errors: %d",
|
||||
$this->imported_count,
|
||||
$this->skipped_count,
|
||||
$this->error_count
|
||||
);
|
||||
|
||||
if ($this->error_count > 0 && !empty($this->errors)) {
|
||||
$this->output .= "\nErrors: " . implode(", ", array_slice($this->errors, 0, 5));
|
||||
}
|
||||
|
||||
$duration = round(microtime(true) - $this->startTime, 2);
|
||||
$this->cronLog("Completed: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}, duration={$duration}s");
|
||||
$this->cronLog("========== CRON END (success) ==========");
|
||||
|
||||
// Send GlobalNotify notifications
|
||||
$this->sendImportNotifications();
|
||||
|
||||
return ($this->error_count > 0) ? -1 : 0;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->error = 'Exception: '.$e->getMessage();
|
||||
$this->cronLog("EXCEPTION: ".$e->getMessage()."\n".$e->getTraceAsString(), 'ERROR');
|
||||
$this->cronLog("========== CRON END (exception) ==========");
|
||||
$this->notify(
|
||||
'error',
|
||||
'ZUGFeRD Import fehlgeschlagen',
|
||||
'Exception: '.$e->getMessage(),
|
||||
dol_buildpath('/importzugferd/admin/setup.php', 1),
|
||||
'Einstellungen prüfen'
|
||||
);
|
||||
return -1;
|
||||
} catch (Throwable $t) {
|
||||
$this->error = 'Fatal: '.$t->getMessage();
|
||||
$this->cronLog("FATAL: ".$t->getMessage()."\n".$t->getTraceAsString(), 'ERROR');
|
||||
$this->cronLog("========== CRON END (fatal) ==========");
|
||||
$this->notify(
|
||||
'error',
|
||||
'ZUGFeRD Import Absturz',
|
||||
'Fatal: '.$t->getMessage(),
|
||||
dol_buildpath('/importzugferd/admin/setup.php', 1),
|
||||
'Einstellungen prüfen'
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return ($this->error_count > 0) ? -1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -171,7 +316,7 @@ class CronImportZugferd
|
|||
*/
|
||||
public function importFromFolder()
|
||||
{
|
||||
global $conf, $user, $langs;
|
||||
global $langs;
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
|
|
@ -180,24 +325,45 @@ class CronImportZugferd
|
|||
$errorFolder = getDolGlobalString('IMPORTZUGFERD_ERROR_FOLDER');
|
||||
$autoCreate = getDolGlobalString('IMPORTZUGFERD_AUTO_CREATE_INVOICE');
|
||||
|
||||
$this->cronLog("Watch folder: {$watchFolder}");
|
||||
|
||||
// Validate settings
|
||||
if (empty($watchFolder) || !is_dir($watchFolder)) {
|
||||
$this->output = 'Watch folder not configured or not accessible';
|
||||
return 0; // Not an error, just not configured
|
||||
if (empty($watchFolder)) {
|
||||
$this->cronLog("Watch folder not configured - skipping");
|
||||
$this->output = 'Watch folder not configured';
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!is_dir($watchFolder)) {
|
||||
$this->cronLog("Watch folder not accessible: {$watchFolder}", 'WARNING');
|
||||
$this->output = 'Watch folder not accessible';
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->cronLog("Watch folder accessible, scanning for PDFs...");
|
||||
|
||||
// Load admin user for import actions
|
||||
$admin_user = new User($this->db);
|
||||
$admin_user->fetch(1);
|
||||
|
||||
// Find PDF files
|
||||
$this->cronLog("Running glob for *.pdf...");
|
||||
$files = glob($watchFolder . '/*.pdf');
|
||||
$files = array_merge($files, glob($watchFolder . '/*.PDF'));
|
||||
$this->cronLog("Found ".count($files)." .pdf files");
|
||||
|
||||
$this->cronLog("Running glob for *.PDF...");
|
||||
$filesUpper = glob($watchFolder . '/*.PDF');
|
||||
$this->cronLog("Found ".count($filesUpper)." .PDF files");
|
||||
|
||||
$files = array_merge($files, $filesUpper);
|
||||
|
||||
if (empty($files)) {
|
||||
$this->cronLog("No PDF files found in watch folder");
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->cronLog("Total ".count($files)." PDF files to process");
|
||||
|
||||
// Ensure archive folder exists if configured
|
||||
if (!empty($archiveFolder) && !is_dir($archiveFolder)) {
|
||||
dol_mkdir($archiveFolder);
|
||||
|
|
@ -209,20 +375,22 @@ class CronImportZugferd
|
|||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$this->cronLog("Processing: ".basename($file));
|
||||
|
||||
// Use ZugferdImport::importFromFile for consistent handling
|
||||
$import = new ZugferdImport($this->db);
|
||||
$result = $import->importFromFile($admin_user, $file, $autoCreate);
|
||||
$result = $import->importFromFile($admin_user, $file, !empty($autoCreate));
|
||||
|
||||
if ($result > 0) {
|
||||
$this->imported_count++;
|
||||
dol_syslog("CronImportZugferd: Imported invoice from folder: " . basename($file), LOG_INFO);
|
||||
$this->cronLog("Imported: ".basename($file)." -> ID {$result}");
|
||||
|
||||
// Archive the file
|
||||
$this->moveFile($file, $archiveFolder, 'imported_');
|
||||
} elseif ($result == -2) {
|
||||
// Duplicate - already imported
|
||||
$this->skipped_count++;
|
||||
dol_syslog("CronImportZugferd: Skipped duplicate invoice: " . basename($file), LOG_INFO);
|
||||
$this->cronLog("Skipped (duplicate): ".basename($file));
|
||||
|
||||
// Archive duplicates - delete if no archive folder
|
||||
if (!$this->moveFile($file, $archiveFolder, 'duplicate_')) {
|
||||
|
|
@ -231,7 +399,7 @@ class CronImportZugferd
|
|||
} else {
|
||||
$this->error_count++;
|
||||
$this->errors[] = basename($file) . ': ' . $import->error;
|
||||
dol_syslog("CronImportZugferd: Error importing: " . basename($file) . " - " . $import->error, LOG_WARNING);
|
||||
$this->cronLog("Error importing ".basename($file).": ".$import->error, 'ERROR');
|
||||
|
||||
// Try error folder first, fall back to archive folder
|
||||
if (!$this->moveFile($file, $errorFolder, 'error_')) {
|
||||
|
|
@ -241,6 +409,7 @@ class CronImportZugferd
|
|||
}
|
||||
}
|
||||
|
||||
$this->cronLog("Folder import finished: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -497,7 +666,17 @@ class CronImportZugferd
|
|||
}
|
||||
|
||||
if (is_dir($targetFolder) && is_writable($targetFolder)) {
|
||||
$targetPath = $targetFolder . '/' . $prefix . date('Y-m-d_His') . '_' . basename($file);
|
||||
// Originalen Dateinamen beibehalten, bei Namenskollision Zaehler anhaengen
|
||||
$baseName = basename($file);
|
||||
$targetPath = $targetFolder . '/' . $baseName;
|
||||
if (file_exists($targetPath)) {
|
||||
$pathInfo = pathinfo($baseName);
|
||||
$counter = 1;
|
||||
do {
|
||||
$targetPath = $targetFolder . '/' . $pathInfo['filename'] . '_' . $counter . '.' . $pathInfo['extension'];
|
||||
$counter++;
|
||||
} while (file_exists($targetPath));
|
||||
}
|
||||
|
||||
if (@rename($file, $targetPath)) {
|
||||
dol_syslog("CronImportZugferd: Moved file to: " . $targetPath, LOG_INFO);
|
||||
|
|
@ -534,4 +713,79 @@ class CronImportZugferd
|
|||
dol_syslog("CronImportZugferd: Keeping error file in watch folder: " . $file, LOG_INFO);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notifications based on import results
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function sendImportNotifications()
|
||||
{
|
||||
// Check for errors
|
||||
if ($this->error_count > 0) {
|
||||
$errorSummary = count($this->errors) > 0 ? implode(', ', array_slice($this->errors, 0, 3)) : 'Siehe Log';
|
||||
$this->notify(
|
||||
'warning',
|
||||
$this->error_count.' ZUGFeRD Import-Fehler',
|
||||
$errorSummary,
|
||||
dol_buildpath('/importzugferd/list.php?status=error', 1),
|
||||
'Fehler anzeigen'
|
||||
);
|
||||
}
|
||||
|
||||
// Check for imported invoices that need review
|
||||
if ($this->imported_count > 0) {
|
||||
// Count pending invoices (drafts needing approval)
|
||||
$pendingCount = $this->countPendingInvoices();
|
||||
|
||||
if ($pendingCount > 0) {
|
||||
$this->notify(
|
||||
'action',
|
||||
$this->imported_count.' ZUGFeRD Rechnungen importiert',
|
||||
"{$pendingCount} Lieferantenrechnungen warten auf Prüfung und Freigabe",
|
||||
dol_buildpath('/fourn/facture/list.php?search_status=0', 1),
|
||||
'Rechnungen prüfen'
|
||||
);
|
||||
} else {
|
||||
// All auto-created and validated
|
||||
$this->notify(
|
||||
'info',
|
||||
$this->imported_count.' ZUGFeRD Rechnungen importiert',
|
||||
'Alle Rechnungen wurden erfolgreich verarbeitet',
|
||||
dol_buildpath('/fourn/facture/list.php', 1),
|
||||
'Anzeigen'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// IMAP connection issues
|
||||
if (strpos($this->error, 'IMAP connection failed') !== false) {
|
||||
$this->notify(
|
||||
'error',
|
||||
'IMAP Verbindung fehlgeschlagen',
|
||||
'E-Mail Postfach für ZUGFeRD-Import nicht erreichbar',
|
||||
dol_buildpath('/importzugferd/admin/setup.php', 1),
|
||||
'IMAP prüfen'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count pending (draft) supplier invoices
|
||||
*
|
||||
* @return int Number of draft supplier invoices
|
||||
*/
|
||||
protected function countPendingInvoices()
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_fourn";
|
||||
$sql .= " WHERE fk_statut = 0"; // Draft status
|
||||
$sql .= " AND entity IN (".getEntity('facture_fourn').")";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->cnt;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -520,9 +520,22 @@ class Datanorm extends CommonObject
|
|||
if ($result > 0) {
|
||||
$results[] = $this->toArray();
|
||||
$foundIds[$this->id] = true;
|
||||
// Store EAN and manufacturer_ref for cross-catalog search
|
||||
// Store EAN from Datanorm
|
||||
$foundEan = $this->ean;
|
||||
$foundManufacturerRef = $this->manufacturer_ref;
|
||||
|
||||
// If Datanorm has no EAN, try to get it from supplier price (barcode field)
|
||||
if (empty($foundEan)) {
|
||||
$sqlEan = "SELECT barcode FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sqlEan .= " WHERE fk_soc = " . (int)$fk_soc;
|
||||
$sqlEan .= " AND ref_fourn = '" . $this->db->escape($article_number) . "'";
|
||||
$sqlEan .= " AND barcode IS NOT NULL AND barcode != ''";
|
||||
$sqlEan .= " LIMIT 1";
|
||||
$resEan = $this->db->query($sqlEan);
|
||||
if ($resEan && $this->db->num_rows($resEan) > 0) {
|
||||
$objEan = $this->db->fetch_object($resEan);
|
||||
$foundEan = $objEan->barcode;
|
||||
}
|
||||
}
|
||||
|
||||
// If not searching all catalogs, return immediately
|
||||
if (!$searchAll) {
|
||||
|
|
@ -531,24 +544,15 @@ class Datanorm extends CommonObject
|
|||
}
|
||||
}
|
||||
|
||||
// If searchAll is enabled and we found article with EAN/manufacturer_ref,
|
||||
// search other catalogs using these identifiers (cross-catalog search)
|
||||
if ($searchAll && $fk_soc > 0 && (!empty($foundEan) || !empty($foundManufacturerRef))) {
|
||||
// If searchAll is enabled and we found article with EAN,
|
||||
// search other catalogs using EAN ONLY (cross-catalog search)
|
||||
// Note: Artikelnummern-Vergleich macht keinen Sinn über Kataloge hinweg
|
||||
if ($searchAll && $fk_soc > 0 && !empty($foundEan)) {
|
||||
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
|
||||
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
|
||||
$sql .= " price, price_unit, discount_group, product_group, matchcode";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE (";
|
||||
|
||||
$conditions = array();
|
||||
if (!empty($foundEan)) {
|
||||
$conditions[] = "ean = '" . $this->db->escape($foundEan) . "'";
|
||||
}
|
||||
if (!empty($foundManufacturerRef)) {
|
||||
$conditions[] = "manufacturer_ref = '" . $this->db->escape($foundManufacturerRef) . "'";
|
||||
}
|
||||
$sql .= implode(' OR ', $conditions) . ")";
|
||||
|
||||
$sql .= " WHERE ean = '" . $this->db->escape($foundEan) . "'";
|
||||
$sql .= " AND active = 1";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
$sql .= " AND fk_soc != " . (int) $fk_soc; // Exclude already found supplier
|
||||
|
|
@ -588,55 +592,44 @@ class Datanorm extends CommonObject
|
|||
}
|
||||
}
|
||||
|
||||
// Fallback: Search by partial match on article_number, ean, or manufacturer_ref
|
||||
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
|
||||
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
|
||||
$sql .= " price, price_unit, discount_group, product_group, matchcode";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE (article_number LIKE '" . $this->db->escape($article_number) . "%'";
|
||||
$sql .= " OR ean = '" . $this->db->escape($article_number) . "'";
|
||||
$sql .= " OR manufacturer_ref LIKE '" . $this->db->escape($article_number) . "%')";
|
||||
$sql .= " AND active = 1";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
|
||||
if ($fk_soc > 0 && !$searchAll) {
|
||||
// Fallback: Search by EXACT article number match for the specified supplier only
|
||||
// No LIKE search - cross-catalog comparisons only work via EAN
|
||||
if ($fk_soc > 0 && empty($results)) {
|
||||
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
|
||||
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
|
||||
$sql .= " price, price_unit, discount_group, product_group, matchcode";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE article_number = '" . $this->db->escape($article_number) . "'";
|
||||
$sql .= " AND fk_soc = " . (int) $fk_soc;
|
||||
}
|
||||
$sql .= " AND active = 1";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
// ORDER BY clause
|
||||
if ($fk_soc > 0 && $searchAll) {
|
||||
// Order by matching supplier first, then by price
|
||||
$sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, price ASC";
|
||||
} else {
|
||||
$sql .= " ORDER BY article_number";
|
||||
}
|
||||
|
||||
$sql .= " LIMIT " . (int) $limit;
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
if (!isset($foundIds[$obj->rowid])) {
|
||||
$results[] = array(
|
||||
'id' => $obj->rowid,
|
||||
'fk_soc' => $obj->fk_soc,
|
||||
'article_number' => $obj->article_number,
|
||||
'short_text1' => $obj->short_text1,
|
||||
'short_text2' => $obj->short_text2,
|
||||
'ean' => $obj->ean,
|
||||
'manufacturer_ref' => $obj->manufacturer_ref,
|
||||
'manufacturer_name' => $obj->manufacturer_name,
|
||||
'unit_code' => $obj->unit_code,
|
||||
'price' => $obj->price,
|
||||
'price_unit' => $obj->price_unit,
|
||||
'discount_group' => $obj->discount_group,
|
||||
'product_group' => $obj->product_group,
|
||||
'matchcode' => $obj->matchcode,
|
||||
);
|
||||
$foundIds[$obj->rowid] = true;
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
if (!isset($foundIds[$obj->rowid])) {
|
||||
$results[] = array(
|
||||
'id' => $obj->rowid,
|
||||
'fk_soc' => $obj->fk_soc,
|
||||
'article_number' => $obj->article_number,
|
||||
'short_text1' => $obj->short_text1,
|
||||
'short_text2' => $obj->short_text2,
|
||||
'ean' => $obj->ean,
|
||||
'manufacturer_ref' => $obj->manufacturer_ref,
|
||||
'manufacturer_name' => $obj->manufacturer_name,
|
||||
'unit_code' => $obj->unit_code,
|
||||
'price' => $obj->price,
|
||||
'price_unit' => $obj->price_unit,
|
||||
'discount_group' => $obj->discount_group,
|
||||
'product_group' => $obj->product_group,
|
||||
'matchcode' => $obj->matchcode,
|
||||
);
|
||||
$foundIds[$obj->rowid] = true;
|
||||
}
|
||||
}
|
||||
$this->db->free($resql);
|
||||
}
|
||||
$this->db->free($resql);
|
||||
}
|
||||
|
||||
return $results;
|
||||
|
|
@ -982,13 +975,13 @@ class Datanorm extends CommonObject
|
|||
|
||||
// P;A format - multiple articles per line
|
||||
// Format: P;A;ArtNr;PreisKz;Preis;PE;Zuschlag;x;x;x;ArtNr2;...
|
||||
// PE is the price unit code from DATPREIS (may differ from A-record!)
|
||||
// Rabattkennzeichen aus DATPREIS (wird gespeichert aber nicht fuer price_unit verwendet)
|
||||
if ($recordType === 'P' && isset($parts[1]) && $parts[1] === 'A') {
|
||||
$i = 2;
|
||||
while ($i < count($parts) - 2) {
|
||||
$articleNumber = trim($parts[$i] ?? '');
|
||||
$priceRaw = trim($parts[$i + 2] ?? '0');
|
||||
$datpreisPeCode = (int)trim($parts[$i + 3] ?? '0'); // PE code from DATPREIS
|
||||
$datpreisPeCode = (int)trim($parts[$i + 3] ?? '0'); // Rabattkennzeichen (nicht PE!)
|
||||
$metalSurchargeRaw = trim($parts[$i + 4] ?? '0');
|
||||
$price = (float)$priceRaw / 100; // Convert cents to euros
|
||||
$metalSurcharge = (float)$metalSurchargeRaw / 100; // Convert cents to euros
|
||||
|
|
@ -1007,7 +1000,7 @@ class Datanorm extends CommonObject
|
|||
// Simple format: P;ArtNr;PreisKz;Preis;PE;...
|
||||
$articleNumber = trim($parts[1] ?? '');
|
||||
$priceRaw = trim($parts[3] ?? '0');
|
||||
$datpreisPeCode = (int)trim($parts[4] ?? '0'); // PE code if available
|
||||
$datpreisPeCode = (int)trim($parts[4] ?? '0'); // Rabattkennzeichen (nicht PE!)
|
||||
|
||||
if (strpos($priceRaw, ',') === false && strpos($priceRaw, '.') === false) {
|
||||
$price = (float)$priceRaw / 100;
|
||||
|
|
|
|||
|
|
@ -624,6 +624,32 @@ class ZugferdImport extends CommonObject
|
|||
if (!empty($match) && $match['fk_product'] > 0) {
|
||||
$fk_product = $match['fk_product'];
|
||||
$match_method = $match['method'];
|
||||
|
||||
// Update supplier price with EAN from invoice if empty
|
||||
$invoiceEan = !empty($line_data['product']['global_id']) ? trim($line_data['product']['global_id']) : '';
|
||||
$supplierRef = !empty($line_data['product']['seller_id']) ? $line_data['product']['seller_id'] : '';
|
||||
if (!empty($invoiceEan) && !empty($supplierRef) && ctype_digit($invoiceEan)) {
|
||||
// Barcode-Typ basierend auf Länge bestimmen
|
||||
$eanLen = strlen($invoiceEan);
|
||||
if ($eanLen == 13) {
|
||||
$barcodeType = 2; // EAN13
|
||||
} elseif ($eanLen == 8) {
|
||||
$barcodeType = 1; // EAN8
|
||||
} elseif ($eanLen == 12) {
|
||||
$barcodeType = 3; // UPC-A
|
||||
} else {
|
||||
$barcodeType = 0; // Unbekannt
|
||||
}
|
||||
|
||||
$sqlEan = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sqlEan .= " SET barcode = '" . $this->db->escape($invoiceEan) . "'";
|
||||
$sqlEan .= ", fk_barcode_type = " . (int)$barcodeType;
|
||||
$sqlEan .= " WHERE fk_product = " . (int)$fk_product;
|
||||
$sqlEan .= " AND fk_soc = " . (int)$supplier_id;
|
||||
$sqlEan .= " AND ref_fourn = '" . $this->db->escape($supplierRef) . "'";
|
||||
$sqlEan .= " AND (barcode IS NULL OR barcode = '')";
|
||||
$this->db->query($sqlEan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -649,12 +675,12 @@ class ZugferdImport extends CommonObject
|
|||
$this->status = self::STATUS_IMPORTED;
|
||||
}
|
||||
|
||||
// Copy PDF to documents
|
||||
$destdir = $conf->importzugferd->dir_output . '/imports';
|
||||
// Copy PDF to documents (in subfolder by import ID)
|
||||
$destdir = $conf->importzugferd->dir_output . '/imports/' . $this->id;
|
||||
if (!is_dir($destdir)) {
|
||||
dol_mkdir($destdir);
|
||||
}
|
||||
$destfile = $destdir . '/' . $this->ref . '_' . basename($file_path);
|
||||
$destfile = $destdir . '/' . $this->pdf_filename;
|
||||
copy($file_path, $destfile);
|
||||
|
||||
// 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'
|
||||
|
||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||
$this->version = '3.3';
|
||||
$this->version = '5.5';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
@ -573,113 +573,173 @@ class modImportZugferd extends DolibarrModules
|
|||
$extrafields = new ExtraFields($this->db);
|
||||
|
||||
// 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(
|
||||
'supplier_customer_number', // attribute code
|
||||
'SupplierCustomerNumber', // label (translation key)
|
||||
'varchar', // type
|
||||
100, // position
|
||||
64, // size
|
||||
'thirdparty', // element type
|
||||
0, // unique
|
||||
0, // required
|
||||
'', // default value
|
||||
'', // param
|
||||
1, // always editable
|
||||
'', // permission
|
||||
1, // list (show in list)
|
||||
0, // printable
|
||||
'', // totalizable
|
||||
'', // langfile
|
||||
'importzugferd@importzugferd', // module
|
||||
'isModEnabled("importzugferd")' // enabled condition
|
||||
'supplier_customer_number', // 1. attribute code
|
||||
'SupplierCustomerNumber', // 2. label (translation key)
|
||||
'varchar', // 3. type
|
||||
100, // 4. position
|
||||
64, // 5. size
|
||||
'thirdparty', // 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)
|
||||
'', // 14. help
|
||||
'', // 15. computed
|
||||
'', // 16. entity
|
||||
'importzugferd@importzugferd', // 17. langfile
|
||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
||||
0, // 19. totalizable
|
||||
0 // 20. printable
|
||||
);
|
||||
|
||||
// Add extrafield for metal surcharge (Kupferzuschlag) on supplier prices
|
||||
$extrafields->addExtraField(
|
||||
'kupferzuschlag', // attribute code
|
||||
'Kupferzuschlag', // label (translation key)
|
||||
'price', // type (price field)
|
||||
110, // position
|
||||
'24,8', // size
|
||||
'product_fournisseur_price', // element type
|
||||
0, // unique
|
||||
0, // required
|
||||
'', // default value
|
||||
'', // param
|
||||
1, // always editable
|
||||
'', // permission
|
||||
1, // list (show in list)
|
||||
0, // printable
|
||||
'', // totalizable
|
||||
'', // langfile
|
||||
'importzugferd@importzugferd', // module
|
||||
'isModEnabled("importzugferd")' // enabled condition
|
||||
'kupferzuschlag', // 1. attribute code
|
||||
'Kupferzuschlag', // 2. label
|
||||
'price', // 3. type (price field)
|
||||
110, // 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)
|
||||
'Metallzuschlag (Kupfer) für diesen Einkaufspreis', // 14. help
|
||||
'', // 15. computed
|
||||
'', // 16. entity
|
||||
'importzugferd@importzugferd', // 17. langfile
|
||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
||||
0, // 19. totalizable
|
||||
0 // 20. printable
|
||||
);
|
||||
|
||||
// Add extrafield for product price without copper surcharge (only for cables)
|
||||
$extrafields->addExtraField(
|
||||
'produktpreis', // 1. attribute code
|
||||
'Produktpreis', // 2. label
|
||||
'price', // 3. type (price field)
|
||||
115, // 4. position
|
||||
'24,8', // 5. size
|
||||
'product_fournisseur_price', // 6. element type
|
||||
0, // 7. unique
|
||||
0, // 8. required
|
||||
'', // 9. default value
|
||||
'', // 10. param
|
||||
1, // 11. always editable
|
||||
'', // 12. permission
|
||||
1, // 13. list (show in list)
|
||||
'Materialpreis ohne Kupferzuschlag (nur bei Kabeln)', // 14. help
|
||||
'', // 15. computed
|
||||
'', // 16. entity
|
||||
'importzugferd@importzugferd', // 17. langfile
|
||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
||||
0, // 19. totalizable
|
||||
0 // 20. printable
|
||||
);
|
||||
|
||||
// Add extrafield for price unit (Preiseinheit) on supplier prices
|
||||
$extrafields->addExtraField(
|
||||
'preiseinheit', // attribute code
|
||||
'Preiseinheit', // label (translation key)
|
||||
'int', // type
|
||||
120, // position
|
||||
11, // size
|
||||
'product_fournisseur_price', // element type
|
||||
0, // unique
|
||||
0, // required
|
||||
'1', // default value
|
||||
'', // param
|
||||
1, // always editable
|
||||
'', // permission
|
||||
1, // list (show in list)
|
||||
0, // printable
|
||||
'', // totalizable
|
||||
'', // langfile
|
||||
'importzugferd@importzugferd', // module
|
||||
'isModEnabled("importzugferd")' // enabled condition
|
||||
'preiseinheit', // 1. attribute code
|
||||
'Preiseinheit', // 2. label
|
||||
'int', // 3. type
|
||||
120, // 4. position
|
||||
11, // 5. size
|
||||
'product_fournisseur_price', // 6. element type
|
||||
0, // 7. unique
|
||||
0, // 8. required
|
||||
'1', // 9. default value
|
||||
'', // 10. param
|
||||
1, // 11. always editable
|
||||
'', // 12. permission
|
||||
1, // 13. list (show in list)
|
||||
'Preiseinheit aus Datanorm (z.B. 100 für Preis pro 100m)', // 14. help
|
||||
'', // 15. computed
|
||||
'', // 16. entity
|
||||
'importzugferd@importzugferd', // 17. langfile
|
||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
||||
0, // 19. totalizable
|
||||
0 // 20. printable
|
||||
);
|
||||
|
||||
// Add extrafield for product group (Warengruppe) on supplier prices
|
||||
$extrafields->addExtraField(
|
||||
'warengruppe', // attribute code
|
||||
'Warengruppe', // label (translation key)
|
||||
'varchar', // type
|
||||
125, // position
|
||||
32, // size
|
||||
'product_fournisseur_price', // element type
|
||||
0, // unique
|
||||
0, // required
|
||||
'', // default value
|
||||
'', // param
|
||||
1, // always editable
|
||||
'', // permission
|
||||
1, // list (show in list)
|
||||
0, // printable
|
||||
'', // totalizable
|
||||
'', // langfile
|
||||
'importzugferd@importzugferd', // module
|
||||
'isModEnabled("importzugferd")' // enabled condition
|
||||
'warengruppe', // 1. attribute code
|
||||
'Warengruppe', // 2. label
|
||||
'varchar', // 3. type
|
||||
125, // 4. position
|
||||
32, // 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)
|
||||
'Datanorm-Warengruppe', // 14. help
|
||||
'', // 15. computed
|
||||
'', // 16. entity
|
||||
'importzugferd@importzugferd', // 17. langfile
|
||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
||||
0, // 19. totalizable
|
||||
0 // 20. printable
|
||||
);
|
||||
|
||||
// Add extrafield for purchase quantity (Kaufmenge) on supplier prices
|
||||
// Signature: addExtraField($attrname, $label, $type, $pos, $size, $elementtype, $unique, $required, $default_value, $param, $alwayseditable, $perms, $list, $help, $computed, $entity, $langfile, $enabled, $totalizable, $printable)
|
||||
$extrafields->addExtraField(
|
||||
'kaufmenge', // 1. attribute code
|
||||
'Kaufmenge (Datanorm-Vergleich)', // 2. label
|
||||
'int', // 3. type
|
||||
127, // 4. position
|
||||
11, // 5. size
|
||||
'product_fournisseur_price', // 6. element type
|
||||
0, // 7. unique
|
||||
0, // 8. required
|
||||
'', // 9. default value
|
||||
'', // 10. param (empty)
|
||||
1, // 11. always editable
|
||||
'', // 12. permission
|
||||
1, // 13. list (show in forms)
|
||||
'Tatsächliche Kaufmenge für Preisvergleiche. Beispiele: Lüsterklemme mit 12 Positionen = 1, Kabel 100m = leer lassen.', // 14. help
|
||||
'', // 15. computed
|
||||
'', // 16. entity
|
||||
'importzugferd@importzugferd', // 17. langfile
|
||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
||||
0, // 19. totalizable
|
||||
0 // 20. printable
|
||||
);
|
||||
|
||||
// Add extrafield for copper content (Kupfergehalt) on product - kg per km for cables
|
||||
$extrafields->addExtraField(
|
||||
'kupfergehalt', // attribute code
|
||||
'Kupfergehalt', // label (translation key)
|
||||
'double', // type (decimal number)
|
||||
130, // position
|
||||
'24,4', // size (precision,scale)
|
||||
'product', // element type
|
||||
0, // unique
|
||||
0, // required
|
||||
'', // default value
|
||||
'', // param
|
||||
1, // always editable
|
||||
'', // permission
|
||||
1, // list (show in list)
|
||||
0, // printable
|
||||
'', // totalizable
|
||||
'', // langfile
|
||||
'importzugferd@importzugferd', // module
|
||||
'isModEnabled("importzugferd")' // enabled condition
|
||||
'kupfergehalt', // 1. attribute code
|
||||
'Kupfergehalt', // 2. label
|
||||
'double', // 3. type (decimal number)
|
||||
130, // 4. position
|
||||
'24,4', // 5. size (precision,scale)
|
||||
'product', // 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)
|
||||
'Kupfergehalt in kg/km (für Kupferzuschlag-Berechnung bei Kabeln)', // 14. help
|
||||
'', // 15. computed
|
||||
'', // 16. entity
|
||||
'importzugferd@importzugferd', // 17. langfile
|
||||
'isModEnabled("importzugferd")', // 18. enabled condition
|
||||
0, // 19. totalizable
|
||||
0 // 20. printable
|
||||
);
|
||||
|
||||
// Permissions
|
||||
|
|
|
|||
|
|
@ -79,12 +79,18 @@ if ($isFormSubmitted) {
|
|||
$filter_description = GETPOSTINT('filter_description');
|
||||
$filter_label = GETPOSTINT('filter_label');
|
||||
$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 {
|
||||
// Defaults for first page load
|
||||
$filter_price = 1;
|
||||
$filter_description = 1;
|
||||
$filter_label = 0;
|
||||
$only_differences = 0;
|
||||
$hide_cables = 0;
|
||||
$filter_price_up = 0;
|
||||
$filter_price_down = 0;
|
||||
}
|
||||
|
||||
// Initialize objects
|
||||
|
|
@ -117,8 +123,8 @@ if ($action == 'apply_single' && GETPOSTINT('product_id') && GETPOST('datanorm_k
|
|||
setEventMessages($langs->trans('ErrorUpdatingProduct'), null, 'errors');
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Redirect to same page with same parameters (preserve all filters)
|
||||
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&hide_cables='.$hide_cables.'&filter_price_up='.$filter_price_up.'&filter_price_down='.$filter_price_down.'&action=search');
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
@ -137,8 +143,8 @@ if ($action == 'add_pending') {
|
|||
setEventMessages($langs->trans('AddedToPendingChanges'), null, 'mesgs');
|
||||
}
|
||||
|
||||
// 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.'&action=search');
|
||||
// Redirect back with same parameters to preserve supplier selection and filters
|
||||
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&hide_cables='.$hide_cables.'&filter_price_up='.$filter_price_up.'&filter_price_down='.$filter_price_down.'&action=search');
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
@ -192,8 +198,8 @@ if ($action == 'add_all_pending') {
|
|||
}
|
||||
}
|
||||
|
||||
// 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.'&action=search');
|
||||
// Redirect back with same parameters to preserve supplier selection and filters
|
||||
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&hide_cables='.$hide_cables.'&filter_price_up='.$filter_price_up.'&filter_price_down='.$filter_price_down.'&action=search');
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
@ -322,8 +328,7 @@ if ($obj->cnt == 0) {
|
|||
}
|
||||
|
||||
// Search form
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" name="searchform">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'" name="searchform">';
|
||||
print '<input type="hidden" name="action" value="search">';
|
||||
|
||||
print '<div class="fichecenter">';
|
||||
|
|
@ -414,6 +419,21 @@ print '<td>'.$langs->trans('Display').'</td>';
|
|||
print '<td colspan="3">';
|
||||
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 ' ';
|
||||
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 '</tr>';
|
||||
|
||||
|
|
@ -589,6 +609,45 @@ if ($fk_soc > 0 && ($action == 'search' || GETPOST('search_mode'))) {
|
|||
print '</tr>';
|
||||
|
||||
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']) ||
|
||||
($filter_description && $item['description_differs']) ||
|
||||
($filter_label && $item['label_differs']);
|
||||
|
|
@ -623,50 +682,72 @@ if ($fk_soc > 0 && ($action == 'search' || GETPOST('search_mode'))) {
|
|||
$priceStyle = $item['price_differs'] ? 'background-color: #fcf8e3;' : '';
|
||||
print '<td class="right nowraponall" style="'.$priceStyle.'">';
|
||||
if ($item['product_id'] > 0) {
|
||||
print price($item['current_price']);
|
||||
// Show copper surcharge from invoice if available
|
||||
if (!empty($item['current_kupferzuschlag']) && $item['current_kupferzuschlag'] > 0) {
|
||||
print '<br><span class="opacitymedium small" title="Kupferzuschlag aus Rechnung">';
|
||||
print '<i class="fas fa-plus-circle" style="color:#f0ad4e;"></i> '.price($item['current_kupferzuschlag']);
|
||||
print '</span>';
|
||||
// Show total price (material + surcharge)
|
||||
$totalWithSurcharge = $item['current_price'] + $item['current_kupferzuschlag'];
|
||||
print '<br><span class="small" style="color:#337ab7;" title="Gesamt (Material+CU)">';
|
||||
print '<strong>='.price($totalWithSurcharge).'</strong>';
|
||||
print '</span>';
|
||||
$dolibarr_total = isset($item['current_total_price']) ? $item['current_total_price'] : $item['current_price'];
|
||||
$dolibarr_qty = isset($item['current_quantity']) ? $item['current_quantity'] : 1;
|
||||
$dolibarr_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0;
|
||||
|
||||
// IMPORTANT: Dolibarr price already includes Cu! Show as info only
|
||||
if ($dolibarr_cu > 0) {
|
||||
print '<span class="opacitymedium small">(davon '.price($dolibarr_cu).' Cu)</span><br>';
|
||||
}
|
||||
|
||||
// Total price for minimum quantity (already includes Cu!)
|
||||
print '<strong>'.price($dolibarr_total);
|
||||
if ($dolibarr_qty > 1) {
|
||||
print '/'.$dolibarr_qty;
|
||||
}
|
||||
print '</strong>';
|
||||
|
||||
// Unit price as secondary info
|
||||
if ($dolibarr_qty > 1) {
|
||||
$dolibarr_unit = $dolibarr_total / $dolibarr_qty;
|
||||
print '<br><span class="opacitymedium small">('.price($dolibarr_unit).'/Stk.)</span>';
|
||||
}
|
||||
} else {
|
||||
print '-';
|
||||
}
|
||||
print '</td>';
|
||||
print '<td class="right nowraponall" style="'.$priceStyle.'">';
|
||||
print price($item['datanorm_price']);
|
||||
// Show original price and unit if price_unit > 1
|
||||
if (!empty($item['datanorm_price_unit']) && $item['datanorm_price_unit'] > 1) {
|
||||
print '<br><span class="opacitymedium small">('.price($item['datanorm_price_raw']).'/'.$item['datanorm_price_unit'].')</span>';
|
||||
}
|
||||
// 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';
|
||||
$datanorm_pe = isset($item['datanorm_price_unit']) ? $item['datanorm_price_unit'] : 1;
|
||||
$datanorm_raw = isset($item['datanorm_price_raw']) ? $item['datanorm_price_raw'] : $item['datanorm_price'];
|
||||
$current_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0;
|
||||
$effective_qty = isset($item['current_effective_quantity']) ? $item['current_effective_quantity'] : 1;
|
||||
|
||||
print '<br><span class="opacitymedium small" title="Kupferzuschlag ('.$sourceLabel.')">';
|
||||
print '<i class="fas fa-plus-circle" style="color:'.$sourceColor.';"></i> '.price($item['effective_surcharge']);
|
||||
print '</span>';
|
||||
// Show total price with surcharge
|
||||
if (!empty($item['datanorm_price_with_surcharge'])) {
|
||||
print '<br><span class="small" style="color:#337ab7;" title="Gesamt (Material+CU)">';
|
||||
print '<strong>='.price($item['datanorm_price_with_surcharge']).'</strong>';
|
||||
print '</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>';
|
||||
}
|
||||
|
||||
// Total price for Datanorm price_unit (with scaled Cu)
|
||||
$datanorm_total = $datanorm_raw + $cu_for_pe;
|
||||
print '<strong>'.price($datanorm_total);
|
||||
if ($datanorm_pe > 1) {
|
||||
print '/'.$datanorm_pe;
|
||||
}
|
||||
print '</strong>';
|
||||
|
||||
// Unit price as secondary info
|
||||
if ($datanorm_pe > 1) {
|
||||
$datanorm_unit = $datanorm_total / $datanorm_pe;
|
||||
print '<br><span class="opacitymedium small">('.price($datanorm_unit).'/Stk.)</span>';
|
||||
}
|
||||
if ($item['price_differs'] && $item['product_id'] > 0) {
|
||||
$diff = $item['datanorm_price'] - $item['current_price'];
|
||||
$diffPercent = ($item['current_price'] > 0) ? ($diff / $item['current_price'] * 100) : 0;
|
||||
// Calculate percentage difference using UNIT PRICE basis
|
||||
$current_total = isset($item['current_total_price']) ? $item['current_total_price'] : $item['current_price'];
|
||||
|
||||
// Dolibarr: unit price (already includes Cu)
|
||||
$current_compare = $effective_qty > 0 ? $current_total / $effective_qty : $current_total;
|
||||
|
||||
// Datanorm: unit price (material + scaled Cu)
|
||||
$datanorm_compare = $datanorm_total / $datanorm_pe;
|
||||
|
||||
$diff = $datanorm_compare - $current_compare;
|
||||
$diffPercent = ($current_compare > 0) ? ($diff / $current_compare * 100) : 0;
|
||||
print '<br>';
|
||||
if ($diff > 0) {
|
||||
print '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($diffPercent, 1).'%</span>';
|
||||
|
|
@ -715,6 +796,9 @@ if ($fk_soc > 0 && ($action == 'search' || GETPOST('search_mode'))) {
|
|||
print '<input type="hidden" name="filter_description" value="'.$filter_description.'">';
|
||||
print '<input type="hidden" name="filter_label" value="'.$filter_label.'">';
|
||||
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
|
||||
if ($filter_price && $item['price_differs']) {
|
||||
|
|
@ -1101,14 +1185,23 @@ function searchDatanormProducts($db, $fk_soc, $search_term, $search_by_name = 0,
|
|||
$datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
|
||||
|
||||
// Get current price and copper surcharge from extrafield
|
||||
$current_price = 0;
|
||||
$current_total_price = 0;
|
||||
$current_quantity = 1;
|
||||
$current_kaufmenge = 0;
|
||||
$current_kupferzuschlag = 0;
|
||||
$datanorm_price_unit_code = isset($datanorm->price_unit_code) ? $datanorm->price_unit_code : -1;
|
||||
|
||||
if ($product) {
|
||||
$priceDetails = getSupplierPriceDetails($db, $product->rowid, $fk_soc);
|
||||
$current_price = $priceDetails['unitprice'];
|
||||
$current_total_price = $priceDetails['price'];
|
||||
$current_quantity = $priceDetails['quantity'];
|
||||
$current_kaufmenge = $priceDetails['kaufmenge'];
|
||||
$current_kupferzuschlag = $priceDetails['kupferzuschlag'];
|
||||
}
|
||||
|
||||
// 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
|
||||
if ($current_kupferzuschlag > 0) {
|
||||
$effective_surcharge = $current_kupferzuschlag;
|
||||
|
|
@ -1118,14 +1211,43 @@ function searchDatanormProducts($db, $fk_soc, $search_term, $search_by_name = 0,
|
|||
$surcharge_source = 'datanorm';
|
||||
}
|
||||
|
||||
// Calculate prices
|
||||
// Calculate prices for comparison - UNIT PRICE basis
|
||||
// IMPORTANT: Dolibarr price already INCLUDES kupferzuschlag! Don't add it again!
|
||||
// Datanorm price is WITHOUT kupferzuschlag, so add SCALED Cu for comparison
|
||||
//
|
||||
// Example: Kabel NYM-J 5x10
|
||||
// - Dolibarr: 331,27€ for 50m (includes 254,55€ Cu for 50m) → 6,63€/m
|
||||
// - Datanorm: 168,50€ for 100m (PE=100)
|
||||
// - Cu per unit: 254,55€ / 50m = 5,09€/m → for 100m = 509,10€
|
||||
// - Datanorm total: 168,50€ + 509,10€ = 677,60€ → 6,78€/m
|
||||
|
||||
// Calculate Cu per unit (from Dolibarr's quantity basis)
|
||||
$cu_per_unit = ($current_kupferzuschlag > 0 && $effective_quantity > 0)
|
||||
? $current_kupferzuschlag / $effective_quantity
|
||||
: 0;
|
||||
|
||||
// Scale Cu to Datanorm's price_unit basis
|
||||
$cu_for_price_unit = $cu_per_unit * $price_unit;
|
||||
|
||||
// Dolibarr: unit price (already includes Cu)
|
||||
$current_compare_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price;
|
||||
|
||||
// Datanorm: material price + scaled Cu, then to unit price
|
||||
$datanorm_compare_price = ($datanorm->price + $cu_for_price_unit) / $price_unit;
|
||||
|
||||
// For display: always show unit prices
|
||||
$datanorm_material_unit_price = $datanorm->price / $price_unit;
|
||||
$total_price_with_surcharge = $datanorm->price + ($effective_surcharge * $price_unit);
|
||||
$total_price_with_surcharge = $datanorm->price + $cu_for_price_unit;
|
||||
$datanorm_total_unit_price = $total_price_with_surcharge / $price_unit;
|
||||
$current_unit_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price;
|
||||
|
||||
$results[] = array(
|
||||
'product_id' => $product ? $product->rowid : 0,
|
||||
'current_price' => $current_price,
|
||||
'current_price' => $current_unit_price,
|
||||
'current_total_price' => $current_total_price,
|
||||
'current_quantity' => $current_quantity,
|
||||
'current_kaufmenge' => $current_kaufmenge,
|
||||
'current_effective_quantity' => $effective_quantity,
|
||||
'current_kupferzuschlag' => $current_kupferzuschlag,
|
||||
'current_description' => $product ? $product->description : '',
|
||||
'current_label' => $product ? $product->label : '',
|
||||
|
|
@ -1137,12 +1259,14 @@ function searchDatanormProducts($db, $fk_soc, $search_term, $search_by_name = 0,
|
|||
'datanorm_price_raw' => $datanorm->price,
|
||||
'datanorm_material_price' => $datanorm->price,
|
||||
'datanorm_metal_surcharge' => $datanorm_metal_surcharge,
|
||||
'datanorm_price_unit_code' => $datanorm_price_unit_code,
|
||||
'effective_surcharge' => $effective_surcharge,
|
||||
'surcharge_source' => $surcharge_source,
|
||||
'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_label' => $datanorm->short_text1,
|
||||
'price_differs' => $product && abs($current_price - $datanorm_material_unit_price) > 0.01,
|
||||
'price_differs' => $product && abs($current_compare_price - $datanorm_compare_price) > 0.01,
|
||||
'description_differs' => $product && $product->description != trim($datanorm->short_text1.' '.$datanorm->short_text2),
|
||||
'label_differs' => $product && $product->label != $datanorm->short_text1,
|
||||
);
|
||||
|
|
@ -1276,14 +1400,17 @@ function getSupplierPrice($db, $product_id, $fk_soc)
|
|||
function getSupplierPriceDetails($db, $product_id, $fk_soc)
|
||||
{
|
||||
$result = array(
|
||||
'price' => 0,
|
||||
'quantity' => 1,
|
||||
'kaufmenge' => 0,
|
||||
'unitprice' => 0,
|
||||
'kupferzuschlag' => 0,
|
||||
'preiseinheit' => 1,
|
||||
'price_id' => 0,
|
||||
);
|
||||
|
||||
// Get base price
|
||||
$sql = "SELECT pf.rowid, pf.unitprice, pf.price, pf.quantity";
|
||||
// Get base price - ALWAYS load price + quantity, NOT unitprice alone!
|
||||
$sql = "SELECT pf.rowid, pf.price, pf.quantity";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price pf";
|
||||
$sql .= " WHERE pf.fk_product = ".((int)$product_id);
|
||||
$sql .= " AND pf.fk_soc = ".((int)$fk_soc);
|
||||
|
|
@ -1293,18 +1420,14 @@ function getSupplierPriceDetails($db, $product_id, $fk_soc)
|
|||
if ($resql && $db->num_rows($resql) > 0) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
$result['price_id'] = $obj->rowid;
|
||||
$result['price'] = (float)$obj->price;
|
||||
$result['quantity'] = max(1, (int)$obj->quantity);
|
||||
|
||||
// Calculate unit 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;
|
||||
}
|
||||
// Calculate unit price from price / quantity
|
||||
$result['unitprice'] = $result['quantity'] > 0 ? $result['price'] / $result['quantity'] : $result['price'];
|
||||
|
||||
// Get extrafields (Kupferzuschlag, Preiseinheit)
|
||||
$sql_extra = "SELECT kupferzuschlag, preiseinheit";
|
||||
// Get extrafields (Kupferzuschlag, Preiseinheit, Kaufmenge)
|
||||
$sql_extra = "SELECT kupferzuschlag, preiseinheit, kaufmenge";
|
||||
$sql_extra .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
|
||||
$sql_extra .= " WHERE fk_object = ".((int)$obj->rowid);
|
||||
|
||||
|
|
@ -1313,6 +1436,7 @@ function getSupplierPriceDetails($db, $product_id, $fk_soc)
|
|||
$extra = $db->fetch_object($res_extra);
|
||||
$result['kupferzuschlag'] = !empty($extra->kupferzuschlag) ? (float)$extra->kupferzuschlag : 0;
|
||||
$result['preiseinheit'] = !empty($extra->preiseinheit) ? (int)$extra->preiseinheit : 1;
|
||||
$result['kaufmenge'] = !empty($extra->kaufmenge) ? (int)$extra->kaufmenge : 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1393,14 +1517,21 @@ function buildComparisonResult($product, $datanorm)
|
|||
global $db;
|
||||
|
||||
$fk_soc = $datanorm->fk_soc;
|
||||
// Get supplier price details including extrafields (Kupferzuschlag)
|
||||
// Get supplier price details including extrafields (Kupferzuschlag, Kaufmenge)
|
||||
$priceDetails = getSupplierPriceDetails($db, $product->fk_product, $fk_soc);
|
||||
$current_price = $priceDetails['unitprice'];
|
||||
$current_total_price = $priceDetails['price'];
|
||||
$current_quantity = $priceDetails['quantity'];
|
||||
$current_kaufmenge = $priceDetails['kaufmenge']; // Actual purchase quantity (if set)
|
||||
$current_kupferzuschlag = $priceDetails['kupferzuschlag'];
|
||||
|
||||
// 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)
|
||||
// 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;
|
||||
$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;
|
||||
|
||||
// Priority for surcharge: 1) Invoice extrafield, 2) Datanorm
|
||||
|
|
@ -1412,14 +1543,43 @@ function buildComparisonResult($product, $datanorm)
|
|||
$surcharge_source = 'datanorm';
|
||||
}
|
||||
|
||||
// Calculate prices
|
||||
// Calculate prices for comparison
|
||||
// IMPORTANT: Dolibarr price already INCLUDES kupferzuschlag! Don't add it again!
|
||||
// Datanorm price is WITHOUT kupferzuschlag, so add it for comparison
|
||||
// Compare on UNIT PRICE basis (per 1 piece/meter)
|
||||
//
|
||||
// Example: Kabel NYM-J 5x10
|
||||
// - Dolibarr: 331,27€ for 50m (includes 254,55€ Cu for 50m) → 6,63€/m
|
||||
// - Datanorm: 168,50€ for 100m (PE=100) + Cu must be scaled to 100m
|
||||
// - Cu per unit: 254,55€ / 50m = 5,09€/m → for 100m = 509,10€
|
||||
// - Datanorm total for 100m: 168,50€ + 509,10€ = 677,60€ → 6,78€/m
|
||||
|
||||
// Calculate Cu per unit (from Dolibarr's quantity basis)
|
||||
$cu_per_unit = ($current_kupferzuschlag > 0 && $effective_quantity > 0)
|
||||
? $current_kupferzuschlag / $effective_quantity
|
||||
: 0;
|
||||
|
||||
// Scale Cu to Datanorm's price_unit basis
|
||||
$cu_for_price_unit = $cu_per_unit * $price_unit;
|
||||
|
||||
// Dolibarr: unit price (already includes Cu)
|
||||
$current_compare_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price;
|
||||
|
||||
// Datanorm: material price + scaled Cu, then to unit price
|
||||
$datanorm_compare_price = ($datanorm->price + $cu_for_price_unit) / $price_unit;
|
||||
|
||||
// For display: always show unit prices
|
||||
$datanorm_material_unit_price = $datanorm->price / $price_unit;
|
||||
$total_price_with_surcharge = $datanorm->price + ($effective_surcharge * $price_unit);
|
||||
$total_price_with_surcharge = $datanorm->price + $cu_for_price_unit;
|
||||
$datanorm_total_unit_price = $total_price_with_surcharge / $price_unit;
|
||||
|
||||
return array(
|
||||
'product_id' => $product->fk_product,
|
||||
'current_price' => $current_price,
|
||||
'current_price' => $current_unit_price,
|
||||
'current_total_price' => $current_total_price,
|
||||
'current_quantity' => $current_quantity,
|
||||
'current_kaufmenge' => $current_kaufmenge,
|
||||
'current_effective_quantity' => $effective_quantity,
|
||||
'current_kupferzuschlag' => $current_kupferzuschlag,
|
||||
'current_description' => $product->description,
|
||||
'current_label' => $product->label,
|
||||
|
|
@ -1431,12 +1591,14 @@ function buildComparisonResult($product, $datanorm)
|
|||
'datanorm_price_raw' => $datanorm->price, // Raw price from DATPREIS
|
||||
'datanorm_material_price' => $datanorm->price,
|
||||
'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
|
||||
'surcharge_source' => $surcharge_source, // Source of surcharge (invoice/datanorm)
|
||||
'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_label' => $datanorm->short_text1,
|
||||
'price_differs' => abs($current_price - $datanorm_material_unit_price) > 0.01,
|
||||
'price_differs' => abs($current_compare_price - $datanorm_compare_price) > 0.01,
|
||||
'description_differs' => $product->description != trim($datanorm->short_text1.' '.$datanorm->short_text2),
|
||||
'label_differs' => $product->label != $datanorm->short_text1,
|
||||
);
|
||||
|
|
@ -1464,9 +1626,31 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
|
|||
// Calculate unit price (Datanorm price may be per price_unit pieces)
|
||||
// Total price = material price + metal surcharge (for cables)
|
||||
$price_unit = (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) ? $datanorm->price_unit : 1;
|
||||
$metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
|
||||
$total_price = $datanorm->price + $metal_surcharge;
|
||||
$datanorm_unit_price = $total_price / $price_unit;
|
||||
|
||||
// Get existing supplier price details to get kupferzuschlag and quantity from extrafield
|
||||
$priceDetails = getSupplierPriceDetails($db, $product_id, $fk_soc);
|
||||
$current_kupferzuschlag = $priceDetails['kupferzuschlag'];
|
||||
$current_quantity = $priceDetails['quantity'];
|
||||
$current_kaufmenge = $priceDetails['kaufmenge'];
|
||||
$effective_quantity = ($current_kaufmenge > 0) ? $current_kaufmenge : max(1, $current_quantity);
|
||||
|
||||
// Priority for surcharge: 1) Dolibarr extrafield (from invoice), 2) Datanorm metal_surcharge
|
||||
$datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
|
||||
|
||||
// Scale Cu from Dolibarr's quantity to Datanorm's price_unit
|
||||
// Example: Cu 152,73€ for 50m → per meter = 3,05€ → for 100m = 305,46€
|
||||
$cu_per_unit = ($current_kupferzuschlag > 0 && $effective_quantity > 0)
|
||||
? $current_kupferzuschlag / $effective_quantity
|
||||
: 0;
|
||||
$cu_for_price_unit = $cu_per_unit * $price_unit;
|
||||
|
||||
// Use scaled Cu, or fallback to Datanorm metal_surcharge
|
||||
$effective_surcharge = ($cu_for_price_unit > 0) ? $cu_for_price_unit : $datanorm_metal_surcharge;
|
||||
|
||||
// Total price for price_unit includes scaled surcharge
|
||||
$total_price_for_pe = $datanorm->price + $effective_surcharge;
|
||||
// Unit price (per 1 piece/meter)
|
||||
$datanorm_unit_price = $total_price_for_pe / $price_unit;
|
||||
|
||||
// Load product
|
||||
$product = new Product($db);
|
||||
|
|
@ -1494,20 +1678,7 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
|
|||
$updated = true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Update label only (description goes to supplier price desc_fourn below)
|
||||
// Save product changes
|
||||
if ($updated) {
|
||||
$result = $product->update($product->id, $user);
|
||||
|
|
@ -1516,8 +1687,8 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
|
|||
}
|
||||
}
|
||||
|
||||
// Update supplier price
|
||||
if ($apply_price) {
|
||||
// Update supplier price and/or description
|
||||
if ($apply_price || $apply_description) {
|
||||
$productFourn = new ProductFournisseur($db);
|
||||
$productFourn->fetch($product_id);
|
||||
|
||||
|
|
@ -1526,7 +1697,7 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
|
|||
$supplier->fetch($fk_soc);
|
||||
|
||||
// Find existing supplier price
|
||||
$sql = "SELECT rowid, quantity, price, unitprice FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
||||
$sql = "SELECT rowid, quantity, price, unitprice, desc_fourn FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
||||
$sql .= " WHERE fk_product = ".((int)$product_id);
|
||||
$sql .= " AND fk_soc = ".((int)$fk_soc);
|
||||
$sql .= " ORDER BY rowid DESC LIMIT 1";
|
||||
|
|
@ -1534,54 +1705,66 @@ function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $a
|
|||
$resql = $db->query($sql);
|
||||
if ($resql && $db->num_rows($resql) > 0) {
|
||||
$priceObj = $db->fetch_object($resql);
|
||||
$price_rowid = $priceObj->rowid;
|
||||
|
||||
// Get the actual unit price from Dolibarr (price per 1 piece)
|
||||
$current_unit_price = (!empty($priceObj->unitprice) && $priceObj->unitprice > 0)
|
||||
? $priceObj->unitprice
|
||||
: ($priceObj->quantity > 0 ? $priceObj->price / $priceObj->quantity : $priceObj->price);
|
||||
// Use effective_quantity (kaufmenge if set, otherwise quantity) for price comparison
|
||||
// This ensures consistent comparison with buildComparisonResult()
|
||||
$effective_qty = ($priceDetails['kaufmenge'] > 0) ? $priceDetails['kaufmenge'] : max(1, $priceObj->quantity);
|
||||
|
||||
// Only update if unit price differs
|
||||
if (abs($current_unit_price - $datanorm_unit_price) > 0.01) {
|
||||
// Get the actual unit price from Dolibarr (price per 1 effective piece)
|
||||
$current_unit_price = $effective_qty > 0 ? $priceObj->price / $effective_qty : $priceObj->price;
|
||||
|
||||
// Prepare new description if requested
|
||||
$new_desc_fourn = null;
|
||||
if ($apply_description) {
|
||||
$new_desc_fourn = trim($datanorm->short_text1.' '.$datanorm->short_text2);
|
||||
if ($priceObj->desc_fourn != $new_desc_fourn) {
|
||||
$changes[] = array(
|
||||
'field' => 'desc_fourn',
|
||||
'old' => $priceObj->desc_fourn,
|
||||
'new' => $new_desc_fourn
|
||||
);
|
||||
} else {
|
||||
$new_desc_fourn = null; // No change needed
|
||||
}
|
||||
}
|
||||
|
||||
// Check if price needs update
|
||||
$price_changed = $apply_price && (abs($current_unit_price - $datanorm_unit_price) > 0.01);
|
||||
|
||||
if ($price_changed) {
|
||||
$changes[] = array(
|
||||
'field' => 'price',
|
||||
'old' => $current_unit_price,
|
||||
'new' => $datanorm_unit_price
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate total price for the quantity (Dolibarr expects total price, not unit price)
|
||||
// Dolibarr will calculate: unitprice = price / quantity
|
||||
$total_price_for_qty = $datanorm_unit_price * $priceObj->quantity;
|
||||
// Update only the fields that need changing (preserves all other fields!)
|
||||
if ($price_changed || $new_desc_fourn !== null) {
|
||||
$update_fields = array();
|
||||
|
||||
// Update existing price - $supplier must be Societe object, not integer ID
|
||||
$result = $productFourn->update_buyprice(
|
||||
$priceObj->quantity,
|
||||
$total_price_for_qty,
|
||||
$user,
|
||||
'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 ($price_changed) {
|
||||
// Calculate total price for the quantity, round to 2 decimals
|
||||
$total_price_for_qty = round($datanorm_unit_price * $priceObj->quantity, 2);
|
||||
$rounded_unit_price = round($datanorm_unit_price, 2);
|
||||
$update_fields[] = "price = ".((float)$total_price_for_qty);
|
||||
$update_fields[] = "unitprice = ".((float)$rounded_unit_price);
|
||||
}
|
||||
|
||||
if ($result < 0) {
|
||||
return -4;
|
||||
if ($new_desc_fourn !== null) {
|
||||
$update_fields[] = "desc_fourn = '".$db->escape($new_desc_fourn)."'";
|
||||
}
|
||||
|
||||
if (!empty($update_fields)) {
|
||||
$sql_update = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price";
|
||||
$sql_update .= " SET ".implode(", ", $update_fields);
|
||||
$sql_update .= " WHERE rowid = ".(int)$price_rowid;
|
||||
|
||||
$result = $db->query($sql_update);
|
||||
if (!$result) {
|
||||
return -4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1189
import.php
1189
import.php
File diff suppressed because it is too large
Load diff
|
|
@ -54,6 +54,9 @@ $langs->loadLangs(array("importzugferd@importzugferd"));
|
|||
if (!isModEnabled('importzugferd')) {
|
||||
accessforbidden('Module not enabled');
|
||||
}
|
||||
if (!$user->hasRight('importzugferd', 'import', 'read')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
|
|
|
|||
|
|
@ -451,6 +451,8 @@ LabelChange = Namensänderung
|
|||
#
|
||||
Kupferzuschlag = Kupferzuschlag
|
||||
KupferzuschlagHelp = Metallzuschlag pro Einheit (wird aus Rechnungen extrahiert)
|
||||
Produktpreis = Produktpreis
|
||||
ProduktpreisHelp = Reiner Materialpreis ohne Kupferzuschlag (nur bei Kabeln)
|
||||
Preiseinheit = Preiseinheit
|
||||
PreiseinheitHelp = Anzahl Einheiten pro Preis (z.B. 100 = Preis pro 100 Stück)
|
||||
Warengruppe = Warengruppe
|
||||
|
|
@ -486,3 +488,10 @@ AddSelectedPrices = Ausgewählte hinzufügen
|
|||
SupplierPricesAdded = %s Lieferantenpreise hinzugefügt
|
||||
CheaperBy = %s%% günstiger
|
||||
MoreExpensiveBy = %s%% teurer
|
||||
RefreshProductListHelp = Produktlisten neu laden (nach Anlage neuer Produkte)
|
||||
SelectAll = Alle auswählen
|
||||
DeselectAll = Keine auswählen
|
||||
|
||||
# UI Buttons
|
||||
ExpandAll = Alle aufklappen
|
||||
CollapseAll = Alle zuklappen
|
||||
|
|
|
|||
|
|
@ -389,6 +389,8 @@ NotifyEmail = Recipient email
|
|||
#
|
||||
Kupferzuschlag = Copper Surcharge
|
||||
KupferzuschlagHelp = Metal surcharge per unit (extracted from invoices)
|
||||
Produktpreis = Material Price
|
||||
ProduktpreisHelp = Material price without copper surcharge (cables only)
|
||||
Preiseinheit = Price Unit
|
||||
PreiseinheitHelp = Number of units per price (e.g. 100 = price per 100 pieces)
|
||||
Warengruppe = Product Group
|
||||
|
|
@ -417,3 +419,10 @@ AddSelectedPrices = Add Selected
|
|||
SupplierPricesAdded = %s supplier prices added
|
||||
CheaperBy = %s%% cheaper
|
||||
MoreExpensiveBy = %s%% more expensive
|
||||
RefreshProductListHelp = Refresh product lists (after creating new products)
|
||||
SelectAll = Select all
|
||||
DeselectAll = Deselect all
|
||||
|
||||
# UI Buttons
|
||||
ExpandAll = Expand all
|
||||
CollapseAll = Collapse all
|
||||
|
|
|
|||
|
|
@ -15,3 +15,5 @@ ALTER TABLE llx_importzugferd_datanorm ADD COLUMN price_type tinyint DEFAULT 1 A
|
|||
ALTER TABLE llx_importzugferd_datanorm ADD COLUMN metal_surcharge double(24,8) DEFAULT 0 AFTER price_type;
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD COLUMN vpe integer DEFAULT NULL AFTER metal_surcharge;
|
||||
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