feat: v5.5 - Kupferzuschlag-Skalierung und alternative Datanorm-Preise
## Behoben - Kupferzuschlag-Skalierung in Massenaktualisierung (Cu/qty * PE) - Steuersatz bei Preisübernahme wird beibehalten - Preise auf 2 Dezimalstellen gerundet ## Hinzugefügt - Filter für Preisrichtung (rauf/runter) - Filter-Persistenz nach Preisübernahme - Alternative Datanorm-Preise erben Mindestmenge, Verpackung, Steuersatz, kaufmenge - Extrafield kaufmenge sichtbar in Formularen ## Geändert - Kupferzuschlag wird NICHT vom Import gesetzt (separates Modul) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
77a1781182
59 changed files with 19507 additions and 0 deletions
135
CHANGELOG.md
Executable file
135
CHANGELOG.md
Executable file
|
|
@ -0,0 +1,135 @@
|
|||
# Changelog
|
||||
|
||||
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
|
||||
|
||||
## [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
|
||||
621
COPYING
Executable file
621
COPYING
Executable file
|
|
@ -0,0 +1,621 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
107
ChangeLog.md
Executable file
107
ChangeLog.md
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
# Changelog
|
||||
|
||||
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
|
||||
|
||||
## [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
|
||||
141
README.md
Executable file
141
README.md
Executable file
|
|
@ -0,0 +1,141 @@
|
|||
# ZUGFeRD Import for [Dolibarr ERP & CRM](https://www.dolibarr.org)
|
||||
|
||||
Import ZUGFeRD/Factur-X electronic invoices as supplier invoices in Dolibarr.
|
||||
|
||||
## Features
|
||||
|
||||
### Core Functionality
|
||||
- **PDF Import**: Upload ZUGFeRD/Factur-X PDF invoices with embedded XML data
|
||||
- **XML Parsing**: Automatic extraction and parsing of invoice data from embedded XML
|
||||
- **Duplicate Detection**: SHA256 hash-based detection prevents importing the same invoice twice
|
||||
- **Supplier Detection**: Automatic supplier matching via VAT ID or customer reference number
|
||||
|
||||
### Product Matching
|
||||
- **Multi-Method Matching**: Products are matched via:
|
||||
- Article mapping (supplier article number → your product)
|
||||
- EAN/GTIN barcode
|
||||
- Product reference
|
||||
- Manufacturer reference
|
||||
- **Manual Assignment**: Assign products manually when automatic matching fails
|
||||
- **Product Creation**: Create new products directly from import data
|
||||
- **Product Templates**: Duplicate existing products with ZUGFeRD data pre-filled
|
||||
- **EAN Auto-Update**: Automatically updates product barcodes from invoice data
|
||||
|
||||
### Workflow
|
||||
- **Persistent Import Records**: Imports are saved to database immediately
|
||||
- **Status Tracking**:
|
||||
- `Imported` - Ready for invoice creation
|
||||
- `Pending` - Manual intervention required (missing products/supplier)
|
||||
- `Processed` - Supplier invoice created
|
||||
- `Error` - Import failed
|
||||
- **Resume Anytime**: Continue editing imports later
|
||||
- **Sum Validation**: Validates totals between ZUGFeRD data and created invoice
|
||||
|
||||
### Batch Import
|
||||
- **Folder Monitoring**: Import from a local folder (watch folder)
|
||||
- **IMAP Import**: Import from email mailbox
|
||||
- **Automatic Archiving**: Successfully imported files are moved to archive
|
||||
|
||||
### Unit Code Translation
|
||||
- Translates UN/ECE unit codes (C62, MTR, LTR, etc.) to readable labels (Stk., m, l)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Dolibarr 19.0 or higher
|
||||
- PHP 7.1 or higher
|
||||
- PHP IMAP extension (for email import functionality)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy the `importzugferd` folder to your Dolibarr `custom` directory
|
||||
2. Enable the module in **Setup > Modules > ZUGFeRD Import**
|
||||
3. Configure settings in **ZUGFeRD Import > Setup**
|
||||
|
||||
## Configuration
|
||||
|
||||
### IMAP Settings (for email import)
|
||||
- IMAP Server hostname
|
||||
- Port (993 for SSL, 143 for STARTTLS)
|
||||
- Username and password
|
||||
- Mailbox folder to monitor
|
||||
- Use **Test Connection** to verify settings and select folder
|
||||
|
||||
### Folder Settings (for folder import)
|
||||
- **Watch Folder**: Local path for incoming invoices
|
||||
- **Archive Folder**: Local path for processed invoices
|
||||
- **IMAP Archive Folder**: Email folder for processed emails
|
||||
|
||||
### Import Settings
|
||||
- **Auto-create invoices**: Automatically create supplier invoices during batch import
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Import
|
||||
1. Go to **ZUGFeRD Import > Import Invoice**
|
||||
2. Upload a ZUGFeRD/Factur-X PDF file
|
||||
3. Review invoice data and line items
|
||||
4. Assign missing products if needed
|
||||
5. Select supplier (if not auto-detected)
|
||||
6. Click **Create Supplier Invoice**
|
||||
|
||||
### Batch Import
|
||||
1. Go to **ZUGFeRD Import > Batch Import**
|
||||
2. Select source (Folder or Email)
|
||||
3. Click **Start Import**
|
||||
4. Review results
|
||||
|
||||
### Product Mapping
|
||||
1. Go to **ZUGFeRD Import > Product Mapping**
|
||||
2. Select supplier
|
||||
3. Add mappings: Supplier article number → Your product
|
||||
|
||||
## Extrafields
|
||||
|
||||
The module adds a custom field to third parties:
|
||||
- **Customer No. at Supplier**: Your customer number at this supplier (used for automatic supplier detection via buyer reference)
|
||||
|
||||
## Translations
|
||||
|
||||
Available in:
|
||||
- German (de_DE)
|
||||
- English (en_US)
|
||||
|
||||
## Version History
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
|
||||
|
||||
### 5.5 (Current)
|
||||
- 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
|
||||
|
||||
## License
|
||||
|
||||
GPLv3 or (at your option) any later version. See file COPYING for more information.
|
||||
|
||||
## Author
|
||||
|
||||
Eduard Wisch - [data IT solution](https://data-it-solution.de)
|
||||
118
admin/about.php
Executable file
118
admin/about.php
Executable file
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
/* Copyright (C) 2004-2017 Laurent Destailleur <eldy@users.sourceforge.net>
|
||||
* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
* Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file importzugferd/admin/about.php
|
||||
* \ingroup importzugferd
|
||||
* \brief About page of module ImportZugferd.
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
// Try main.inc.php into web root known defined into CONTEXT_DOCUMENT_ROOT (not always defined)
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
// Try main.inc.php into web root detected using web root calculated from SCRIPT_FILENAME
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
// Try main.inc.php using relative path
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
// Libraries
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
|
||||
require_once '../lib/importzugferd.lib.php';
|
||||
|
||||
/**
|
||||
* @var Conf $conf
|
||||
* @var DoliDB $db
|
||||
* @var HookManager $hookmanager
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
// Translations
|
||||
$langs->loadLangs(array("errors", "admin", "importzugferd@importzugferd"));
|
||||
|
||||
// Access control
|
||||
if (!$user->admin) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$backtopage = GETPOST('backtopage', 'alpha');
|
||||
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// None
|
||||
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
$help_url = '';
|
||||
$title = "ImportZugferdSetup";
|
||||
|
||||
llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-importzugferd page-admin_about');
|
||||
|
||||
// Subheader
|
||||
$linkback = '<a href="'.($backtopage ? $backtopage : DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1').'">'.$langs->trans("BackToModuleList").'</a>';
|
||||
|
||||
print load_fiche_titre($langs->trans($title), $linkback, 'title_setup');
|
||||
|
||||
// Configuration header
|
||||
$head = importzugferdAdminPrepareHead();
|
||||
print dol_get_fiche_head($head, 'about', $langs->trans($title), 0, 'importzugferd@importzugferd');
|
||||
|
||||
dol_include_once('/importzugferd/core/modules/modImportZugferd.class.php');
|
||||
$tmpmodule = new modImportZugferd($db);
|
||||
print $tmpmodule->getDescLong();
|
||||
|
||||
// Page end
|
||||
print dol_get_fiche_end();
|
||||
llxFooter();
|
||||
$db->close();
|
||||
870
admin/setup.php
Executable file
870
admin/setup.php
Executable file
|
|
@ -0,0 +1,870 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file importzugferd/admin/setup.php
|
||||
* \ingroup importzugferd
|
||||
* \brief ImportZugferd setup page.
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
// Libraries
|
||||
require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php";
|
||||
require_once '../lib/importzugferd.lib.php';
|
||||
|
||||
// Translations
|
||||
$langs->loadLangs(array("admin", "importzugferd@importzugferd"));
|
||||
|
||||
// Parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$backtopage = GETPOST('backtopage', 'alpha');
|
||||
|
||||
// Access control
|
||||
if (!$user->admin) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Form setup using FormSetup class
|
||||
if (!class_exists('FormSetup')) {
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formsetup.class.php';
|
||||
}
|
||||
$formSetup = new FormSetup($db);
|
||||
|
||||
/*
|
||||
* Setup configuration items
|
||||
*/
|
||||
|
||||
// IMAP Settings Section
|
||||
$formSetup->newItem('IMAPSettings')->setAsTitle();
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_IMAP_HOST');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth300';
|
||||
$item->fieldAttr['placeholder'] = 'imap.example.com';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_IMAP_PORT');
|
||||
$item->defaultFieldValue = '993';
|
||||
$item->cssClass = 'width100';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_IMAP_USER');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth300';
|
||||
$item->fieldAttr['placeholder'] = 'invoices@example.com';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_IMAP_PASSWORD');
|
||||
$item->cssClass = 'minwidth300';
|
||||
$item->fieldAttr['type'] = 'password';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_IMAP_FOLDER');
|
||||
$item->defaultFieldValue = 'INBOX';
|
||||
$item->cssClass = 'minwidth200';
|
||||
|
||||
$formSetup->newItem('IMPORTZUGFERD_IMAP_SSL')->setAsYesNo();
|
||||
|
||||
// Import Settings Section
|
||||
$formSetup->newItem('ImportSettings')->setAsTitle();
|
||||
|
||||
$formSetup->newItem('IMPORTZUGFERD_AUTO_CREATE_INVOICE')->setAsYesNo();
|
||||
|
||||
// Email Notification Settings Section
|
||||
$formSetup->newItem('NotificationSettings')->setAsTitle();
|
||||
|
||||
$formSetup->newItem('IMPORTZUGFERD_NOTIFY_ENABLED')->setAsYesNo();
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_NOTIFY_EMAIL');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth300';
|
||||
$item->fieldAttr['placeholder'] = 'admin@example.com';
|
||||
|
||||
$formSetup->newItem('IMPORTZUGFERD_NOTIFY_MANUAL')->setAsYesNo();
|
||||
$formSetup->newItem('IMPORTZUGFERD_NOTIFY_ERROR')->setAsYesNo();
|
||||
$formSetup->newItem('IMPORTZUGFERD_NOTIFY_PRICE_DIFF')->setAsYesNo();
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_PRICE_DIFF_THRESHOLD');
|
||||
$item->defaultFieldValue = '10';
|
||||
$item->cssClass = 'width75';
|
||||
$item->fieldAttr['type'] = 'number';
|
||||
$item->fieldAttr['min'] = '0';
|
||||
$item->fieldAttr['max'] = '100';
|
||||
$item->fieldAttr['step'] = '1';
|
||||
|
||||
// Scheduling Settings Section
|
||||
$formSetup->newItem('SchedulingSettings')->setAsTitle();
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_IMPORT_FREQUENCY');
|
||||
$item->setAsSelect(array(
|
||||
'manual' => $langs->trans('FrequencyManual'),
|
||||
'hourly' => $langs->trans('FrequencyHourly'),
|
||||
'daily' => $langs->trans('FrequencyDaily'),
|
||||
'weekly' => $langs->trans('FrequencyWeekly')
|
||||
));
|
||||
$item->defaultFieldValue = 'manual';
|
||||
|
||||
// Folder Import Settings Section
|
||||
$formSetup->newItem('FolderImportSettings')->setAsTitle();
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_WATCH_FOLDER');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth400';
|
||||
$item->fieldAttr['placeholder'] = '/path/to/invoices';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_ARCHIVE_FOLDER');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth400';
|
||||
$item->fieldAttr['placeholder'] = '/path/to/archive';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_ERROR_FOLDER');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth400';
|
||||
$item->fieldAttr['placeholder'] = '/path/to/errors';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_IMAP_ARCHIVE_FOLDER');
|
||||
$item->defaultFieldValue = 'Archive';
|
||||
$item->cssClass = 'minwidth200';
|
||||
|
||||
// Datanorm Settings Section
|
||||
$formSetup->newItem('DatanormSettings')->setAsTitle();
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_DATANORM_MARKUP');
|
||||
$item->defaultFieldValue = '30';
|
||||
$item->cssClass = 'width100';
|
||||
$item->fieldAttr['placeholder'] = '30';
|
||||
|
||||
$formSetup->newItem('IMPORTZUGFERD_DATANORM_SEARCH_ALL')->setAsYesNo();
|
||||
|
||||
// Accounting Codes Section (Standard-Konten für neue Produkte)
|
||||
$formSetup->newItem('AccountingSettings')->setAsTitle();
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_SELL');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth200';
|
||||
$item->fieldAttr['placeholder'] = '700000';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth200';
|
||||
$item->fieldAttr['placeholder'] = '700100';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth200';
|
||||
$item->fieldAttr['placeholder'] = '700200';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_BUY');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth200';
|
||||
$item->fieldAttr['placeholder'] = '400000';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth200';
|
||||
$item->fieldAttr['placeholder'] = '400100';
|
||||
|
||||
$item = $formSetup->newItem('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT');
|
||||
$item->defaultFieldValue = '';
|
||||
$item->cssClass = 'minwidth200';
|
||||
$item->fieldAttr['placeholder'] = '400200';
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
if (versioncompare(explode('.', DOL_VERSION), array(15)) < 0 && $action == 'update' && !empty($user->admin)) {
|
||||
$formSetup->saveConfFromPost();
|
||||
}
|
||||
|
||||
include DOL_DOCUMENT_ROOT.'/core/actions_setmoduleoptions.inc.php';
|
||||
|
||||
// AJAX action for folder browsing
|
||||
if ($action == 'browse_folders') {
|
||||
$path = GETPOST('path', 'alpha');
|
||||
$target = GETPOST('target', 'alpha');
|
||||
|
||||
// Sanitize path - default to /home for easier navigation
|
||||
if (empty($path)) {
|
||||
$path = '/home';
|
||||
}
|
||||
$path = realpath($path);
|
||||
if ($path === false) {
|
||||
$path = '/home';
|
||||
if (!is_dir($path)) {
|
||||
$path = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// Get directories
|
||||
$dirs = array();
|
||||
if (is_dir($path) && is_readable($path)) {
|
||||
$entries = @scandir($path);
|
||||
if ($entries) {
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry == '.') continue;
|
||||
$fullPath = $path . '/' . $entry;
|
||||
if (is_dir($fullPath) && is_readable($fullPath)) {
|
||||
$dirs[] = array(
|
||||
'name' => $entry,
|
||||
'path' => $fullPath
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return JSON
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array(
|
||||
'current' => $path,
|
||||
'parent' => dirname($path),
|
||||
'dirs' => $dirs,
|
||||
'target' => $target
|
||||
));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Save folder from browser
|
||||
if ($action == 'set_folder') {
|
||||
$target = GETPOST('target', 'alpha');
|
||||
$folder_path = GETPOST('folder_path', 'alpha');
|
||||
|
||||
if (in_array($target, array('IMPORTZUGFERD_WATCH_FOLDER', 'IMPORTZUGFERD_ARCHIVE_FOLDER'))) {
|
||||
if (is_dir($folder_path)) {
|
||||
dolibarr_set_const($db, $target, $folder_path, 'chaine', 0, '', $conf->entity);
|
||||
setEventMessages($langs->trans('FolderSelected').': '.$folder_path, null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans('ErrorFolderNotFound'), null, 'errors');
|
||||
}
|
||||
}
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
$title = "ImportZugferdSetup";
|
||||
llxHeader('', $langs->trans($title), '', '', 0, 0, '', '', '', 'mod-importzugferd page-admin');
|
||||
|
||||
// Subheader
|
||||
$linkback = '<a href="'.($backtopage ? $backtopage : DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1').'">'.$langs->trans("BackToModuleList").'</a>';
|
||||
|
||||
print load_fiche_titre($langs->trans($title), $linkback, 'title_setup');
|
||||
|
||||
// Configuration header
|
||||
$head = importzugferdAdminPrepareHead();
|
||||
print dol_get_fiche_head($head, 'settings', $langs->trans($title), -1, "importzugferd@importzugferd");
|
||||
|
||||
// Setup page description
|
||||
print '<span class="opacitymedium">'.$langs->trans("ImportZugferdSetupPage").'</span><br><br>';
|
||||
|
||||
// Display the form
|
||||
print $formSetup->generateOutput(true);
|
||||
|
||||
// Build folder validation data for JavaScript
|
||||
$folderValidation = array();
|
||||
|
||||
// Watch folder - only needs to be readable
|
||||
$watchFolder = getDolGlobalString('IMPORTZUGFERD_WATCH_FOLDER');
|
||||
if (!empty($watchFolder)) {
|
||||
$watchExists = is_dir($watchFolder);
|
||||
$watchReadable = $watchExists && is_readable($watchFolder);
|
||||
if (!$watchExists) {
|
||||
$folderValidation['IMPORTZUGFERD_WATCH_FOLDER'] = array('ok' => false, 'msg' => $langs->trans('FolderNotFound'));
|
||||
} elseif (!$watchReadable) {
|
||||
$folderValidation['IMPORTZUGFERD_WATCH_FOLDER'] = array('ok' => false, 'msg' => $langs->trans('FolderNotReadable'));
|
||||
} else {
|
||||
$files = glob($watchFolder.'/*.pdf');
|
||||
$files = array_merge($files ?: [], glob($watchFolder.'/*.PDF') ?: []);
|
||||
$folderValidation['IMPORTZUGFERD_WATCH_FOLDER'] = array('ok' => true, 'msg' => $langs->trans('FolderOK').' ('.count($files).' PDF)');
|
||||
}
|
||||
}
|
||||
|
||||
// Archive folder - needs to be writable
|
||||
$archiveFolder = getDolGlobalString('IMPORTZUGFERD_ARCHIVE_FOLDER');
|
||||
if (!empty($archiveFolder)) {
|
||||
$archiveExists = is_dir($archiveFolder);
|
||||
$archiveWritable = $archiveExists && is_writable($archiveFolder);
|
||||
if (!$archiveExists) {
|
||||
$folderValidation['IMPORTZUGFERD_ARCHIVE_FOLDER'] = array('ok' => false, 'msg' => $langs->trans('FolderNotFound'));
|
||||
} elseif (!$archiveWritable) {
|
||||
$folderValidation['IMPORTZUGFERD_ARCHIVE_FOLDER'] = array('ok' => false, 'msg' => $langs->trans('FolderNotWritable'));
|
||||
} else {
|
||||
$folderValidation['IMPORTZUGFERD_ARCHIVE_FOLDER'] = array('ok' => true, 'msg' => $langs->trans('FolderOK'));
|
||||
}
|
||||
}
|
||||
|
||||
// Error folder - needs to be writable
|
||||
$errorFolder = getDolGlobalString('IMPORTZUGFERD_ERROR_FOLDER');
|
||||
if (!empty($errorFolder)) {
|
||||
$errorExists = is_dir($errorFolder);
|
||||
$errorWritable = $errorExists && is_writable($errorFolder);
|
||||
if (!$errorExists) {
|
||||
$folderValidation['IMPORTZUGFERD_ERROR_FOLDER'] = array('ok' => false, 'msg' => $langs->trans('FolderNotFound'));
|
||||
} elseif (!$errorWritable) {
|
||||
$folderValidation['IMPORTZUGFERD_ERROR_FOLDER'] = array('ok' => false, 'msg' => $langs->trans('FolderNotWritable'));
|
||||
} else {
|
||||
$folderValidation['IMPORTZUGFERD_ERROR_FOLDER'] = array('ok' => true, 'msg' => $langs->trans('FolderOK'));
|
||||
}
|
||||
}
|
||||
|
||||
// Folder Browser Modal
|
||||
print '
|
||||
<div id="folderBrowserModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:9999;">
|
||||
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); background:white; padding:20px; border-radius:8px; min-width:500px; max-width:80%; max-height:80%; overflow:auto; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
|
||||
<h3 style="margin-top:0;"><i class="fas fa-folder-open paddingright"></i>'.$langs->trans('SelectFolder').'</h3>
|
||||
<div style="margin-bottom:10px;">
|
||||
<input type="text" id="pathInput" style="width:80%; font-family:monospace;" placeholder="/path/to/folder">
|
||||
<a class="button buttongen smallpaddingimp" href="#" onclick="goToPath(); return false;" title="'.$langs->trans('Go').'"><i class="fas fa-arrow-right"></i></a>
|
||||
</div>
|
||||
<div style="margin-bottom:5px;">
|
||||
<span class="opacitymedium">'.$langs->trans('QuickLinks').':</span>
|
||||
<a href="#" onclick="loadFolderContents(\'/home\'); return false;" class="paddingleft">/home</a>
|
||||
<a href="#" onclick="loadFolderContents(\'/srv\'); return false;" class="paddingleft">/srv</a>
|
||||
<a href="#" onclick="loadFolderContents(\'/var\'); return false;" class="paddingleft">/var</a>
|
||||
<a href="#" onclick="loadFolderContents(\'/tmp\'); return false;" class="paddingleft">/tmp</a>
|
||||
</div>
|
||||
<div style="border:1px solid #ccc; padding:10px; max-height:300px; overflow-y:auto; background:#f9f9f9;" id="folderList">
|
||||
</div>
|
||||
<div style="margin-top:15px; text-align:right;">
|
||||
<input type="hidden" id="folderTarget" value="">
|
||||
<a class="button" href="#" onclick="selectCurrentFolder(); return false;"><i class="fas fa-check paddingright"></i>'.$langs->trans('SelectThisFolder').'</a>
|
||||
<a class="button" href="#" onclick="closeFolderBrowser(); return false;">'.$langs->trans('Cancel').'</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Folder validation data from PHP
|
||||
var folderValidation = '.json_encode($folderValidation).';
|
||||
|
||||
// Add browse buttons and validation icons next to folder input fields
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var folderFields = ["IMPORTZUGFERD_WATCH_FOLDER", "IMPORTZUGFERD_ARCHIVE_FOLDER", "IMPORTZUGFERD_ERROR_FOLDER"];
|
||||
folderFields.forEach(function(fieldName) {
|
||||
var input = document.querySelector("input[name=\"" + fieldName + "\"]");
|
||||
if (input) {
|
||||
// Add browse button
|
||||
var btn = document.createElement("a");
|
||||
btn.href = "#";
|
||||
btn.className = "button buttongen smallpaddingimp";
|
||||
btn.style.marginLeft = "5px";
|
||||
btn.innerHTML = "<i class=\"fas fa-folder-open\"></i>";
|
||||
btn.title = "'.$langs->trans('Browse').'";
|
||||
btn.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
var startPath = input.value || "/home";
|
||||
openFolderBrowser(fieldName, startPath);
|
||||
};
|
||||
input.parentNode.insertBefore(btn, input.nextSibling);
|
||||
|
||||
// Add validation icon if folder is configured
|
||||
if (folderValidation[fieldName]) {
|
||||
var validIcon = document.createElement("span");
|
||||
validIcon.style.marginLeft = "10px";
|
||||
validIcon.id = "validation_" + fieldName;
|
||||
if (folderValidation[fieldName].ok) {
|
||||
validIcon.innerHTML = "<i class=\"fas fa-check-circle\" style=\"color:green;\"></i> <span class=\"opacitymedium\">" + folderValidation[fieldName].msg + "</span>";
|
||||
} else {
|
||||
validIcon.innerHTML = "<i class=\"fas fa-times-circle\" style=\"color:red;\"></i> <span style=\"color:red;\">" + folderValidation[fieldName].msg + "</span>";
|
||||
}
|
||||
btn.parentNode.insertBefore(validIcon, btn.nextSibling);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function openFolderBrowser(target, startPath) {
|
||||
document.getElementById("folderTarget").value = target;
|
||||
document.getElementById("folderBrowserModal").style.display = "block";
|
||||
document.getElementById("pathInput").value = startPath || "/home";
|
||||
loadFolderContents(startPath || "/home");
|
||||
}
|
||||
|
||||
function goToPath() {
|
||||
var path = document.getElementById("pathInput").value;
|
||||
if (path) {
|
||||
loadFolderContents(path);
|
||||
}
|
||||
}
|
||||
|
||||
function closeFolderBrowser() {
|
||||
document.getElementById("folderBrowserModal").style.display = "none";
|
||||
}
|
||||
|
||||
function loadFolderContents(path) {
|
||||
var target = document.getElementById("folderTarget").value;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "'.$_SERVER['PHP_SELF'].'?action=browse_folders&path=" + encodeURIComponent(path) + "&target=" + target + "&token='.newToken().'", true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
var data = JSON.parse(xhr.responseText);
|
||||
document.getElementById("pathInput").value = data.current;
|
||||
|
||||
var html = "";
|
||||
if (data.current !== "/" && data.parent) {
|
||||
html += "<div style=\"padding:5px; cursor:pointer; border-bottom:1px solid #eee;\" onclick=\"loadFolderContents(\'" + data.parent.replace(/\'/g, "\\\'") + "\')\">";
|
||||
html += "<i class=\"fas fa-level-up-alt paddingright\"></i><strong>..</strong> ('.$langs->trans('ParentFolder').')";
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.dirs.length; i++) {
|
||||
var dir = data.dirs[i];
|
||||
html += "<div style=\"padding:5px; cursor:pointer; border-bottom:1px solid #eee;\" onclick=\"loadFolderContents(\'" + dir.path.replace(/\'/g, "\\\'") + "\')\" onmouseover=\"this.style.background=\'#e8f4fc\'\" onmouseout=\"this.style.background=\'transparent\'\">";
|
||||
html += "<i class=\"fas fa-folder paddingright\" style=\"color:#f0ad4e;\"></i>" + dir.name;
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
if (data.dirs.length === 0 && data.current !== "/") {
|
||||
html += "<div style=\"padding:10px; color:#666; text-align:center;\">'.$langs->trans('NoSubfolders').'</div>";
|
||||
}
|
||||
|
||||
document.getElementById("folderList").innerHTML = html;
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function selectCurrentFolder() {
|
||||
var path = document.getElementById("pathInput").value;
|
||||
var target = document.getElementById("folderTarget").value;
|
||||
// Update the input field directly
|
||||
var input = document.querySelector("input[name=\"" + target + "\"]");
|
||||
if (input) {
|
||||
input.value = path;
|
||||
}
|
||||
closeFolderBrowser();
|
||||
}
|
||||
|
||||
// Close modal on escape key
|
||||
document.addEventListener("keydown", function(e) {
|
||||
if (e.key === "Escape") closeFolderBrowser();
|
||||
});
|
||||
|
||||
// Close modal on background click
|
||||
document.getElementById("folderBrowserModal").addEventListener("click", function(e) {
|
||||
if (e.target === this) closeFolderBrowser();
|
||||
});
|
||||
</script>
|
||||
';
|
||||
|
||||
// Email Notification Test Section
|
||||
if (getDolGlobalString('IMPORTZUGFERD_NOTIFY_ENABLED') && getDolGlobalString('IMPORTZUGFERD_NOTIFY_EMAIL')) {
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans('TestEmailNotification').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Handle test email action
|
||||
if ($action == 'test_email') {
|
||||
dol_include_once('/importzugferd/class/importnotification.class.php');
|
||||
$notification = new ImportNotification($db);
|
||||
$result = $notification->sendTestNotification();
|
||||
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('TestEmailSent', getDolGlobalString('IMPORTZUGFERD_NOTIFY_EMAIL')), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans('TestEmailFailed').': '.$notification->error, null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans('SendTestEmail').'</td>';
|
||||
print '<td>';
|
||||
print '<a class="button" href="'.$_SERVER['PHP_SELF'].'?action=test_email&token='.newToken().'">';
|
||||
print '<i class="fas fa-paper-plane paddingright"></i>'.$langs->trans('SendTestEmail');
|
||||
print '</a>';
|
||||
print ' <span class="opacitymedium">'.$langs->trans('SendTo').': '.getDolGlobalString('IMPORTZUGFERD_NOTIFY_EMAIL').'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Test IMAP connection button and folder selection
|
||||
if (getDolGlobalString('IMPORTZUGFERD_IMAP_HOST')) {
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="3">'.$langs->trans('TestConnection').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Check if IMAP extension is available
|
||||
$imap_available = function_exists('imap_open');
|
||||
|
||||
// Test connection action
|
||||
$imap_folders = array();
|
||||
$connection_ok = false;
|
||||
|
||||
if (!$imap_available) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="3">';
|
||||
print '<span class="error"><i class="fas fa-exclamation-triangle paddingright"></i>';
|
||||
print $langs->trans('IMAPExtensionNotInstalled');
|
||||
print '</span><br>';
|
||||
print '<span class="opacitymedium">'.$langs->trans('IMAPExtensionHelp').'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
} elseif ($action == 'test_imap' || $action == 'select_folder') {
|
||||
$host = getDolGlobalString('IMPORTZUGFERD_IMAP_HOST');
|
||||
$port = getDolGlobalString('IMPORTZUGFERD_IMAP_PORT', '993');
|
||||
$imap_user = getDolGlobalString('IMPORTZUGFERD_IMAP_USER');
|
||||
$password = getDolGlobalString('IMPORTZUGFERD_IMAP_PASSWORD');
|
||||
$ssl = getDolGlobalString('IMPORTZUGFERD_IMAP_SSL');
|
||||
|
||||
$mailbox_base = '{' . $host . ':' . $port . '/imap' . ($ssl ? '/ssl' : '') . '/novalidate-cert}';
|
||||
$mailbox = $mailbox_base . 'INBOX';
|
||||
|
||||
$connection = @imap_open($mailbox, $imap_user, $password);
|
||||
|
||||
if ($connection) {
|
||||
$connection_ok = true;
|
||||
setEventMessages($langs->trans('ConnectionSuccessful'), null, 'mesgs');
|
||||
|
||||
// Get list of folders
|
||||
$folders_raw = imap_list($connection, $mailbox_base, '*');
|
||||
if ($folders_raw) {
|
||||
foreach ($folders_raw as $folder) {
|
||||
// Remove the mailbox base from folder name
|
||||
$folder_name = str_replace($mailbox_base, '', $folder);
|
||||
// Decode folder name (IMAP uses modified UTF-7)
|
||||
$folder_name_decoded = imap_utf7_decode($folder_name);
|
||||
$imap_folders[$folder_name] = $folder_name_decoded;
|
||||
}
|
||||
}
|
||||
|
||||
imap_close($connection);
|
||||
} else {
|
||||
setEventMessages($langs->trans('ConnectionFailed') . ': ' . imap_last_error(), null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
// Save selected folder
|
||||
if ($action == 'select_folder' && GETPOST('imap_folder', 'alpha')) {
|
||||
$selected_folder = GETPOST('imap_folder', 'alpha');
|
||||
dolibarr_set_const($db, 'IMPORTZUGFERD_IMAP_FOLDER', $selected_folder, 'chaine', 0, '', $conf->entity);
|
||||
setEventMessages($langs->trans('FolderSelected').': '.$selected_folder, null, 'mesgs');
|
||||
}
|
||||
|
||||
// Only show status and folder selection if IMAP is available
|
||||
if ($imap_available) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans('Status').'</td>';
|
||||
print '<td colspan="2">';
|
||||
if ($action == 'test_imap' || $action == 'select_folder') {
|
||||
if ($connection_ok) {
|
||||
print '<span class="ok"><i class="fas fa-check paddingright"></i>'.$langs->trans('ConnectionSuccessful').'</span>';
|
||||
} else {
|
||||
print '<span class="error"><i class="fas fa-times paddingright"></i>'.$langs->trans('ConnectionFailed').'</span>';
|
||||
}
|
||||
} else {
|
||||
print '<span class="opacitymedium">'.$langs->trans('ClickTestToCheck').'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Show folder selection if connection was successful
|
||||
if ($imap_available && $connection_ok && !empty($imap_folders)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans('SelectFolder').'</td>';
|
||||
print '<td>';
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" class="inline-block">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="select_folder">';
|
||||
|
||||
$current_folder = getDolGlobalString('IMPORTZUGFERD_IMAP_FOLDER', 'INBOX');
|
||||
print '<select name="imap_folder" class="flat minwidth200">';
|
||||
foreach ($imap_folders as $folder_raw => $folder_decoded) {
|
||||
$selected = ($folder_raw == $current_folder) ? ' selected' : '';
|
||||
print '<option value="'.dol_escape_htmltag($folder_raw).'"'.$selected.'>';
|
||||
print dol_escape_htmltag($folder_decoded);
|
||||
print '</option>';
|
||||
}
|
||||
print '</select>';
|
||||
print ' <input type="submit" class="button" value="'.$langs->trans('Save').'">';
|
||||
print '</form>';
|
||||
print '</td>';
|
||||
print '<td>';
|
||||
print '<span class="opacitymedium">'.$langs->trans('FoundFolders').': '.count($imap_folders).'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Only show test button if IMAP extension is available
|
||||
if ($imap_available) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="3" class="center">';
|
||||
print '<a class="button" href="'.$_SERVER['PHP_SELF'].'?action=test_imap&token='.newToken().'">';
|
||||
print '<i class="fas fa-plug paddingright"></i>'.$langs->trans('TestConnection');
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Manual Import Trigger Section
|
||||
$hasFolder = getDolGlobalString('IMPORTZUGFERD_WATCH_FOLDER') && is_dir(getDolGlobalString('IMPORTZUGFERD_WATCH_FOLDER'));
|
||||
$hasImap = getDolGlobalString('IMPORTZUGFERD_IMAP_HOST') && getDolGlobalString('IMPORTZUGFERD_IMAP_USER');
|
||||
|
||||
if ($hasFolder || $hasImap) {
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans('ManualImportTrigger').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Handle manual import action
|
||||
if ($action == 'run_import') {
|
||||
$source = GETPOST('import_source', 'alpha');
|
||||
dol_include_once('/importzugferd/class/zugferdimport.class.php');
|
||||
dol_include_once('/importzugferd/class/zugferdparser.class.php');
|
||||
|
||||
$successCount = 0;
|
||||
$errorCount = 0;
|
||||
$skippedCount = 0;
|
||||
|
||||
if ($source == 'folder' && $hasFolder) {
|
||||
$watchFolder = getDolGlobalString('IMPORTZUGFERD_WATCH_FOLDER');
|
||||
$archiveFolder = getDolGlobalString('IMPORTZUGFERD_ARCHIVE_FOLDER');
|
||||
$errorFolder = getDolGlobalString('IMPORTZUGFERD_ERROR_FOLDER');
|
||||
$autoCreate = getDolGlobalInt('IMPORTZUGFERD_AUTO_CREATE_INVOICE');
|
||||
|
||||
$files = glob($watchFolder.'/*.pdf');
|
||||
$files = array_merge($files, glob($watchFolder.'/*.PDF'));
|
||||
|
||||
// Create archive folder if configured but doesn't exist
|
||||
if (!empty($archiveFolder) && !is_dir($archiveFolder)) {
|
||||
dol_mkdir($archiveFolder);
|
||||
}
|
||||
// Create error folder if configured but doesn't exist
|
||||
if (!empty($errorFolder) && !is_dir($errorFolder)) {
|
||||
dol_mkdir($errorFolder);
|
||||
}
|
||||
|
||||
// Helper function for moving files with fallback
|
||||
$moveFile = function($file, $targetFolder, $prefix) {
|
||||
if (empty($targetFolder)) {
|
||||
return false;
|
||||
}
|
||||
if (!is_dir($targetFolder)) {
|
||||
dol_mkdir($targetFolder);
|
||||
}
|
||||
if (!is_dir($targetFolder) || !is_writable($targetFolder)) {
|
||||
dol_syslog("ImportZugferd: Target folder not accessible: ".$targetFolder, LOG_WARNING);
|
||||
return false;
|
||||
}
|
||||
$destFile = $targetFolder.'/'.$prefix.date('Y-m-d_His').'_'.basename($file);
|
||||
if (@rename($file, $destFile)) {
|
||||
dol_syslog("ImportZugferd: Moved file to: ".$destFile, LOG_INFO);
|
||||
return true;
|
||||
}
|
||||
// Fallback: copy + delete (for cross-filesystem moves)
|
||||
if (@copy($file, $destFile)) {
|
||||
@unlink($file);
|
||||
dol_syslog("ImportZugferd: Copied file to: ".$destFile, LOG_INFO);
|
||||
return true;
|
||||
}
|
||||
dol_syslog("ImportZugferd: Failed to move file: ".$file." to ".$destFile, LOG_ERR);
|
||||
return false;
|
||||
};
|
||||
|
||||
foreach ($files as $file) {
|
||||
$import = new ZugferdImport($db);
|
||||
$result = $import->importFromFile($user, $file, $autoCreate);
|
||||
|
||||
if ($result > 0) {
|
||||
$successCount++;
|
||||
$moveFile($file, $archiveFolder, 'imported_');
|
||||
} elseif ($result == -2) {
|
||||
// Duplicate - move to archive
|
||||
$skippedCount++;
|
||||
if (!$moveFile($file, $archiveFolder, 'duplicate_')) {
|
||||
@unlink($file);
|
||||
}
|
||||
} else {
|
||||
// Error - move to error folder, fallback to archive
|
||||
$errorCount++;
|
||||
if (!$moveFile($file, $errorFolder, 'error_')) {
|
||||
if (!$moveFile($file, $archiveFolder, 'error_')) {
|
||||
dol_syslog("ImportZugferd: File stays in watch folder: ".$file, LOG_WARNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($source == 'imap' && $hasImap && function_exists('imap_open')) {
|
||||
$host = getDolGlobalString('IMPORTZUGFERD_IMAP_HOST');
|
||||
$port = getDolGlobalString('IMPORTZUGFERD_IMAP_PORT', '993');
|
||||
$imap_user = getDolGlobalString('IMPORTZUGFERD_IMAP_USER');
|
||||
$password = getDolGlobalString('IMPORTZUGFERD_IMAP_PASSWORD');
|
||||
$ssl = getDolGlobalString('IMPORTZUGFERD_IMAP_SSL');
|
||||
$folder = getDolGlobalString('IMPORTZUGFERD_IMAP_FOLDER', 'INBOX');
|
||||
$archiveFolder = getDolGlobalString('IMPORTZUGFERD_IMAP_ARCHIVE_FOLDER');
|
||||
$autoCreate = getDolGlobalInt('IMPORTZUGFERD_AUTO_CREATE_INVOICE');
|
||||
|
||||
$mailbox = '{' . $host . ':' . $port . '/imap' . ($ssl ? '/ssl' : '') . '/novalidate-cert}' . $folder;
|
||||
$connection = @imap_open($mailbox, $imap_user, $password);
|
||||
|
||||
if ($connection) {
|
||||
$emails = imap_search($connection, 'UNSEEN');
|
||||
if ($emails) {
|
||||
foreach ($emails as $email_number) {
|
||||
$structure = imap_fetchstructure($connection, $email_number);
|
||||
|
||||
// Find PDF attachments
|
||||
if (isset($structure->parts)) {
|
||||
foreach ($structure->parts as $partIndex => $part) {
|
||||
$filename = '';
|
||||
if ($part->ifdparameters) {
|
||||
foreach ($part->dparameters as $param) {
|
||||
if (strtolower($param->attribute) == 'filename') {
|
||||
$filename = $param->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($filename) && $part->ifparameters) {
|
||||
foreach ($part->parameters as $param) {
|
||||
if (strtolower($param->attribute) == 'name') {
|
||||
$filename = $param->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($filename) && preg_match('/\.pdf$/i', $filename)) {
|
||||
$attachment = imap_fetchbody($connection, $email_number, $partIndex + 1);
|
||||
if ($part->encoding == 3) { // BASE64
|
||||
$attachment = base64_decode($attachment);
|
||||
} elseif ($part->encoding == 4) { // QUOTED-PRINTABLE
|
||||
$attachment = quoted_printable_decode($attachment);
|
||||
}
|
||||
|
||||
// Save to temp file
|
||||
$tempFile = $conf->importzugferd->dir_temp.'/'.uniqid().'_'.$filename;
|
||||
if (!is_dir($conf->importzugferd->dir_temp)) {
|
||||
dol_mkdir($conf->importzugferd->dir_temp);
|
||||
}
|
||||
file_put_contents($tempFile, $attachment);
|
||||
|
||||
// Import
|
||||
$import = new ZugferdImport($db);
|
||||
$result = $import->importFromFile($user, $tempFile, $autoCreate);
|
||||
|
||||
if ($result > 0) {
|
||||
$successCount++;
|
||||
} elseif ($result == -2) {
|
||||
$skippedCount++;
|
||||
} else {
|
||||
$errorCount++;
|
||||
}
|
||||
|
||||
unlink($tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Archive processed emails
|
||||
if (!empty($archiveFolder) && $successCount > 0) {
|
||||
foreach ($emails as $email_number) {
|
||||
imap_mail_move($connection, $email_number, $archiveFolder);
|
||||
}
|
||||
imap_expunge($connection);
|
||||
}
|
||||
}
|
||||
imap_close($connection);
|
||||
} else {
|
||||
setEventMessages($langs->trans('ConnectionFailed').': '.imap_last_error(), null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
if ($successCount > 0 || $errorCount > 0 || $skippedCount > 0) {
|
||||
setEventMessages($langs->trans('BatchImportComplete', $successCount, $errorCount, $skippedCount), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans('NoFilesFound'), null, 'warnings');
|
||||
}
|
||||
}
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans('ImportFromFolder').'</td>';
|
||||
print '<td>';
|
||||
if ($hasFolder) {
|
||||
print '<a class="button" href="'.$_SERVER['PHP_SELF'].'?action=run_import&import_source=folder&token='.newToken().'">';
|
||||
print '<i class="fas fa-folder-open paddingright"></i>'.$langs->trans('StartImport');
|
||||
print '</a>';
|
||||
print ' <span class="opacitymedium">'.getDolGlobalString('IMPORTZUGFERD_WATCH_FOLDER').'</span>';
|
||||
} else {
|
||||
print '<span class="opacitymedium">'.$langs->trans('ErrorWatchFolderNotConfigured').'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans('ImportFromIMAP').'</td>';
|
||||
print '<td>';
|
||||
if ($hasImap && function_exists('imap_open')) {
|
||||
print '<a class="button" href="'.$_SERVER['PHP_SELF'].'?action=run_import&import_source=imap&token='.newToken().'">';
|
||||
print '<i class="fas fa-envelope paddingright"></i>'.$langs->trans('StartImport');
|
||||
print '</a>';
|
||||
print ' <span class="opacitymedium">'.getDolGlobalString('IMPORTZUGFERD_IMAP_USER').'</span>';
|
||||
} elseif (!function_exists('imap_open')) {
|
||||
print '<span class="opacitymedium">'.$langs->trans('IMAPExtensionNotInstalled').'</span>';
|
||||
} else {
|
||||
print '<span class="opacitymedium">'.$langs->trans('ErrorIMAPNotConfigured').'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
print '<br>';
|
||||
|
||||
// Page end
|
||||
print dol_get_fiche_end();
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
444
batch.php
Executable file
444
batch.php
Executable file
|
|
@ -0,0 +1,444 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file batch.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Batch import from folder or IMAP
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../main.inc.php")) {
|
||||
$res = @include "../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
||||
|
||||
dol_include_once('/importzugferd/class/actions_importzugferd.class.php');
|
||||
dol_include_once('/importzugferd/lib/importzugferd.lib.php');
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array("importzugferd@importzugferd", "bills", "products"));
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('importzugferd', 'import', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$source = GETPOST('source', 'alpha');
|
||||
|
||||
// Initialize objects
|
||||
$actions = new ActionsImportZugferd($db);
|
||||
|
||||
$import_results = array();
|
||||
$error = 0;
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// Process batch import
|
||||
if ($action == 'process') {
|
||||
$auto_create = getDolGlobalString('IMPORTZUGFERD_AUTO_CREATE_INVOICE');
|
||||
|
||||
if ($source == 'folder') {
|
||||
// Import from local folder
|
||||
$watch_folder = getDolGlobalString('IMPORTZUGFERD_WATCH_FOLDER');
|
||||
$archive_folder = getDolGlobalString('IMPORTZUGFERD_ARCHIVE_FOLDER');
|
||||
$error_folder = getDolGlobalString('IMPORTZUGFERD_ERROR_FOLDER');
|
||||
|
||||
if (empty($watch_folder) || !is_dir($watch_folder)) {
|
||||
setEventMessages($langs->trans('ErrorWatchFolderNotConfigured'), null, 'errors');
|
||||
$error++;
|
||||
} else {
|
||||
// Create archive folder if needed
|
||||
if (!empty($archive_folder) && !is_dir($archive_folder)) {
|
||||
dol_mkdir($archive_folder);
|
||||
}
|
||||
// Create error folder if needed
|
||||
if (!empty($error_folder) && !is_dir($error_folder)) {
|
||||
dol_mkdir($error_folder);
|
||||
}
|
||||
|
||||
// Get PDF files from watch folder
|
||||
$files = glob($watch_folder . '/*.pdf');
|
||||
if (empty($files)) {
|
||||
$files = glob($watch_folder . '/*.PDF');
|
||||
}
|
||||
|
||||
if (!empty($files)) {
|
||||
foreach ($files as $pdf_path) {
|
||||
$result = array(
|
||||
'file' => basename($pdf_path),
|
||||
'status' => 'error',
|
||||
'message' => '',
|
||||
'invoice_id' => 0,
|
||||
);
|
||||
|
||||
$res = $actions->processPdf($pdf_path, $user, $auto_create);
|
||||
|
||||
if ($res > 0) {
|
||||
$result['status'] = 'success';
|
||||
$result['message'] = $langs->trans('ImportSuccessful');
|
||||
$import_data = $actions->getResult();
|
||||
$result['invoice_id'] = $import_data['invoice_id'];
|
||||
|
||||
// Move to archive
|
||||
if (!empty($archive_folder) && is_dir($archive_folder)) {
|
||||
$archive_path = $archive_folder . '/success_' . date('Y-m-d_His') . '_' . basename($pdf_path);
|
||||
if (@rename($pdf_path, $archive_path) || (@copy($pdf_path, $archive_path) && @unlink($pdf_path))) {
|
||||
$result['archived'] = true;
|
||||
}
|
||||
}
|
||||
} elseif ($res == -3) {
|
||||
// Duplicate - move to archive (already imported)
|
||||
$result['status'] = 'skipped';
|
||||
$result['message'] = $langs->trans('ErrorDuplicateInvoice');
|
||||
|
||||
if (!empty($archive_folder) && is_dir($archive_folder)) {
|
||||
$archive_path = $archive_folder . '/duplicate_' . date('Y-m-d_His') . '_' . basename($pdf_path);
|
||||
if (@rename($pdf_path, $archive_path) || (@copy($pdf_path, $archive_path) && @unlink($pdf_path))) {
|
||||
$result['archived'] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Error - move to error folder
|
||||
$result['message'] = $actions->error;
|
||||
|
||||
if (!empty($error_folder) && is_dir($error_folder)) {
|
||||
$error_path = $error_folder . '/error_' . date('Y-m-d_His') . '_' . basename($pdf_path);
|
||||
if (@rename($pdf_path, $error_path) || (@copy($pdf_path, $error_path) && @unlink($pdf_path))) {
|
||||
$result['moved_to_error'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$import_results[] = $result;
|
||||
}
|
||||
} else {
|
||||
setEventMessages($langs->trans('NoFilesFound'), null, 'warnings');
|
||||
}
|
||||
}
|
||||
} elseif ($source == 'imap') {
|
||||
// Import from IMAP
|
||||
if (!function_exists('imap_open')) {
|
||||
setEventMessages($langs->trans('IMAPExtensionNotInstalled'), null, 'errors');
|
||||
$error++;
|
||||
} else {
|
||||
$host = getDolGlobalString('IMPORTZUGFERD_IMAP_HOST');
|
||||
$port = getDolGlobalString('IMPORTZUGFERD_IMAP_PORT', '993');
|
||||
$imap_user = getDolGlobalString('IMPORTZUGFERD_IMAP_USER');
|
||||
$password = getDolGlobalString('IMPORTZUGFERD_IMAP_PASSWORD');
|
||||
$ssl = getDolGlobalString('IMPORTZUGFERD_IMAP_SSL');
|
||||
$folder = getDolGlobalString('IMPORTZUGFERD_IMAP_FOLDER', 'INBOX');
|
||||
$archive_folder = getDolGlobalString('IMPORTZUGFERD_IMAP_ARCHIVE_FOLDER', 'Archive');
|
||||
|
||||
if (empty($host) || empty($imap_user)) {
|
||||
setEventMessages($langs->trans('ErrorIMAPNotConfigured'), null, 'errors');
|
||||
$error++;
|
||||
} else {
|
||||
$mailbox_base = '{' . $host . ':' . $port . '/imap' . ($ssl ? '/ssl' : '') . '/novalidate-cert}';
|
||||
$mailbox = $mailbox_base . $folder;
|
||||
|
||||
$connection = @imap_open($mailbox, $imap_user, $password);
|
||||
|
||||
if ($connection) {
|
||||
// Search for emails with PDF attachments
|
||||
$emails = imap_search($connection, 'ALL');
|
||||
|
||||
if ($emails) {
|
||||
// Create temp directory for attachments
|
||||
$temp_dir = $conf->importzugferd->dir_output . '/temp';
|
||||
if (!is_dir($temp_dir)) {
|
||||
dol_mkdir($temp_dir);
|
||||
}
|
||||
|
||||
foreach ($emails as $email_num) {
|
||||
$structure = imap_fetchstructure($connection, $email_num);
|
||||
$attachments = array();
|
||||
|
||||
// Find PDF attachments
|
||||
if (isset($structure->parts)) {
|
||||
foreach ($structure->parts as $part_num => $part) {
|
||||
$filename = '';
|
||||
if ($part->ifdparameters) {
|
||||
foreach ($part->dparameters as $param) {
|
||||
if (strtolower($param->attribute) == 'filename') {
|
||||
$filename = $param->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($filename) && $part->ifparameters) {
|
||||
foreach ($part->parameters as $param) {
|
||||
if (strtolower($param->attribute) == 'name') {
|
||||
$filename = $param->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if PDF
|
||||
if (!empty($filename) && preg_match('/\.pdf$/i', $filename)) {
|
||||
$attachments[] = array(
|
||||
'filename' => $filename,
|
||||
'part_num' => $part_num + 1,
|
||||
'encoding' => $part->encoding,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process each PDF attachment
|
||||
$email_processed = false;
|
||||
foreach ($attachments as $attachment) {
|
||||
$data = imap_fetchbody($connection, $email_num, $attachment['part_num']);
|
||||
|
||||
// Decode attachment
|
||||
if ($attachment['encoding'] == 3) { // BASE64
|
||||
$data = base64_decode($data);
|
||||
} elseif ($attachment['encoding'] == 4) { // QUOTED-PRINTABLE
|
||||
$data = quoted_printable_decode($data);
|
||||
}
|
||||
|
||||
// Save to temp file
|
||||
$temp_file = $temp_dir . '/' . uniqid() . '_' . $attachment['filename'];
|
||||
file_put_contents($temp_file, $data);
|
||||
|
||||
$result = array(
|
||||
'file' => $attachment['filename'],
|
||||
'status' => 'error',
|
||||
'message' => '',
|
||||
'invoice_id' => 0,
|
||||
);
|
||||
|
||||
// Process the PDF
|
||||
$res = $actions->processPdf($temp_file, $user, $auto_create);
|
||||
|
||||
if ($res > 0) {
|
||||
$result['status'] = 'success';
|
||||
$result['message'] = $langs->trans('ImportSuccessful');
|
||||
$import_data = $actions->getResult();
|
||||
$result['invoice_id'] = $import_data['invoice_id'];
|
||||
$email_processed = true;
|
||||
} elseif ($res == -3) {
|
||||
$result['status'] = 'skipped';
|
||||
$result['message'] = $langs->trans('ErrorDuplicateInvoice');
|
||||
} else {
|
||||
$result['message'] = $actions->error;
|
||||
}
|
||||
|
||||
// Clean up temp file
|
||||
@unlink($temp_file);
|
||||
|
||||
$import_results[] = $result;
|
||||
}
|
||||
|
||||
// Move email to archive folder if successfully processed
|
||||
if ($email_processed && !empty($archive_folder)) {
|
||||
$archive_mailbox = $mailbox_base . $archive_folder;
|
||||
@imap_mail_move($connection, $email_num, $archive_folder);
|
||||
}
|
||||
}
|
||||
|
||||
// Expunge to apply moves
|
||||
imap_expunge($connection);
|
||||
} else {
|
||||
setEventMessages($langs->trans('NoEmailsFound'), null, 'warnings');
|
||||
}
|
||||
|
||||
imap_close($connection);
|
||||
} else {
|
||||
setEventMessages($langs->trans('ConnectionFailed') . ': ' . imap_last_error(), null, 'errors');
|
||||
$error++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($import_results)) {
|
||||
$success_count = 0;
|
||||
$error_count = 0;
|
||||
$skipped_count = 0;
|
||||
foreach ($import_results as $r) {
|
||||
if ($r['status'] == 'success') $success_count++;
|
||||
elseif ($r['status'] == 'skipped') $skipped_count++;
|
||||
else $error_count++;
|
||||
}
|
||||
setEventMessages($langs->trans('BatchImportComplete', $success_count, $error_count, $skipped_count), null, 'mesgs');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
$title = $langs->trans('BatchImport');
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-batch');
|
||||
|
||||
print load_fiche_titre($title, '', 'fa-file-import');
|
||||
|
||||
// Check configuration
|
||||
$watch_folder = getDolGlobalString('IMPORTZUGFERD_WATCH_FOLDER');
|
||||
$imap_host = getDolGlobalString('IMPORTZUGFERD_IMAP_HOST');
|
||||
|
||||
if (empty($watch_folder) && empty($imap_host)) {
|
||||
print '<div class="warning">'.$langs->trans('BatchImportNotConfigured').'</div>';
|
||||
print '<br><a href="'.dol_buildpath('/importzugferd/admin/setup.php', 1).'" class="button">'.$langs->trans('ConfigureModule').'</a>';
|
||||
} else {
|
||||
// Source selection
|
||||
print '<div class="fichecenter">';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>'.$langs->trans('SelectSource').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Folder option
|
||||
if (!empty($watch_folder)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>';
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="process">';
|
||||
print '<input type="hidden" name="source" value="folder">';
|
||||
|
||||
print '<div class="inline-block valignmiddle">';
|
||||
print '<i class="fas fa-folder fa-2x paddingright"></i>';
|
||||
print '</div>';
|
||||
print '<div class="inline-block valignmiddle">';
|
||||
print '<strong>'.$langs->trans('ImportFromFolder').'</strong><br>';
|
||||
print '<span class="opacitymedium">'.$watch_folder.'</span>';
|
||||
|
||||
// Count files
|
||||
$files = glob($watch_folder . '/*.pdf');
|
||||
if (empty($files)) $files = glob($watch_folder . '/*.PDF');
|
||||
$file_count = !empty($files) ? count($files) : 0;
|
||||
print '<br><span class="badge badge-info">'.$file_count.' '.$langs->trans('Files').'</span>';
|
||||
|
||||
print '</div>';
|
||||
print '<div class="inline-block valignmiddle floatright">';
|
||||
print '<input type="submit" class="button button-primary" value="'.$langs->trans('StartImport').'"'.($file_count == 0 ? ' disabled' : '').'>';
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// IMAP option
|
||||
if (!empty($imap_host)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>';
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="process">';
|
||||
print '<input type="hidden" name="source" value="imap">';
|
||||
|
||||
print '<div class="inline-block valignmiddle">';
|
||||
print '<i class="fas fa-envelope fa-2x paddingright"></i>';
|
||||
print '</div>';
|
||||
print '<div class="inline-block valignmiddle">';
|
||||
print '<strong>'.$langs->trans('ImportFromIMAP').'</strong><br>';
|
||||
print '<span class="opacitymedium">'.$imap_host.' / '.getDolGlobalString('IMPORTZUGFERD_IMAP_FOLDER', 'INBOX').'</span>';
|
||||
print '</div>';
|
||||
print '<div class="inline-block valignmiddle floatright">';
|
||||
if (function_exists('imap_open')) {
|
||||
print '<input type="submit" class="button button-primary" value="'.$langs->trans('StartImport').'">';
|
||||
} else {
|
||||
print '<span class="error">'.$langs->trans('IMAPExtensionNotInstalled').'</span>';
|
||||
}
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
|
||||
// Show results
|
||||
if (!empty($import_results)) {
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>'.$langs->trans('File').'</td>';
|
||||
print '<td>'.$langs->trans('Status').'</td>';
|
||||
print '<td>'.$langs->trans('Message').'</td>';
|
||||
print '<td>'.$langs->trans('SupplierInvoice').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
foreach ($import_results as $result) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.dol_escape_htmltag($result['file']).'</td>';
|
||||
print '<td>';
|
||||
if ($result['status'] == 'success') {
|
||||
print '<span class="badge badge-status4">'.$langs->trans('Success').'</span>';
|
||||
if (!empty($result['archived'])) {
|
||||
print ' <i class="fas fa-archive opacitymedium" title="'.$langs->trans('Archived').'"></i>';
|
||||
}
|
||||
} elseif ($result['status'] == 'skipped') {
|
||||
print '<span class="badge badge-status1">'.$langs->trans('Skipped').'</span>';
|
||||
} else {
|
||||
print '<span class="badge badge-status8">'.$langs->trans('Error').'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '<td>'.dol_escape_htmltag($result['message']).'</td>';
|
||||
print '<td>';
|
||||
if ($result['invoice_id'] > 0) {
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
||||
$invoice = new FactureFournisseur($db);
|
||||
$invoice->fetch($result['invoice_id']);
|
||||
print $invoice->getNomUrl(1);
|
||||
} else {
|
||||
print '-';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
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.
316
build/buildzip.php
Executable file
316
build/buildzip.php
Executable file
|
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/env php -d memory_limit=256M
|
||||
<?php
|
||||
/**
|
||||
* buildzip.php
|
||||
*
|
||||
* Copyright (c) 2023-2025 Eric Seigne <eric.seigne@cap-rel.fr>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
The goal of that php CLI script is to make zip package of your module
|
||||
as an alternative to web "build zip" or "perl script makepack"
|
||||
*/
|
||||
|
||||
// ============================================= configuration
|
||||
|
||||
/**
|
||||
* list of files & dirs of your module
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
$listOfModuleContent = [
|
||||
'admin',
|
||||
'ajax',
|
||||
'backport',
|
||||
'class',
|
||||
'css',
|
||||
'COPYING',
|
||||
'core',
|
||||
'img',
|
||||
'js',
|
||||
'langs',
|
||||
'lib',
|
||||
'sql',
|
||||
'tpl',
|
||||
'*.md',
|
||||
'*.json',
|
||||
'*.php',
|
||||
'modulebuilder.txt',
|
||||
];
|
||||
|
||||
/**
|
||||
* if you want to exclude some files from your zip
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
$exclude_list = [
|
||||
'/^.git$/',
|
||||
'/.*js.map/',
|
||||
'/DEV.md/'
|
||||
];
|
||||
|
||||
// ============================================= end of configuration
|
||||
|
||||
/**
|
||||
* auto detect module name and version from file name
|
||||
*
|
||||
* @return (string|string)[] module name and module version
|
||||
*/
|
||||
function detectModule()
|
||||
{
|
||||
$name = $version = "";
|
||||
$tab = glob("core/modules/mod*.class.php");
|
||||
if (count($tab) == 0) {
|
||||
echo "[fail] Error on auto detect data : there is no mod*.class.php file into core/modules dir\n";
|
||||
exit(-1);
|
||||
}
|
||||
if (count($tab) == 1) {
|
||||
$file = $tab[0];
|
||||
$pattern = "/.*mod(?<mod>.*)\.class\.php/";
|
||||
if (preg_match_all($pattern, $file, $matches)) {
|
||||
$name = strtolower(reset($matches['mod']));
|
||||
}
|
||||
|
||||
echo "extract data from $file\n";
|
||||
if (!file_exists($file) || $name == "") {
|
||||
echo "[fail] Error on auto detect data\n";
|
||||
exit(-2);
|
||||
}
|
||||
} else {
|
||||
echo "[fail] Error there is more than one mod*.class.php file into core/modules dir\n";
|
||||
exit(-3);
|
||||
}
|
||||
|
||||
//extract version from file
|
||||
$contents = file_get_contents($file);
|
||||
$pattern = "/^.*this->version\s*=\s*'(?<version>.*)'\s*;.*\$/m";
|
||||
|
||||
// search, and store all matching occurrences in $matches
|
||||
if (preg_match_all($pattern, $contents, $matches)) {
|
||||
$version = reset($matches['version']);
|
||||
}
|
||||
|
||||
if (version_compare($version, '0.0.1', '>=') != 1) {
|
||||
echo "[fail] Error auto extract version fail\n";
|
||||
exit(-4);
|
||||
}
|
||||
|
||||
echo "module name = $name, version = $version\n";
|
||||
return [(string) $name, (string) $version];
|
||||
}
|
||||
|
||||
/**
|
||||
* delete recursively a directory
|
||||
*
|
||||
* @param string $dir dir path to delete
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
function delTree($dir)
|
||||
{
|
||||
$files = array_diff(scandir($dir), array('.', '..'));
|
||||
foreach ($files as $file) {
|
||||
(is_dir("$dir/$file")) ? delTree("$dir/$file") : secureUnlink("$dir/$file");
|
||||
}
|
||||
return rmdir($dir);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* do a secure delete file/dir with double check
|
||||
* (don't trust unlink return)
|
||||
*
|
||||
* @param string $path full path to delete
|
||||
*
|
||||
* @return bool true on success ($path does not exists at the end of process), else exit
|
||||
*/
|
||||
function secureUnlink($path)
|
||||
{
|
||||
if (file_exists($path)) {
|
||||
if (unlink($path)) {
|
||||
//then check if really deleted
|
||||
clearstatcache();
|
||||
if (file_exists($path)) { // @phpstan-ignore-line
|
||||
echo "[fail] unlink of $path fail !\n";
|
||||
exit(-5);
|
||||
}
|
||||
} else {
|
||||
echo "[fail] unlink of $path fail !\n";
|
||||
exit(-6);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* create a directory and check if dir exists
|
||||
*
|
||||
* @param string $path path to make
|
||||
*
|
||||
* @return bool true on success ($path exists at the end of process), else exit
|
||||
*/
|
||||
function mkdirAndCheck($path)
|
||||
{
|
||||
if (mkdir($path)) {
|
||||
clearstatcache();
|
||||
if (is_dir($path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
echo "[fail] Error on $path (mkdir)\n";
|
||||
exit(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* check if that filename is concerned by exclude filter
|
||||
*
|
||||
* @param string $filename file name to check
|
||||
*
|
||||
* @return bool true if file is in excluded list
|
||||
*/
|
||||
function is_excluded($filename)
|
||||
{
|
||||
global $exclude_list;
|
||||
$count = 0;
|
||||
$notused = preg_filter($exclude_list, '1', $filename, -1, $count);
|
||||
if ($count > 0) {
|
||||
echo " - exclude $filename\n";
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* recursive copy files & dirs
|
||||
*
|
||||
* @param string $src source dir
|
||||
* @param string $dst target dir
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
function rcopy($src, $dst)
|
||||
{
|
||||
if (is_dir($src)) {
|
||||
// Make the destination directory if not exist
|
||||
mkdirAndCheck($dst);
|
||||
// open the source directory
|
||||
$dir = opendir($src);
|
||||
|
||||
// Loop through the files in source directory
|
||||
while ($file = readdir($dir)) {
|
||||
if (($file != '.') && ($file != '..')) {
|
||||
if (is_dir($src . '/' . $file)) {
|
||||
// Recursively calling custom copy function
|
||||
// for sub directory
|
||||
if (!rcopy($src . '/' . $file, $dst . '/' . $file)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!is_excluded($file)) {
|
||||
if (!copy($src . '/' . $file, $dst . '/' . $file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($dir);
|
||||
} elseif (is_file($src)) {
|
||||
if (!is_excluded($src)) {
|
||||
if (!copy($src, $dst)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* build a zip file with only php code and no external depends
|
||||
* on "zip" exec for example
|
||||
*
|
||||
* @param string $folder folder to use as zip root
|
||||
* @param ZipArchive $zip zip object (ZipArchive)
|
||||
* @param string $root relative root path into the zip
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
function zipDir($folder, &$zip, $root = "")
|
||||
{
|
||||
foreach (new \DirectoryIterator($folder) as $f) {
|
||||
if ($f->isDot()) {
|
||||
continue;
|
||||
} //skip . ..
|
||||
$src = $folder . '/' . $f;
|
||||
$dst = substr($f->getPathname(), strlen($root));
|
||||
if ($f->isDir()) {
|
||||
if ($zip->addEmptyDir($dst)) {
|
||||
if (zipDir($src, $zip, $root)) {
|
||||
continue;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ($f->isFile()) {
|
||||
if (! $zip->addFile($src, $dst)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* main part of script
|
||||
*/
|
||||
|
||||
list($mod, $version) = detectModule();
|
||||
$outzip = sys_get_temp_dir() . "/module_" . $mod . "-" . $version . ".zip";
|
||||
if (file_exists($outzip)) {
|
||||
secureUnlink($outzip);
|
||||
}
|
||||
|
||||
//copy all sources into system temp directory
|
||||
$tmpdir = tempnam(sys_get_temp_dir(), $mod . "-module");
|
||||
secureUnlink($tmpdir);
|
||||
mkdirAndCheck($tmpdir);
|
||||
$dst = $tmpdir . "/" . $mod;
|
||||
mkdirAndCheck($dst);
|
||||
|
||||
foreach ($listOfModuleContent as $moduleContent) {
|
||||
foreach (glob($moduleContent) as $entry) {
|
||||
if (!rcopy($entry, $dst . '/' . $entry)) {
|
||||
echo "[fail] Error on copy " . $entry . " to " . $dst . "/" . $entry . "\n";
|
||||
echo "Please take time to analyze the problem and fix the bug\n";
|
||||
exit(-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$z = new ZipArchive();
|
||||
$z->open($outzip, ZIPARCHIVE::CREATE);
|
||||
zipDir($tmpdir, $z, $tmpdir . '/');
|
||||
$z->close();
|
||||
delTree($tmpdir);
|
||||
if (file_exists($outzip)) {
|
||||
echo "[success] module archive is ready : $outzip ...\n";
|
||||
} else {
|
||||
echo "[fail] build zip error\n";
|
||||
exit(-9);
|
||||
}
|
||||
11
build/makepack-importzugferd.conf
Executable file
11
build/makepack-importzugferd.conf
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
# Your module name here
|
||||
#
|
||||
# Goal: Goal of module
|
||||
# Version: <version>
|
||||
# Author: Copyright <year> - <name of author>
|
||||
# License: GPLv3
|
||||
# Install: Just unpack content of module package in Dolibarr directory.
|
||||
# Setup: Go on Dolibarr setup - modules to enable module.
|
||||
#
|
||||
# Files in module
|
||||
mymodule/
|
||||
275
card.php
Executable file
275
card.php
Executable file
|
|
@ -0,0 +1,275 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file card.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Card page for ZUGFeRD import record
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../main.inc.php")) {
|
||||
$res = @include "../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
||||
|
||||
dol_include_once('/importzugferd/class/zugferdimport.class.php');
|
||||
dol_include_once('/importzugferd/lib/importzugferd.lib.php');
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array("importzugferd@importzugferd", "bills", "companies"));
|
||||
|
||||
// Get parameters
|
||||
$id = GETPOST('id', 'int');
|
||||
$ref = GETPOST('ref', 'alpha');
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
|
||||
// Initialize object
|
||||
$object = new ZugferdImport($db);
|
||||
|
||||
// Load object
|
||||
if ($id > 0 || !empty($ref)) {
|
||||
$result = $object->fetch($id, $ref);
|
||||
if ($result <= 0) {
|
||||
setEventMessages($langs->trans('RecordNotFound'), null, 'errors');
|
||||
header('Location: '.dol_buildpath('/importzugferd/list.php', 1));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('importzugferd', 'import', 'read')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
$permissiontodelete = $user->hasRight('importzugferd', 'import', 'delete');
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// Delete confirmation
|
||||
if ($action == 'delete' && $confirm == 'yes' && $permissiontodelete) {
|
||||
$result = $object->delete($user);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||
header('Location: '.dol_buildpath('/importzugferd/list.php', 1));
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($object->error, $object->errors, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
$title = $langs->trans('ImportRecord').' - '.$object->ref;
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-card');
|
||||
|
||||
// Confirmation dialog for delete
|
||||
if ($action == 'delete') {
|
||||
print $form->formconfirm(
|
||||
$_SERVER["PHP_SELF"].'?id='.$object->id,
|
||||
$langs->trans('DeleteImportRecord'),
|
||||
$langs->trans('ConfirmDeleteImportRecord', $object->ref),
|
||||
'delete',
|
||||
'',
|
||||
0,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
// Header
|
||||
print '<div class="fichecenter">';
|
||||
print '<div class="underbanner clearboth"></div>';
|
||||
|
||||
print '<table class="border centpercent tableforfield">';
|
||||
|
||||
// Ref
|
||||
print '<tr>';
|
||||
print '<td class="titlefield">'.$langs->trans('Ref').'</td>';
|
||||
print '<td>'.$object->ref.'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Invoice number
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('InvoiceNumber').'</td>';
|
||||
print '<td><strong>'.dol_escape_htmltag($object->invoice_number).'</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
// Invoice date
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('InvoiceDate').'</td>';
|
||||
print '<td>'.dol_print_date($object->invoice_date, 'day').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Seller
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('Supplier').'</td>';
|
||||
print '<td>';
|
||||
if ($object->fk_soc > 0) {
|
||||
$supplier = new Societe($db);
|
||||
$supplier->fetch($object->fk_soc);
|
||||
print $supplier->getNomUrl(1);
|
||||
print ' <span class="opacitymedium">('.dol_escape_htmltag($object->seller_name).')</span>';
|
||||
} else {
|
||||
print dol_escape_htmltag($object->seller_name);
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// VAT ID
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('VATIntra').'</td>';
|
||||
print '<td>'.dol_escape_htmltag($object->seller_vat).'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Buyer reference
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('BuyerReference').'</td>';
|
||||
print '<td>'.dol_escape_htmltag($object->buyer_reference).'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Total HT
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('TotalHT').'</td>';
|
||||
print '<td>'.price($object->total_ht).' '.$object->currency.'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Total TTC
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('TotalTTC').'</td>';
|
||||
print '<td><strong>'.price($object->total_ttc).' '.$object->currency.'</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
// Supplier invoice
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('SupplierInvoice').'</td>';
|
||||
print '<td>';
|
||||
if ($object->fk_facture_fourn > 0) {
|
||||
$invoice = new FactureFournisseur($db);
|
||||
$invoice->fetch($object->fk_facture_fourn);
|
||||
print $invoice->getNomUrl(1);
|
||||
} else {
|
||||
print '-';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Status
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('Status').'</td>';
|
||||
print '<td>'.$object->getLibStatut(1).'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Error message
|
||||
if ($object->status == ZugferdImport::STATUS_ERROR && !empty($object->error_message)) {
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('ErrorMessage').'</td>';
|
||||
print '<td><span class="error">'.dol_escape_htmltag($object->error_message).'</span></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// PDF filename
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('File').'</td>';
|
||||
print '<td>'.dol_escape_htmltag($object->pdf_filename).'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Date creation
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('DateCreation').'</td>';
|
||||
print '<td>'.dol_print_date($object->date_creation, 'dayhour').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print '</div>';
|
||||
|
||||
// Action buttons
|
||||
print '<div class="tabsAction">';
|
||||
|
||||
// Reimport button - link to import page
|
||||
print '<a class="butAction" href="'.dol_buildpath('/importzugferd/import.php', 1).'">'.$langs->trans('ImportAnother').'</a>';
|
||||
|
||||
// Delete button
|
||||
if ($permissiontodelete) {
|
||||
print '<a class="butActionDelete" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=delete&token='.newToken().'">'.$langs->trans('Delete').'</a>';
|
||||
}
|
||||
|
||||
print '</div>';
|
||||
|
||||
// Show XML content (collapsed)
|
||||
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">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans('XMLContent').'</td>';
|
||||
print '</tr>';
|
||||
print '<tr class="oddeven">';
|
||||
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: 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>';
|
||||
print '</tr>';
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
1067
class/actions_importzugferd.class.php
Executable file
1067
class/actions_importzugferd.class.php
Executable file
File diff suppressed because it is too large
Load diff
791
class/cron_importzugferd.class.php
Executable file
791
class/cron_importzugferd.class.php
Executable file
|
|
@ -0,0 +1,791 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file class/cron_importzugferd.class.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Cron job class for fetching ZUGFeRD invoices from mailbox
|
||||
*/
|
||||
|
||||
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');
|
||||
dol_include_once('/importzugferd/class/actions_importzugferd.class.php');
|
||||
|
||||
/**
|
||||
* Class CronImportZugferd
|
||||
* Cronjob handler for fetching ZUGFeRD invoices from IMAP mailbox and folder
|
||||
*/
|
||||
class CronImportZugferd
|
||||
{
|
||||
/**
|
||||
* @var DoliDB Database handler
|
||||
*/
|
||||
public $db;
|
||||
|
||||
/**
|
||||
* @var string Error message
|
||||
*/
|
||||
public $error = '';
|
||||
|
||||
/**
|
||||
* @var array Error messages
|
||||
*/
|
||||
public $errors = array();
|
||||
|
||||
/**
|
||||
* @var string Output message
|
||||
*/
|
||||
public $output = '';
|
||||
|
||||
/**
|
||||
* @var int Number of imported invoices
|
||||
*/
|
||||
public $imported_count = 0;
|
||||
|
||||
/**
|
||||
* @var int Number of skipped invoices (duplicates)
|
||||
*/
|
||||
public $skipped_count = 0;
|
||||
|
||||
/**
|
||||
* @var int Number of errors
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
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) ==========");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if import should run based on configured frequency
|
||||
*
|
||||
* @return bool True if import should run
|
||||
*/
|
||||
protected function shouldRunImport()
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$frequency = getDolGlobalString('IMPORTZUGFERD_IMPORT_FREQUENCY', 'manual');
|
||||
|
||||
if ($frequency === 'manual') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get last run timestamp
|
||||
$lastRun = getDolGlobalInt('IMPORTZUGFERD_LAST_IMPORT_RUN', 0);
|
||||
$now = dol_now();
|
||||
|
||||
// Calculate minimum interval based on frequency
|
||||
$interval = 0;
|
||||
switch ($frequency) {
|
||||
case 'hourly':
|
||||
$interval = 3600; // 1 hour
|
||||
break;
|
||||
case 'daily':
|
||||
$interval = 86400; // 24 hours
|
||||
break;
|
||||
case 'weekly':
|
||||
$interval = 604800; // 7 days
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if enough time has passed
|
||||
if ($now - $lastRun < $interval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last run timestamp
|
||||
*/
|
||||
protected function updateLastRunTime()
|
||||
{
|
||||
global $conf;
|
||||
dolibarr_set_const($this->db, 'IMPORTZUGFERD_LAST_IMPORT_RUN', dol_now(), 'chaine', 0, '', $conf->entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main import method - imports from both folder and mailbox
|
||||
*
|
||||
* @return int 0 if OK, <0 if error
|
||||
*/
|
||||
public function runScheduledImport()
|
||||
{
|
||||
global $langs;
|
||||
|
||||
// Initialize timing and shutdown handler
|
||||
$this->startTime = microtime(true);
|
||||
register_shutdown_function(array($this, 'handleShutdown'));
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
$this->cronLog("========== CRON START ==========");
|
||||
$this->cronLog("PHP Version: ".PHP_VERSION.", Memory Limit: ".ini_get('memory_limit'));
|
||||
|
||||
// Reset counters
|
||||
$this->imported_count = 0;
|
||||
$this->skipped_count = 0;
|
||||
$this->error_count = 0;
|
||||
$this->errors = array();
|
||||
|
||||
try {
|
||||
$this->cronLog("Starting folder import...");
|
||||
$this->importFromFolder();
|
||||
$this->cronLog("Folder import completed");
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Update last run time
|
||||
$this->updateLastRunTime();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import ZUGFeRD invoices from watch folder
|
||||
*
|
||||
* @return int 0 if OK, <0 if error
|
||||
*/
|
||||
public function importFromFolder()
|
||||
{
|
||||
global $langs;
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
$watchFolder = getDolGlobalString('IMPORTZUGFERD_WATCH_FOLDER');
|
||||
$archiveFolder = getDolGlobalString('IMPORTZUGFERD_ARCHIVE_FOLDER');
|
||||
$errorFolder = getDolGlobalString('IMPORTZUGFERD_ERROR_FOLDER');
|
||||
$autoCreate = getDolGlobalString('IMPORTZUGFERD_AUTO_CREATE_INVOICE');
|
||||
|
||||
$this->cronLog("Watch folder: {$watchFolder}");
|
||||
|
||||
// Validate settings
|
||||
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');
|
||||
$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);
|
||||
}
|
||||
|
||||
// Ensure error folder exists if configured
|
||||
if (!empty($errorFolder) && !is_dir($errorFolder)) {
|
||||
dol_mkdir($errorFolder);
|
||||
}
|
||||
|
||||
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, !empty($autoCreate));
|
||||
|
||||
if ($result > 0) {
|
||||
$this->imported_count++;
|
||||
$this->cronLog("Imported: ".basename($file)." -> ID {$result}");
|
||||
|
||||
// Archive the file
|
||||
$this->moveFile($file, $archiveFolder, 'imported_');
|
||||
} elseif ($result == -2) {
|
||||
// Duplicate - already imported
|
||||
$this->skipped_count++;
|
||||
$this->cronLog("Skipped (duplicate): ".basename($file));
|
||||
|
||||
// Archive duplicates - delete if no archive folder
|
||||
if (!$this->moveFile($file, $archiveFolder, 'duplicate_')) {
|
||||
@unlink($file);
|
||||
}
|
||||
} else {
|
||||
$this->error_count++;
|
||||
$this->errors[] = basename($file) . ': ' . $import->error;
|
||||
$this->cronLog("Error importing ".basename($file).": ".$import->error, 'ERROR');
|
||||
|
||||
// Try error folder first, fall back to archive folder
|
||||
if (!$this->moveFile($file, $errorFolder, 'error_')) {
|
||||
// Use archive folder as fallback for errors
|
||||
$this->moveFile($file, $archiveFolder, 'error_');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->cronLog("Folder import finished: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ZUGFeRD invoices from configured IMAP mailbox
|
||||
*
|
||||
* @return int 0 if OK, <0 if error
|
||||
*/
|
||||
public function fetchFromMailbox()
|
||||
{
|
||||
global $conf, $user, $langs;
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
// Get IMAP settings
|
||||
$host = getDolGlobalString('IMPORTZUGFERD_IMAP_HOST');
|
||||
$port = getDolGlobalString('IMPORTZUGFERD_IMAP_PORT', '993');
|
||||
$imap_user = getDolGlobalString('IMPORTZUGFERD_IMAP_USER');
|
||||
$password = getDolGlobalString('IMPORTZUGFERD_IMAP_PASSWORD');
|
||||
$folder = getDolGlobalString('IMPORTZUGFERD_IMAP_FOLDER', 'INBOX');
|
||||
$ssl = getDolGlobalString('IMPORTZUGFERD_IMAP_SSL');
|
||||
$auto_create = getDolGlobalString('IMPORTZUGFERD_AUTO_CREATE_INVOICE');
|
||||
|
||||
// Validate settings
|
||||
if (empty($host) || empty($imap_user) || empty($password)) {
|
||||
$this->error = 'IMAP settings not configured';
|
||||
$this->output = $this->error;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Build mailbox string
|
||||
$mailbox = '{' . $host . ':' . $port . '/imap';
|
||||
if ($ssl) {
|
||||
$mailbox .= '/ssl';
|
||||
}
|
||||
$mailbox .= '/novalidate-cert}' . $folder;
|
||||
|
||||
// Connect to IMAP
|
||||
$connection = @imap_open($mailbox, $imap_user, $password);
|
||||
|
||||
if (!$connection) {
|
||||
$this->error = 'IMAP connection failed: ' . imap_last_error();
|
||||
$this->output = $this->error;
|
||||
return -2;
|
||||
}
|
||||
|
||||
// Search for unread messages with attachments
|
||||
$messages = imap_search($connection, 'UNSEEN');
|
||||
|
||||
if ($messages === false) {
|
||||
$this->output = 'No new messages found';
|
||||
imap_close($connection);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$temp_dir = $conf->importzugferd->dir_output . '/temp';
|
||||
if (!is_dir($temp_dir)) {
|
||||
dol_mkdir($temp_dir);
|
||||
}
|
||||
|
||||
// Load admin user for import actions
|
||||
$admin_user = new User($this->db);
|
||||
$admin_user->fetch(1); // Fetch admin user
|
||||
|
||||
$actions = new ActionsImportZugferd($this->db);
|
||||
|
||||
foreach ($messages as $msg_num) {
|
||||
$structure = imap_fetchstructure($connection, $msg_num);
|
||||
|
||||
// Check for attachments
|
||||
$attachments = $this->getAttachments($connection, $msg_num, $structure);
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
// Check if it's a PDF
|
||||
if (strtolower($attachment['type']) !== 'pdf') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save attachment temporarily
|
||||
$temp_file = $temp_dir . '/' . uniqid('zugferd_') . '.pdf';
|
||||
file_put_contents($temp_file, $attachment['data']);
|
||||
|
||||
// Check if it's a ZUGFeRD PDF
|
||||
$parser = new ZugferdParser($this->db);
|
||||
$result = $parser->extractFromPdf($temp_file);
|
||||
|
||||
if ($result > 0) {
|
||||
// It's a ZUGFeRD invoice, try to import
|
||||
$result = $actions->processPdf($temp_file, $admin_user, $auto_create);
|
||||
|
||||
if ($result > 0) {
|
||||
$this->imported_count++;
|
||||
dol_syslog("CronImportZugferd: Imported invoice from email, ID: " . $result, LOG_INFO);
|
||||
} elseif ($result == -3) {
|
||||
// Duplicate
|
||||
$this->skipped_count++;
|
||||
dol_syslog("CronImportZugferd: Skipped duplicate invoice", LOG_INFO);
|
||||
} else {
|
||||
$this->error_count++;
|
||||
$this->errors[] = $actions->error;
|
||||
dol_syslog("CronImportZugferd: Error importing invoice: " . $actions->error, LOG_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up temp file
|
||||
if (file_exists($temp_file)) {
|
||||
unlink($temp_file);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark message as read
|
||||
imap_setflag_full($connection, (string)$msg_num, '\\Seen');
|
||||
}
|
||||
|
||||
imap_close($connection);
|
||||
|
||||
// Build output message
|
||||
$this->output = sprintf(
|
||||
"Processed %d messages. Imported: %d, Skipped (duplicates): %d, Errors: %d",
|
||||
count($messages),
|
||||
$this->imported_count,
|
||||
$this->skipped_count,
|
||||
$this->error_count
|
||||
);
|
||||
|
||||
if ($this->error_count > 0) {
|
||||
$this->output .= "\nErrors: " . implode(", ", $this->errors);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract attachments from email
|
||||
*
|
||||
* @param resource $connection IMAP connection
|
||||
* @param int $msg_num Message number
|
||||
* @param object $structure Message structure
|
||||
* @param string $part_num Part number for nested parts
|
||||
* @return array Attachments
|
||||
*/
|
||||
private function getAttachments($connection, $msg_num, $structure, $part_num = '')
|
||||
{
|
||||
$attachments = array();
|
||||
|
||||
// Check if it's a multipart message
|
||||
if (isset($structure->parts) && count($structure->parts)) {
|
||||
foreach ($structure->parts as $key => $part) {
|
||||
$attachments = array_merge(
|
||||
$attachments,
|
||||
$this->getAttachments($connection, $msg_num, $part, ($part_num ? $part_num . '.' : '') . ($key + 1))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Check if this part is an attachment
|
||||
$attachment = $this->extractAttachment($connection, $msg_num, $structure, $part_num);
|
||||
if ($attachment) {
|
||||
$attachments[] = $attachment;
|
||||
}
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a single attachment
|
||||
*
|
||||
* @param resource $connection IMAP connection
|
||||
* @param int $msg_num Message number
|
||||
* @param object $part Part structure
|
||||
* @param string $part_num Part number
|
||||
* @return array|null Attachment data or null
|
||||
*/
|
||||
private function extractAttachment($connection, $msg_num, $part, $part_num)
|
||||
{
|
||||
$filename = '';
|
||||
|
||||
// Get filename from parameters
|
||||
if (isset($part->dparameters)) {
|
||||
foreach ($part->dparameters as $param) {
|
||||
if (strtolower($param->attribute) === 'filename') {
|
||||
$filename = $param->value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($filename) && isset($part->parameters)) {
|
||||
foreach ($part->parameters as $param) {
|
||||
if (strtolower($param->attribute) === 'name') {
|
||||
$filename = $param->value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a PDF attachment
|
||||
if (empty($filename) || !preg_match('/\.pdf$/i', $filename)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get attachment data
|
||||
if ($part_num) {
|
||||
$data = imap_fetchbody($connection, $msg_num, $part_num);
|
||||
} else {
|
||||
$data = imap_body($connection, $msg_num);
|
||||
}
|
||||
|
||||
// Decode based on encoding
|
||||
if (isset($part->encoding)) {
|
||||
switch ($part->encoding) {
|
||||
case 3: // BASE64
|
||||
$data = base64_decode($data);
|
||||
break;
|
||||
case 4: // QUOTED-PRINTABLE
|
||||
$data = quoted_printable_decode($data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Get file extension
|
||||
$ext = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
|
||||
return array(
|
||||
'filename' => $filename,
|
||||
'type' => strtolower($ext),
|
||||
'data' => $data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move file to target folder with proper error handling
|
||||
*
|
||||
* @param string $file Source file path
|
||||
* @param string $targetFolder Target folder path
|
||||
* @param string $prefix Filename prefix (e.g., 'imported_', 'duplicate_', 'error_')
|
||||
* @return bool True if moved/deleted, false on failure
|
||||
*/
|
||||
protected function moveFile($file, $targetFolder, $prefix = '')
|
||||
{
|
||||
if (!file_exists($file)) {
|
||||
dol_syslog("CronImportZugferd: File not found: " . $file, LOG_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If target folder is configured and exists/writable
|
||||
if (!empty($targetFolder)) {
|
||||
// Create folder if it doesn't exist
|
||||
if (!is_dir($targetFolder)) {
|
||||
$result = dol_mkdir($targetFolder);
|
||||
if ($result < 0) {
|
||||
dol_syslog("CronImportZugferd: Failed to create folder: " . $targetFolder, LOG_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_dir($targetFolder) && is_writable($targetFolder)) {
|
||||
// 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);
|
||||
return true;
|
||||
} else {
|
||||
// Try copy + delete as fallback (for cross-filesystem moves)
|
||||
if (@copy($file, $targetPath)) {
|
||||
@unlink($file);
|
||||
dol_syslog("CronImportZugferd: Copied file to: " . $targetPath, LOG_INFO);
|
||||
return true;
|
||||
} else {
|
||||
dol_syslog("CronImportZugferd: Failed to move/copy file to: " . $targetPath, LOG_ERR);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dol_syslog("CronImportZugferd: Target folder not writable: " . $targetFolder, LOG_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
// No target folder configured or not writable - delete file from watch folder
|
||||
// to prevent re-processing (except for errors without error folder)
|
||||
if ($prefix !== 'error_') {
|
||||
if (@unlink($file)) {
|
||||
dol_syslog("CronImportZugferd: Deleted processed file: " . $file, LOG_INFO);
|
||||
return true;
|
||||
} else {
|
||||
dol_syslog("CronImportZugferd: Failed to delete file: " . $file, LOG_ERR);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Error files stay in watch folder if no error folder configured
|
||||
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;
|
||||
}
|
||||
}
|
||||
1150
class/datanorm.class.php
Executable file
1150
class/datanorm.class.php
Executable file
File diff suppressed because it is too large
Load diff
1016
class/datanormparser.class.php
Executable file
1016
class/datanormparser.class.php
Executable file
File diff suppressed because it is too large
Load diff
431
class/importline.class.php
Executable file
431
class/importline.class.php
Executable file
|
|
@ -0,0 +1,431 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file importline.class.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Class for import line items
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class ImportLine
|
||||
* Manages line items for ZUGFeRD imports
|
||||
*/
|
||||
class ImportLine
|
||||
{
|
||||
/**
|
||||
* @var DoliDB Database handler
|
||||
*/
|
||||
public $db;
|
||||
|
||||
/**
|
||||
* @var string Error message
|
||||
*/
|
||||
public $error;
|
||||
|
||||
/**
|
||||
* @var array Error messages
|
||||
*/
|
||||
public $errors = array();
|
||||
|
||||
/**
|
||||
* @var int ID
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @var int ID (alias)
|
||||
*/
|
||||
public $rowid;
|
||||
|
||||
/**
|
||||
* @var int Import ID reference
|
||||
*/
|
||||
public $fk_import;
|
||||
|
||||
/**
|
||||
* @var string Line ID from ZUGFeRD
|
||||
*/
|
||||
public $line_id;
|
||||
|
||||
/**
|
||||
* @var string Supplier article reference
|
||||
*/
|
||||
public $supplier_ref;
|
||||
|
||||
/**
|
||||
* @var string Product name from ZUGFeRD
|
||||
*/
|
||||
public $product_name;
|
||||
|
||||
/**
|
||||
* @var string Additional description
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var float Quantity
|
||||
*/
|
||||
public $quantity;
|
||||
|
||||
/**
|
||||
* @var string UN/ECE unit code
|
||||
*/
|
||||
public $unit_code;
|
||||
|
||||
/**
|
||||
* @var float Unit price (calculated)
|
||||
*/
|
||||
public $unit_price;
|
||||
|
||||
/**
|
||||
* @var float Original unit price
|
||||
*/
|
||||
public $unit_price_raw;
|
||||
|
||||
/**
|
||||
* @var float Basis quantity for price
|
||||
*/
|
||||
public $basis_quantity;
|
||||
|
||||
/**
|
||||
* @var string Basis quantity unit
|
||||
*/
|
||||
public $basis_quantity_unit;
|
||||
|
||||
/**
|
||||
* @var float Line total (net)
|
||||
*/
|
||||
public $line_total;
|
||||
|
||||
/**
|
||||
* @var float Tax percentage
|
||||
*/
|
||||
public $tax_percent;
|
||||
|
||||
/**
|
||||
* @var string EAN/GTIN
|
||||
*/
|
||||
public $ean;
|
||||
|
||||
/**
|
||||
* @var float Copper surcharge per unit (Kupferzuschlag)
|
||||
*/
|
||||
public $copper_surcharge;
|
||||
|
||||
/**
|
||||
* @var float Basis quantity for copper surcharge
|
||||
*/
|
||||
public $copper_surcharge_basis_qty;
|
||||
|
||||
/**
|
||||
* @var int Assigned Dolibarr product ID
|
||||
*/
|
||||
public $fk_product;
|
||||
|
||||
/**
|
||||
* @var int Assigned Datanorm article ID
|
||||
*/
|
||||
public $fk_datanorm;
|
||||
|
||||
/**
|
||||
* @var string Match method description
|
||||
*/
|
||||
public $match_method;
|
||||
|
||||
/**
|
||||
* @var int Creation timestamp
|
||||
*/
|
||||
public $date_creation;
|
||||
|
||||
/**
|
||||
* @var string Table name
|
||||
*/
|
||||
public $table_element = 'importzugferd_import_line';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create line in database
|
||||
*
|
||||
* @param User $user User creating the line
|
||||
* @return int >0 if OK, <0 if KO
|
||||
*/
|
||||
public function create($user)
|
||||
{
|
||||
$this->date_creation = dol_now();
|
||||
|
||||
$sql = "INSERT INTO " . MAIN_DB_PREFIX . $this->table_element . " (";
|
||||
$sql .= "fk_import, line_id, supplier_ref, product_name, description,";
|
||||
$sql .= "quantity, unit_code, unit_price, unit_price_raw, basis_quantity, basis_quantity_unit,";
|
||||
$sql .= "line_total, tax_percent, ean, copper_surcharge, copper_surcharge_basis_qty,";
|
||||
$sql .= "fk_product, match_method, date_creation";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= ((int) $this->fk_import) . ",";
|
||||
$sql .= "'" . $this->db->escape($this->line_id) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->supplier_ref) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->product_name) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->description) . "',";
|
||||
$sql .= ((float) $this->quantity) . ",";
|
||||
$sql .= "'" . $this->db->escape($this->unit_code) . "',";
|
||||
$sql .= ((float) $this->unit_price) . ",";
|
||||
$sql .= ((float) $this->unit_price_raw) . ",";
|
||||
$sql .= ((float) $this->basis_quantity) . ",";
|
||||
$sql .= "'" . $this->db->escape($this->basis_quantity_unit) . "',";
|
||||
$sql .= ((float) $this->line_total) . ",";
|
||||
$sql .= ((float) $this->tax_percent) . ",";
|
||||
$sql .= "'" . $this->db->escape($this->ean) . "',";
|
||||
$sql .= ($this->copper_surcharge !== null ? ((float) $this->copper_surcharge) : "NULL") . ",";
|
||||
$sql .= ($this->copper_surcharge_basis_qty !== null ? ((float) $this->copper_surcharge_basis_qty) : "NULL") . ",";
|
||||
$sql .= ($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL") . ",";
|
||||
$sql .= "'" . $this->db->escape($this->match_method) . "',";
|
||||
$sql .= "'" . $this->db->idate($this->date_creation) . "'";
|
||||
$sql .= ")";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX . $this->table_element);
|
||||
$this->rowid = $this->id;
|
||||
return $this->id;
|
||||
} else {
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch line from database
|
||||
*
|
||||
* @param int $id Line ID
|
||||
* @return int >0 if OK, <0 if KO
|
||||
*/
|
||||
public function fetch($id)
|
||||
{
|
||||
// Use SELECT * to be compatible with older database versions without fk_datanorm column
|
||||
$sql = "SELECT * FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE rowid = " . ((int) $id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
if ($this->db->num_rows($resql)) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$this->id = $obj->rowid;
|
||||
$this->rowid = $obj->rowid;
|
||||
$this->fk_import = $obj->fk_import;
|
||||
$this->line_id = $obj->line_id;
|
||||
$this->supplier_ref = $obj->supplier_ref;
|
||||
$this->product_name = $obj->product_name;
|
||||
$this->description = $obj->description;
|
||||
$this->quantity = $obj->quantity;
|
||||
$this->unit_code = $obj->unit_code;
|
||||
$this->unit_price = $obj->unit_price;
|
||||
$this->unit_price_raw = $obj->unit_price_raw;
|
||||
$this->basis_quantity = $obj->basis_quantity;
|
||||
$this->basis_quantity_unit = $obj->basis_quantity_unit;
|
||||
$this->line_total = $obj->line_total;
|
||||
$this->tax_percent = $obj->tax_percent;
|
||||
$this->ean = $obj->ean;
|
||||
$this->copper_surcharge = isset($obj->copper_surcharge) ? $obj->copper_surcharge : null;
|
||||
$this->copper_surcharge_basis_qty = isset($obj->copper_surcharge_basis_qty) ? $obj->copper_surcharge_basis_qty : null;
|
||||
$this->fk_product = $obj->fk_product;
|
||||
$this->fk_datanorm = isset($obj->fk_datanorm) ? $obj->fk_datanorm : null;
|
||||
$this->match_method = $obj->match_method;
|
||||
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update line in database
|
||||
*
|
||||
* @param User $user User making the update
|
||||
* @return int >0 if OK, <0 if KO
|
||||
*/
|
||||
public function update($user)
|
||||
{
|
||||
$sql = "UPDATE " . MAIN_DB_PREFIX . $this->table_element . " SET";
|
||||
$sql .= " fk_product = " . ($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL") . ",";
|
||||
$sql .= " fk_datanorm = " . ($this->fk_datanorm > 0 ? ((int) $this->fk_datanorm) : "NULL") . ",";
|
||||
$sql .= " match_method = '" . $this->db->escape($this->match_method) . "'";
|
||||
$sql .= " WHERE rowid = " . ((int) $this->id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
return 1;
|
||||
}
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete line from database
|
||||
*
|
||||
* @param User $user User deleting the line
|
||||
* @return int >0 if OK, <0 if KO
|
||||
*/
|
||||
public function delete($user)
|
||||
{
|
||||
$sql = "DELETE FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE rowid = " . ((int) $this->id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
return 1;
|
||||
}
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all lines for an import
|
||||
*
|
||||
* @param int $fk_import Import ID
|
||||
* @return array|int Array of ImportLine objects or <0 if error
|
||||
*/
|
||||
public function fetchAllByImport($fk_import)
|
||||
{
|
||||
$lines = array();
|
||||
|
||||
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE fk_import = " . ((int) $fk_import);
|
||||
$sql .= " ORDER BY rowid ASC";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$line = new ImportLine($this->db);
|
||||
$line->fetch($obj->rowid);
|
||||
$lines[] = $line;
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all lines for an import
|
||||
*
|
||||
* @param int $fk_import Import ID
|
||||
* @return int >0 if OK, <0 if KO
|
||||
*/
|
||||
public function deleteAllByImport($fk_import)
|
||||
{
|
||||
$sql = "DELETE FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE fk_import = " . ((int) $fk_import);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
return 1;
|
||||
}
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all lines for an import have products assigned
|
||||
*
|
||||
* @param int $fk_import Import ID
|
||||
* @return bool True if all lines have products, false otherwise
|
||||
*/
|
||||
public function allLinesHaveProducts($fk_import)
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as total, SUM(CASE WHEN fk_product IS NOT NULL AND fk_product > 0 THEN 1 ELSE 0 END) as with_product";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE fk_import = " . ((int) $fk_import);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return ($obj->total > 0 && $obj->total == $obj->with_product);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count lines without product assignment
|
||||
*
|
||||
* @param int $fk_import Import ID
|
||||
* @return int Number of lines without product
|
||||
*/
|
||||
public function countLinesWithoutProduct($fk_import)
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as cnt FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE fk_import = " . ((int) $fk_import);
|
||||
$sql .= " AND (fk_product IS NULL OR fk_product = 0)";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->cnt;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set product for this line
|
||||
*
|
||||
* @param int $fk_product Product ID
|
||||
* @param string $match_method How product was assigned
|
||||
* @param User $user User making the change
|
||||
* @return int >0 if OK, <0 if KO
|
||||
*/
|
||||
public function setProduct($fk_product, $match_method, $user)
|
||||
{
|
||||
$this->fk_product = $fk_product;
|
||||
$this->match_method = $match_method;
|
||||
return $this->update($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Datanorm reference for this line
|
||||
*
|
||||
* @param int $fk_datanorm Datanorm article ID
|
||||
* @param User $user User making the change
|
||||
* @return int >0 if OK, <0 if KO
|
||||
*/
|
||||
public function setDatanorm($fk_datanorm, $user)
|
||||
{
|
||||
$this->fk_datanorm = $fk_datanorm;
|
||||
$this->match_method = 'datanorm_assigned';
|
||||
return $this->update($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count lines with Datanorm assignment
|
||||
*
|
||||
* @param int $fk_import Import ID
|
||||
* @return int Number of lines with Datanorm
|
||||
*/
|
||||
public function countLinesWithDatanorm($fk_import)
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as cnt FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE fk_import = " . ((int) $fk_import);
|
||||
$sql .= " AND fk_datanorm IS NOT NULL AND fk_datanorm > 0";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->cnt;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
389
class/importnotification.class.php
Executable file
389
class/importnotification.class.php
Executable file
|
|
@ -0,0 +1,389 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file class/importnotification.class.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Email notification class for ZUGFeRD imports
|
||||
*/
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
|
||||
|
||||
/**
|
||||
* Class ImportNotification
|
||||
* Handles email notifications for ZUGFeRD import events
|
||||
*/
|
||||
class ImportNotification
|
||||
{
|
||||
/**
|
||||
* @var DoliDB Database handler
|
||||
*/
|
||||
public $db;
|
||||
|
||||
/**
|
||||
* @var string Error message
|
||||
*/
|
||||
public $error = '';
|
||||
|
||||
/**
|
||||
* @var array Error messages
|
||||
*/
|
||||
public $errors = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if notifications are enabled
|
||||
*
|
||||
* @return bool True if enabled
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return getDolGlobalString('IMPORTZUGFERD_NOTIFY_ENABLED') && getDolGlobalString('IMPORTZUGFERD_NOTIFY_EMAIL');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification email address
|
||||
*
|
||||
* @return string Email address
|
||||
*/
|
||||
public function getNotifyEmail()
|
||||
{
|
||||
return getDolGlobalString('IMPORTZUGFERD_NOTIFY_EMAIL');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification for manual intervention required
|
||||
*
|
||||
* @param ZugferdImport $import Import object
|
||||
* @param array $lines Import lines
|
||||
* @return int 1 if sent, 0 if not needed, -1 on error
|
||||
*/
|
||||
public function sendManualInterventionNotification($import, $lines = array())
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
if (!$this->isEnabled() || !getDolGlobalString('IMPORTZUGFERD_NOTIFY_MANUAL')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
$subject = $langs->trans('NotifySubjectManualIntervention', $import->invoice_number);
|
||||
|
||||
$body = $langs->trans('NotifyBodyManualIntervention', $import->invoice_number, $import->seller_name);
|
||||
$body .= "\n\n";
|
||||
$body .= $langs->trans('InvoiceNumber').': '.$import->invoice_number."\n";
|
||||
$body .= $langs->trans('Supplier').': '.$import->seller_name."\n";
|
||||
$body .= $langs->trans('InvoiceDate').': '.dol_print_date($import->invoice_date, 'day')."\n";
|
||||
$body .= $langs->trans('TotalTTC').': '.price($import->total_ttc).' '.$import->currency."\n";
|
||||
$body .= "\n";
|
||||
|
||||
// List issues
|
||||
$missingProducts = 0;
|
||||
$missingSupplier = ($import->fk_soc <= 0);
|
||||
|
||||
if (!empty($lines)) {
|
||||
foreach ($lines as $line) {
|
||||
if ($line->fk_product <= 0) {
|
||||
$missingProducts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($missingSupplier) {
|
||||
$body .= "- ".$langs->trans('SupplierNotAssigned')."\n";
|
||||
}
|
||||
if ($missingProducts > 0) {
|
||||
$body .= "- ".$missingProducts." ".$langs->trans('ProductsNotAssigned')."\n";
|
||||
}
|
||||
|
||||
$body .= "\n";
|
||||
$body .= $langs->trans('NotifyLinkToImport').': '.dol_buildpath('/importzugferd/import.php', 2).'?action=edit&id='.$import->id;
|
||||
|
||||
return $this->sendEmail($subject, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification for import error
|
||||
*
|
||||
* @param ZugferdImport $import Import object (may be partial)
|
||||
* @param string $errorMessage Error message
|
||||
* @param string $filename Original filename
|
||||
* @return int 1 if sent, 0 if not needed, -1 on error
|
||||
*/
|
||||
public function sendErrorNotification($import, $errorMessage, $filename = '')
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
if (!$this->isEnabled() || !getDolGlobalString('IMPORTZUGFERD_NOTIFY_ERROR')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
$invoiceNum = !empty($import->invoice_number) ? $import->invoice_number : $filename;
|
||||
$subject = $langs->trans('NotifySubjectError', $invoiceNum);
|
||||
|
||||
$body = $langs->trans('NotifyBodyError', $invoiceNum);
|
||||
$body .= "\n\n";
|
||||
|
||||
if (!empty($import->invoice_number)) {
|
||||
$body .= $langs->trans('InvoiceNumber').': '.$import->invoice_number."\n";
|
||||
}
|
||||
if (!empty($import->seller_name)) {
|
||||
$body .= $langs->trans('Supplier').': '.$import->seller_name."\n";
|
||||
}
|
||||
if (!empty($filename)) {
|
||||
$body .= $langs->trans('File').': '.$filename."\n";
|
||||
}
|
||||
|
||||
$body .= "\n";
|
||||
$body .= $langs->trans('ErrorMessage').":\n";
|
||||
$body .= $errorMessage."\n";
|
||||
|
||||
if ($import->id > 0) {
|
||||
$body .= "\n";
|
||||
$body .= $langs->trans('NotifyLinkToImport').': '.dol_buildpath('/importzugferd/import.php', 2).'?action=edit&id='.$import->id;
|
||||
}
|
||||
|
||||
return $this->sendEmail($subject, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification for significant price differences
|
||||
*
|
||||
* @param ZugferdImport $import Import object
|
||||
* @param array $priceDiffs Array of price differences: array of ['line' => ImportLine, 'product' => Product, 'old_price' => float, 'new_price' => float, 'diff_percent' => float]
|
||||
* @return int 1 if sent, 0 if not needed, -1 on error
|
||||
*/
|
||||
public function sendPriceDifferenceNotification($import, $priceDiffs)
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
if (!$this->isEnabled() || !getDolGlobalString('IMPORTZUGFERD_NOTIFY_PRICE_DIFF')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (empty($priceDiffs)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
$threshold = getDolGlobalInt('IMPORTZUGFERD_PRICE_DIFF_THRESHOLD', 10);
|
||||
$subject = $langs->trans('NotifySubjectPriceDiff', $import->invoice_number, count($priceDiffs));
|
||||
|
||||
$body = $langs->trans('NotifyBodyPriceDiff', $import->invoice_number, $import->seller_name, $threshold);
|
||||
$body .= "\n\n";
|
||||
$body .= $langs->trans('InvoiceNumber').': '.$import->invoice_number."\n";
|
||||
$body .= $langs->trans('Supplier').': '.$import->seller_name."\n";
|
||||
$body .= $langs->trans('InvoiceDate').': '.dol_print_date($import->invoice_date, 'day')."\n";
|
||||
$body .= "\n";
|
||||
|
||||
// Table header
|
||||
$body .= str_pad($langs->trans('Product'), 40)." | ";
|
||||
$body .= str_pad($langs->trans('OldPrice'), 12)." | ";
|
||||
$body .= str_pad($langs->trans('NewPrice'), 12)." | ";
|
||||
$body .= str_pad($langs->trans('Difference'), 10)."\n";
|
||||
$body .= str_repeat('-', 80)."\n";
|
||||
|
||||
// List products with price differences
|
||||
foreach ($priceDiffs as $diff) {
|
||||
$productName = $diff['product']->ref.' - '.$diff['product']->label;
|
||||
if (strlen($productName) > 38) {
|
||||
$productName = substr($productName, 0, 35).'...';
|
||||
}
|
||||
|
||||
$oldPrice = price($diff['old_price']).' '.$import->currency;
|
||||
$newPrice = price($diff['new_price']).' '.$import->currency;
|
||||
$diffPercent = ($diff['diff_percent'] > 0 ? '+' : '').number_format($diff['diff_percent'], 1).'%';
|
||||
|
||||
$body .= str_pad($productName, 40)." | ";
|
||||
$body .= str_pad($oldPrice, 12)." | ";
|
||||
$body .= str_pad($newPrice, 12)." | ";
|
||||
$body .= str_pad($diffPercent, 10)."\n";
|
||||
}
|
||||
|
||||
$body .= "\n";
|
||||
$body .= $langs->trans('NotifyLinkToImport').': '.dol_buildpath('/importzugferd/import.php', 2).'?action=edit&id='.$import->id;
|
||||
|
||||
return $this->sendEmail($subject, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for price differences and send notification if needed
|
||||
*
|
||||
* @param ZugferdImport $import Import object
|
||||
* @param array $lines Import lines with fk_product set
|
||||
* @return int 1 if notification sent, 0 if not needed, -1 on error
|
||||
*/
|
||||
public function checkAndNotifyPriceDifferences($import, $lines)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
if (!$this->isEnabled() || !getDolGlobalString('IMPORTZUGFERD_NOTIFY_PRICE_DIFF')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$threshold = getDolGlobalInt('IMPORTZUGFERD_PRICE_DIFF_THRESHOLD', 10);
|
||||
$priceDiffs = array();
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if ($line->fk_product <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get current supplier price
|
||||
$productFourn = new ProductFournisseur($this->db);
|
||||
$result = $productFourn->find_min_price_product_fournisseur($line->fk_product, 1, $import->fk_soc);
|
||||
|
||||
if ($result > 0 && $productFourn->fourn_price > 0) {
|
||||
$oldPrice = $productFourn->fourn_price;
|
||||
$newPrice = $line->unit_price;
|
||||
|
||||
// Calculate percentage difference
|
||||
$diffPercent = (($newPrice - $oldPrice) / $oldPrice) * 100;
|
||||
|
||||
if (abs($diffPercent) >= $threshold) {
|
||||
$product = new Product($this->db);
|
||||
$product->fetch($line->fk_product);
|
||||
|
||||
$priceDiffs[] = array(
|
||||
'line' => $line,
|
||||
'product' => $product,
|
||||
'old_price' => $oldPrice,
|
||||
'new_price' => $newPrice,
|
||||
'diff_percent' => $diffPercent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($priceDiffs)) {
|
||||
return $this->sendPriceDifferenceNotification($import, $priceDiffs);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send test notification email
|
||||
*
|
||||
* @return int 1 if sent, -1 on error
|
||||
*/
|
||||
public function sendTestNotification()
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
if (!$this->isEnabled()) {
|
||||
$this->error = $langs->trans('NotificationsNotEnabled');
|
||||
return -1;
|
||||
}
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
$subject = $langs->trans('NotifySubjectTest');
|
||||
|
||||
$body = $langs->trans('NotifyBodyTest');
|
||||
$body .= "\n\n";
|
||||
$body .= $langs->trans('NotifyTestInfo')."\n\n";
|
||||
|
||||
// Show current notification settings
|
||||
$body .= $langs->trans('CurrentSettings').":\n";
|
||||
$body .= "- ".$langs->trans('NotifyEmail').": ".$this->getNotifyEmail()."\n";
|
||||
$body .= "- ".$langs->trans('IMPORTZUGFERD_NOTIFY_MANUAL').": ".(getDolGlobalString('IMPORTZUGFERD_NOTIFY_MANUAL') ? $langs->trans('Yes') : $langs->trans('No'))."\n";
|
||||
$body .= "- ".$langs->trans('IMPORTZUGFERD_NOTIFY_ERROR').": ".(getDolGlobalString('IMPORTZUGFERD_NOTIFY_ERROR') ? $langs->trans('Yes') : $langs->trans('No'))."\n";
|
||||
$body .= "- ".$langs->trans('IMPORTZUGFERD_NOTIFY_PRICE_DIFF').": ".(getDolGlobalString('IMPORTZUGFERD_NOTIFY_PRICE_DIFF') ? $langs->trans('Yes') : $langs->trans('No'))."\n";
|
||||
|
||||
if (getDolGlobalString('IMPORTZUGFERD_NOTIFY_PRICE_DIFF')) {
|
||||
$body .= "- ".$langs->trans('IMPORTZUGFERD_PRICE_DIFF_THRESHOLD').": ".getDolGlobalInt('IMPORTZUGFERD_PRICE_DIFF_THRESHOLD', 10)."%\n";
|
||||
}
|
||||
|
||||
$body .= "\n";
|
||||
$body .= $langs->trans('NotifyTestSuccess');
|
||||
|
||||
return $this->sendEmail($subject, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email using Dolibarr's mail system
|
||||
*
|
||||
* @param string $subject Email subject
|
||||
* @param string $body Email body (plain text)
|
||||
* @return int 1 if sent, -1 on error
|
||||
*/
|
||||
protected function sendEmail($subject, $body)
|
||||
{
|
||||
global $conf, $langs, $mysoc;
|
||||
|
||||
$to = $this->getNotifyEmail();
|
||||
if (empty($to)) {
|
||||
$this->error = 'No notification email configured';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get sender
|
||||
$from = getDolGlobalString('MAIN_MAIL_EMAIL_FROM');
|
||||
if (empty($from)) {
|
||||
$from = $mysoc->email;
|
||||
}
|
||||
if (empty($from)) {
|
||||
$this->error = 'No sender email configured';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Add module prefix to subject
|
||||
$subject = '[ZUGFeRD Import] '.$subject;
|
||||
|
||||
// Create mail object
|
||||
$mailfile = new CMailFile(
|
||||
$subject,
|
||||
$to,
|
||||
$from,
|
||||
$body,
|
||||
array(), // files
|
||||
array(), // mimefiles
|
||||
array(), // ccfiles
|
||||
'', // cc
|
||||
'', // bcc
|
||||
0, // deliveryreceipt
|
||||
0, // msgishtml
|
||||
'', // errors_to
|
||||
'', // css
|
||||
'', // trackid
|
||||
'', // moreinheader
|
||||
'standard', // sendcontext
|
||||
'' // replyto
|
||||
);
|
||||
|
||||
$result = $mailfile->sendfile();
|
||||
|
||||
if ($result) {
|
||||
dol_syslog("ImportNotification: Email sent to ".$to." - Subject: ".$subject, LOG_INFO);
|
||||
return 1;
|
||||
} else {
|
||||
$this->error = $mailfile->error;
|
||||
$this->errors = $mailfile->errors;
|
||||
dol_syslog("ImportNotification: Failed to send email - ".$this->error, LOG_ERR);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
478
class/productmapping.class.php
Executable file
478
class/productmapping.class.php
Executable file
|
|
@ -0,0 +1,478 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file class/productmapping.class.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Class for product mapping (supplier article numbers to products)
|
||||
*/
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
||||
|
||||
/**
|
||||
* Class ProductMapping
|
||||
* Maps supplier article numbers to Dolibarr products
|
||||
*/
|
||||
class ProductMapping extends CommonObject
|
||||
{
|
||||
/**
|
||||
* @var string ID to identify managed object
|
||||
*/
|
||||
public $element = 'productmapping';
|
||||
|
||||
/**
|
||||
* @var string Name of table without prefix
|
||||
*/
|
||||
public $table_element = 'importzugferd_productmapping';
|
||||
|
||||
/**
|
||||
* @var int Does object support multicompany
|
||||
*/
|
||||
public $ismultientitymanaged = 1;
|
||||
|
||||
/**
|
||||
* @var int Supplier ID
|
||||
*/
|
||||
public $fk_soc;
|
||||
|
||||
/**
|
||||
* @var string Supplier article number
|
||||
*/
|
||||
public $supplier_ref;
|
||||
|
||||
/**
|
||||
* @var int Product ID
|
||||
*/
|
||||
public $fk_product;
|
||||
|
||||
/**
|
||||
* @var string EAN/GTIN
|
||||
*/
|
||||
public $ean;
|
||||
|
||||
/**
|
||||
* @var string Manufacturer article number
|
||||
*/
|
||||
public $manufacturer_ref;
|
||||
|
||||
/**
|
||||
* @var string Description
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var int Priority
|
||||
*/
|
||||
public $priority = 0;
|
||||
|
||||
/**
|
||||
* @var int Active flag
|
||||
*/
|
||||
public $active = 1;
|
||||
|
||||
/**
|
||||
* @var string Date creation
|
||||
*/
|
||||
public $date_creation;
|
||||
|
||||
/**
|
||||
* @var int User creator
|
||||
*/
|
||||
public $fk_user_creat;
|
||||
|
||||
/**
|
||||
* @var int User modifier
|
||||
*/
|
||||
public $fk_user_modif;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create object into database
|
||||
*
|
||||
* @param User $user User that creates
|
||||
* @return int <0 if KO, Id of created object if OK
|
||||
*/
|
||||
public function create($user)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$this->entity = $conf->entity;
|
||||
|
||||
if (empty($this->date_creation)) {
|
||||
$this->date_creation = dol_now();
|
||||
}
|
||||
|
||||
$this->fk_user_creat = $user->id;
|
||||
|
||||
$sql = "INSERT INTO " . MAIN_DB_PREFIX . $this->table_element . " (";
|
||||
$sql .= "fk_soc, supplier_ref, fk_product, ean, manufacturer_ref,";
|
||||
$sql .= "description, priority, active, date_creation, fk_user_creat, entity";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= (int) $this->fk_soc . ",";
|
||||
$sql .= "'" . $this->db->escape($this->supplier_ref) . "',";
|
||||
$sql .= (int) $this->fk_product . ",";
|
||||
$sql .= "'" . $this->db->escape($this->ean) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->manufacturer_ref) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->description) . "',";
|
||||
$sql .= (int) $this->priority . ",";
|
||||
$sql .= (int) $this->active . ",";
|
||||
$sql .= "'" . $this->db->escape($this->db->idate($this->date_creation)) . "',";
|
||||
$sql .= (int) $this->fk_user_creat . ",";
|
||||
$sql .= (int) $this->entity;
|
||||
$sql .= ")";
|
||||
|
||||
dol_syslog(get_class($this) . "::create", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
|
||||
if (!$resql) {
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX . $this->table_element);
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load object in memory from database
|
||||
*
|
||||
* @param int $id Id object
|
||||
* @return int <0 if KO, 0 if not found, >0 if OK
|
||||
*/
|
||||
public function fetch($id)
|
||||
{
|
||||
$sql = "SELECT rowid, fk_soc, supplier_ref, fk_product, ean, manufacturer_ref,";
|
||||
$sql .= " description, priority, active, date_creation, tms, fk_user_creat, fk_user_modif, entity";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE rowid = " . (int) $id;
|
||||
|
||||
dol_syslog(get_class($this) . "::fetch", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
|
||||
if ($resql) {
|
||||
if ($this->db->num_rows($resql)) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
|
||||
$this->id = $obj->rowid;
|
||||
$this->fk_soc = $obj->fk_soc;
|
||||
$this->supplier_ref = $obj->supplier_ref;
|
||||
$this->fk_product = $obj->fk_product;
|
||||
$this->ean = $obj->ean;
|
||||
$this->manufacturer_ref = $obj->manufacturer_ref;
|
||||
$this->description = $obj->description;
|
||||
$this->priority = $obj->priority;
|
||||
$this->active = $obj->active;
|
||||
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||
$this->tms = $this->db->jdate($obj->tms);
|
||||
$this->fk_user_creat = $obj->fk_user_creat;
|
||||
$this->fk_user_modif = $obj->fk_user_modif;
|
||||
$this->entity = $obj->entity;
|
||||
|
||||
$this->db->free($resql);
|
||||
return 1;
|
||||
} else {
|
||||
$this->db->free($resql);
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update object in database
|
||||
*
|
||||
* @param User $user User that modifies
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function update($user)
|
||||
{
|
||||
$this->fk_user_modif = $user->id;
|
||||
|
||||
$sql = "UPDATE " . MAIN_DB_PREFIX . $this->table_element . " SET";
|
||||
$sql .= " fk_soc = " . (int) $this->fk_soc . ",";
|
||||
$sql .= " supplier_ref = '" . $this->db->escape($this->supplier_ref) . "',";
|
||||
$sql .= " fk_product = " . (int) $this->fk_product . ",";
|
||||
$sql .= " ean = '" . $this->db->escape($this->ean) . "',";
|
||||
$sql .= " manufacturer_ref = '" . $this->db->escape($this->manufacturer_ref) . "',";
|
||||
$sql .= " description = '" . $this->db->escape($this->description) . "',";
|
||||
$sql .= " priority = " . (int) $this->priority . ",";
|
||||
$sql .= " active = " . (int) $this->active . ",";
|
||||
$sql .= " fk_user_modif = " . (int) $this->fk_user_modif;
|
||||
$sql .= " WHERE rowid = " . (int) $this->id;
|
||||
|
||||
dol_syslog(get_class($this) . "::update", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
|
||||
if (!$resql) {
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete object from database
|
||||
*
|
||||
* @param User $user User that deletes
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function delete($user)
|
||||
{
|
||||
$sql = "DELETE FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE rowid = " . (int) $this->id;
|
||||
|
||||
dol_syslog(get_class($this) . "::delete", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
|
||||
if (!$resql) {
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find product by supplier reference
|
||||
*
|
||||
* @param int $fk_soc Supplier ID
|
||||
* @param string $supplier_ref Supplier article number
|
||||
* @return int Product ID or 0 if not found
|
||||
*/
|
||||
public function findProductBySupplierRef($fk_soc, $supplier_ref)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
// First check our mapping table
|
||||
$sql = "SELECT fk_product FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE fk_soc = " . (int) $fk_soc;
|
||||
$sql .= " AND supplier_ref = '" . $this->db->escape($supplier_ref) . "'";
|
||||
$sql .= " AND active = 1";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
$sql .= " ORDER BY priority DESC";
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->fk_product;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find product by EAN
|
||||
*
|
||||
* @param string $ean EAN/GTIN
|
||||
* @return int Product ID or 0 if not found
|
||||
*/
|
||||
public function findProductByEan($ean)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
if (empty($ean)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// First check our mapping table
|
||||
$sql = "SELECT fk_product FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE ean = '" . $this->db->escape($ean) . "'";
|
||||
$sql .= " AND active = 1";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->fk_product;
|
||||
}
|
||||
|
||||
// Check product barcode
|
||||
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product";
|
||||
$sql .= " WHERE barcode = '" . $this->db->escape($ean) . "'";
|
||||
$sql .= " AND entity IN (" . getEntity('product') . ")";
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->rowid;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find product by supplier price reference
|
||||
*
|
||||
* @param int $fk_soc Supplier ID
|
||||
* @param string $ref_fourn Supplier reference
|
||||
* @return int Product ID or 0 if not found
|
||||
*/
|
||||
public function findProductBySupplierPrice($fk_soc, $ref_fourn)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$sql = "SELECT fk_product FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sql .= " WHERE fk_soc = " . (int) $fk_soc;
|
||||
$sql .= " AND ref_fourn = '" . $this->db->escape($ref_fourn) . "'";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->fk_product;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find product using all available methods
|
||||
*
|
||||
* @param int $fk_soc Supplier ID
|
||||
* @param array $product_data Product data from ZUGFeRD (seller_id, buyer_id, global_id, name)
|
||||
* @return array Array with 'fk_product' and 'method' used
|
||||
*/
|
||||
public function findProduct($fk_soc, $product_data)
|
||||
{
|
||||
$result = array('fk_product' => 0, 'method' => '');
|
||||
|
||||
// 1. Check our mapping table with supplier reference
|
||||
if (!empty($product_data['seller_id'])) {
|
||||
$fk_product = $this->findProductBySupplierRef($fk_soc, $product_data['seller_id']);
|
||||
if ($fk_product > 0) {
|
||||
return array('fk_product' => $fk_product, 'method' => 'mapping_supplier_ref');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check supplier price table
|
||||
if (!empty($product_data['seller_id'])) {
|
||||
$fk_product = $this->findProductBySupplierPrice($fk_soc, $product_data['seller_id']);
|
||||
if ($fk_product > 0) {
|
||||
return array('fk_product' => $fk_product, 'method' => 'supplier_price');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check by EAN/GTIN
|
||||
if (!empty($product_data['global_id'])) {
|
||||
$fk_product = $this->findProductByEan($product_data['global_id']);
|
||||
if ($fk_product > 0) {
|
||||
return array('fk_product' => $fk_product, 'method' => 'ean');
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check buyer assigned ID (our article number)
|
||||
if (!empty($product_data['buyer_id'])) {
|
||||
global $conf;
|
||||
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product";
|
||||
$sql .= " WHERE ref = '" . $this->db->escape($product_data['buyer_id']) . "'";
|
||||
$sql .= " AND entity IN (" . getEntity('product') . ")";
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return array('fk_product' => (int) $obj->rowid, 'method' => 'buyer_ref');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all mappings for a supplier
|
||||
*
|
||||
* @param int $fk_soc Supplier ID
|
||||
* @param int $limit Limit results
|
||||
* @param int $offset Offset
|
||||
* @return array Array of mappings
|
||||
*/
|
||||
public function fetchAllBySupplier($fk_soc, $limit = 0, $offset = 0)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$mappings = array();
|
||||
|
||||
$sql = "SELECT pm.rowid, pm.fk_soc, pm.supplier_ref, pm.fk_product, pm.ean,";
|
||||
$sql .= " pm.manufacturer_ref, pm.description, pm.priority, pm.active,";
|
||||
$sql .= " p.ref as product_ref, p.label as product_label";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element . " as pm";
|
||||
$sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "product as p ON p.rowid = pm.fk_product";
|
||||
$sql .= " WHERE pm.fk_soc = " . (int) $fk_soc;
|
||||
$sql .= " AND pm.entity = " . (int) $conf->entity;
|
||||
$sql .= " ORDER BY pm.supplier_ref ASC";
|
||||
|
||||
if ($limit > 0) {
|
||||
$sql .= " LIMIT " . $limit;
|
||||
if ($offset > 0) {
|
||||
$sql .= " OFFSET " . $offset;
|
||||
}
|
||||
}
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$mappings[] = array(
|
||||
'id' => $obj->rowid,
|
||||
'fk_soc' => $obj->fk_soc,
|
||||
'supplier_ref' => $obj->supplier_ref,
|
||||
'fk_product' => $obj->fk_product,
|
||||
'product_ref' => $obj->product_ref,
|
||||
'product_label' => $obj->product_label,
|
||||
'ean' => $obj->ean,
|
||||
'manufacturer_ref' => $obj->manufacturer_ref,
|
||||
'description' => $obj->description,
|
||||
'priority' => $obj->priority,
|
||||
'active' => $obj->active,
|
||||
);
|
||||
}
|
||||
$this->db->free($resql);
|
||||
}
|
||||
|
||||
return $mappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count mappings for a supplier
|
||||
*
|
||||
* @param int $fk_soc Supplier ID
|
||||
* @return int Count
|
||||
*/
|
||||
public function countBySupplier($fk_soc)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$sql = "SELECT COUNT(*) as nb FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE fk_soc = " . (int) $fk_soc;
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->nb;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
795
class/zugferdimport.class.php
Executable file
795
class/zugferdimport.class.php
Executable file
|
|
@ -0,0 +1,795 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file class/zugferdimport.class.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Class for ZUGFeRD import records
|
||||
*/
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
||||
|
||||
/**
|
||||
* Class ZugferdImport
|
||||
* Manages imported ZUGFeRD invoices
|
||||
*/
|
||||
class ZugferdImport extends CommonObject
|
||||
{
|
||||
/**
|
||||
* @var string ID to identify managed object
|
||||
*/
|
||||
public $element = 'zugferdimport';
|
||||
|
||||
/**
|
||||
* @var string Name of table without prefix
|
||||
*/
|
||||
public $table_element = 'importzugferd_import';
|
||||
|
||||
/**
|
||||
* @var int Does object support multicompany
|
||||
*/
|
||||
public $ismultientitymanaged = 1;
|
||||
|
||||
/**
|
||||
* @var string Field with ID of parent key if object has a parent
|
||||
*/
|
||||
public $fk_element = 'fk_zugferdimport';
|
||||
|
||||
/**
|
||||
* @var array Fields definition
|
||||
*/
|
||||
public $fields = array(
|
||||
'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'position' => 1, 'notnull' => 1, 'visible' => 0, 'index' => 1),
|
||||
'ref' => array('type' => 'varchar(128)', 'label' => 'Ref', 'enabled' => 1, 'position' => 10, 'notnull' => 1, 'visible' => 4, 'index' => 1, 'searchall' => 1),
|
||||
'invoice_number' => array('type' => 'varchar(128)', 'label' => 'InvoiceNumber', 'enabled' => 1, 'position' => 20, 'notnull' => 1, 'visible' => 1, 'searchall' => 1),
|
||||
'invoice_date' => array('type' => 'date', 'label' => 'InvoiceDate', 'enabled' => 1, 'position' => 30, 'notnull' => 1, 'visible' => 1),
|
||||
'seller_name' => array('type' => 'varchar(255)', 'label' => 'SellerName', 'enabled' => 1, 'position' => 40, 'notnull' => 0, 'visible' => 1, 'searchall' => 1),
|
||||
'seller_vat' => array('type' => 'varchar(50)', 'label' => 'SellerVAT', 'enabled' => 1, 'position' => 50, 'notnull' => 0, 'visible' => 1),
|
||||
'buyer_reference' => array('type' => 'varchar(128)', 'label' => 'BuyerReference', 'enabled' => 1, 'position' => 60, 'notnull' => 0, 'visible' => 1),
|
||||
'total_ht' => array('type' => 'price', 'label' => 'TotalHT', 'enabled' => 1, 'position' => 70, 'notnull' => 0, 'visible' => 1),
|
||||
'total_ttc' => array('type' => 'price', 'label' => 'TotalTTC', 'enabled' => 1, 'position' => 80, 'notnull' => 0, 'visible' => 1),
|
||||
'currency' => array('type' => 'varchar(3)', 'label' => 'Currency', 'enabled' => 1, 'position' => 90, 'notnull' => 0, 'visible' => 1, 'default' => 'EUR'),
|
||||
'fk_soc' => array('type' => 'integer:Societe:societe/class/societe.class.php', 'label' => 'Supplier', 'enabled' => 1, 'position' => 100, 'notnull' => 0, 'visible' => 1),
|
||||
'fk_facture_fourn' => array('type' => 'integer:FactureFournisseur:fourn/class/fournisseur.facture.class.php', 'label' => 'SupplierInvoice', 'enabled' => 1, 'position' => 110, 'notnull' => 0, 'visible' => 1),
|
||||
'status' => array('type' => 'integer', 'label' => 'Status', 'enabled' => 1, 'position' => 500, 'notnull' => 1, 'visible' => 2, 'default' => 0, 'index' => 1, 'arrayofkeyval' => array(0 => 'Imported', 1 => 'Processed', 2 => 'Error')),
|
||||
'error_message' => array('type' => 'text', 'label' => 'ErrorMessage', 'enabled' => 1, 'position' => 510, 'notnull' => 0, 'visible' => 0),
|
||||
'file_hash' => array('type' => 'varchar(64)', 'label' => 'FileHash', 'enabled' => 1, 'position' => 520, 'notnull' => 0, 'visible' => 0),
|
||||
'pdf_filename' => array('type' => 'varchar(255)', 'label' => 'PDFFilename', 'enabled' => 1, 'position' => 530, 'notnull' => 0, 'visible' => 1),
|
||||
'date_creation' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'position' => 600, 'notnull' => 1, 'visible' => 2),
|
||||
'date_import' => array('type' => 'datetime', 'label' => 'DateImport', 'enabled' => 1, 'position' => 610, 'notnull' => 0, 'visible' => 2),
|
||||
'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'position' => 620, 'notnull' => 0, 'visible' => 0),
|
||||
'fk_user_creat' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserCreator', 'enabled' => 1, 'position' => 700, 'notnull' => 0, 'visible' => 0),
|
||||
'fk_user_modif' => array('type' => 'integer:User:User/class/user.class.php', 'label' => 'UserModifier', 'enabled' => 1, 'position' => 710, 'notnull' => 0, 'visible' => 0),
|
||||
'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportKey', 'enabled' => 1, 'position' => 800, 'notnull' => 0, 'visible' => 0),
|
||||
'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'position' => 900, 'notnull' => 1, 'visible' => 0, 'default' => 1, 'index' => 1),
|
||||
);
|
||||
|
||||
/**
|
||||
* @var string Ref
|
||||
*/
|
||||
public $ref;
|
||||
|
||||
/**
|
||||
* @var string Invoice number from ZUGFeRD
|
||||
*/
|
||||
public $invoice_number;
|
||||
|
||||
/**
|
||||
* @var string Invoice date
|
||||
*/
|
||||
public $invoice_date;
|
||||
|
||||
/**
|
||||
* @var string Seller name
|
||||
*/
|
||||
public $seller_name;
|
||||
|
||||
/**
|
||||
* @var string Seller VAT ID
|
||||
*/
|
||||
public $seller_vat;
|
||||
|
||||
/**
|
||||
* @var string Buyer reference (our customer number at supplier)
|
||||
*/
|
||||
public $buyer_reference;
|
||||
|
||||
/**
|
||||
* @var float Net total
|
||||
*/
|
||||
public $total_ht;
|
||||
|
||||
/**
|
||||
* @var float Gross total
|
||||
*/
|
||||
public $total_ttc;
|
||||
|
||||
/**
|
||||
* @var string Currency
|
||||
*/
|
||||
public $currency = 'EUR';
|
||||
|
||||
/**
|
||||
* @var int Supplier ID
|
||||
*/
|
||||
public $fk_soc;
|
||||
|
||||
/**
|
||||
* @var int Created supplier invoice ID
|
||||
*/
|
||||
public $fk_facture_fourn;
|
||||
|
||||
/**
|
||||
* @var string XML content
|
||||
*/
|
||||
public $xml_content;
|
||||
|
||||
/**
|
||||
* @var string PDF filename
|
||||
*/
|
||||
public $pdf_filename;
|
||||
|
||||
/**
|
||||
* @var string File hash for duplicate detection
|
||||
*/
|
||||
public $file_hash;
|
||||
|
||||
/**
|
||||
* @var int Status: 0=imported, 1=processed, 2=error
|
||||
*/
|
||||
public $status = 0;
|
||||
|
||||
/**
|
||||
* @var string Error message
|
||||
*/
|
||||
public $error_message;
|
||||
|
||||
/**
|
||||
* @var string Date creation
|
||||
*/
|
||||
public $date_creation;
|
||||
|
||||
/**
|
||||
* @var string Date import
|
||||
*/
|
||||
public $date_import;
|
||||
|
||||
/**
|
||||
* @var int User creator
|
||||
*/
|
||||
public $fk_user_creat;
|
||||
|
||||
/**
|
||||
* @var int User modifier
|
||||
*/
|
||||
public $fk_user_modif;
|
||||
|
||||
/**
|
||||
* @var string Import key
|
||||
*/
|
||||
public $import_key;
|
||||
|
||||
/**
|
||||
* @var array Parsed line items
|
||||
*/
|
||||
public $lines = array();
|
||||
|
||||
/**
|
||||
* Status constants
|
||||
*/
|
||||
const STATUS_IMPORTED = 0;
|
||||
const STATUS_PROCESSED = 1;
|
||||
const STATUS_ERROR = 2;
|
||||
const STATUS_PENDING = 3; // Pending manual product assignment
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
$this->db = $db;
|
||||
|
||||
if (empty($conf->global->MAIN_SHOW_TECHNICAL_ID) && isset($this->fields['rowid'])) {
|
||||
$this->fields['rowid']['visible'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create object into database
|
||||
*
|
||||
* @param User $user User that creates
|
||||
* @param bool $notrigger false=launch triggers, true=disable triggers
|
||||
* @return int <0 if KO, Id of created object if OK
|
||||
*/
|
||||
public function create($user, $notrigger = false)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$this->entity = $conf->entity;
|
||||
|
||||
if (empty($this->ref)) {
|
||||
$this->ref = $this->getNextRef();
|
||||
}
|
||||
|
||||
if (empty($this->date_creation)) {
|
||||
$this->date_creation = dol_now();
|
||||
}
|
||||
|
||||
$this->fk_user_creat = $user->id;
|
||||
|
||||
$sql = "INSERT INTO " . MAIN_DB_PREFIX . $this->table_element . " (";
|
||||
$sql .= "ref, invoice_number, invoice_date, seller_name, seller_vat, buyer_reference,";
|
||||
$sql .= "total_ht, total_ttc, currency, fk_soc, fk_facture_fourn,";
|
||||
$sql .= "xml_content, pdf_filename, file_hash, status, error_message,";
|
||||
$sql .= "date_creation, date_import, fk_user_creat, import_key, entity";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= "'" . $this->db->escape($this->ref) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->invoice_number) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->invoice_date) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->seller_name) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->seller_vat) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->buyer_reference) . "',";
|
||||
$sql .= price2num($this->total_ht) . ",";
|
||||
$sql .= price2num($this->total_ttc) . ",";
|
||||
$sql .= "'" . $this->db->escape($this->currency) . "',";
|
||||
$sql .= ($this->fk_soc > 0 ? $this->fk_soc : "null") . ",";
|
||||
$sql .= ($this->fk_facture_fourn > 0 ? $this->fk_facture_fourn : "null") . ",";
|
||||
// Normalize XML before storing (compact format without whitespace)
|
||||
$normalizedXml = self::normalizeXml($this->xml_content);
|
||||
$sql .= "'" . $this->db->escape($normalizedXml) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->pdf_filename) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->file_hash) . "',";
|
||||
$sql .= (int) $this->status . ",";
|
||||
$sql .= "'" . $this->db->escape($this->error_message) . "',";
|
||||
$sql .= "'" . $this->db->escape($this->db->idate($this->date_creation)) . "',";
|
||||
$sql .= ($this->date_import ? "'" . $this->db->escape($this->db->idate($this->date_import)) . "'" : "null") . ",";
|
||||
$sql .= (int) $this->fk_user_creat . ",";
|
||||
$sql .= "'" . $this->db->escape($this->import_key) . "',";
|
||||
$sql .= (int) $this->entity;
|
||||
$sql .= ")";
|
||||
|
||||
$this->db->begin();
|
||||
|
||||
dol_syslog(get_class($this) . "::create", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
|
||||
if (!$resql) {
|
||||
$this->error = $this->db->lasterror();
|
||||
$this->db->rollback();
|
||||
return -1;
|
||||
}
|
||||
|
||||
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX . $this->table_element);
|
||||
|
||||
$this->db->commit();
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load object in memory from database
|
||||
*
|
||||
* @param int $id Id object
|
||||
* @param string $ref Ref
|
||||
* @param string $file_hash File hash
|
||||
* @return int <0 if KO, 0 if not found, >0 if OK
|
||||
*/
|
||||
public function fetch($id, $ref = null, $file_hash = null)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$sql = "SELECT rowid, ref, invoice_number, invoice_date, seller_name, seller_vat, buyer_reference,";
|
||||
$sql .= " total_ht, total_ttc, currency, fk_soc, fk_facture_fourn,";
|
||||
$sql .= " xml_content, pdf_filename, file_hash, status, error_message,";
|
||||
$sql .= " date_creation, date_import, tms, fk_user_creat, fk_user_modif, import_key, entity";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE entity IN (" . getEntity($this->table_element) . ")";
|
||||
|
||||
if ($id) {
|
||||
$sql .= " AND rowid = " . (int) $id;
|
||||
} elseif ($ref) {
|
||||
$sql .= " AND ref = '" . $this->db->escape($ref) . "'";
|
||||
} elseif ($file_hash) {
|
||||
$sql .= " AND file_hash = '" . $this->db->escape($file_hash) . "'";
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
dol_syslog(get_class($this) . "::fetch", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
|
||||
if ($resql) {
|
||||
if ($this->db->num_rows($resql)) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
|
||||
$this->id = $obj->rowid;
|
||||
$this->ref = $obj->ref;
|
||||
$this->invoice_number = $obj->invoice_number;
|
||||
$this->invoice_date = $this->db->jdate($obj->invoice_date);
|
||||
$this->seller_name = $obj->seller_name;
|
||||
$this->seller_vat = $obj->seller_vat;
|
||||
$this->buyer_reference = $obj->buyer_reference;
|
||||
$this->total_ht = $obj->total_ht;
|
||||
$this->total_ttc = $obj->total_ttc;
|
||||
$this->currency = $obj->currency;
|
||||
$this->fk_soc = $obj->fk_soc;
|
||||
$this->fk_facture_fourn = $obj->fk_facture_fourn;
|
||||
$this->xml_content = $obj->xml_content;
|
||||
$this->pdf_filename = $obj->pdf_filename;
|
||||
$this->file_hash = $obj->file_hash;
|
||||
$this->status = $obj->status;
|
||||
$this->error_message = $obj->error_message;
|
||||
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||
$this->date_import = $this->db->jdate($obj->date_import);
|
||||
$this->tms = $this->db->jdate($obj->tms);
|
||||
$this->fk_user_creat = $obj->fk_user_creat;
|
||||
$this->fk_user_modif = $obj->fk_user_modif;
|
||||
$this->import_key = $obj->import_key;
|
||||
$this->entity = $obj->entity;
|
||||
|
||||
$this->db->free($resql);
|
||||
return 1;
|
||||
} else {
|
||||
$this->db->free($resql);
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update object in database
|
||||
*
|
||||
* @param User $user User that modifies
|
||||
* @param bool $notrigger false=launch triggers, true=disable triggers
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function update($user, $notrigger = false)
|
||||
{
|
||||
$this->fk_user_modif = $user->id;
|
||||
|
||||
$sql = "UPDATE " . MAIN_DB_PREFIX . $this->table_element . " SET";
|
||||
$sql .= " ref = '" . $this->db->escape($this->ref) . "',";
|
||||
$sql .= " invoice_number = '" . $this->db->escape($this->invoice_number) . "',";
|
||||
$sql .= " invoice_date = " . ($this->invoice_date ? "'" . $this->db->idate($this->invoice_date) . "'" : "null") . ",";
|
||||
$sql .= " seller_name = '" . $this->db->escape($this->seller_name) . "',";
|
||||
$sql .= " seller_vat = '" . $this->db->escape($this->seller_vat) . "',";
|
||||
$sql .= " buyer_reference = '" . $this->db->escape($this->buyer_reference) . "',";
|
||||
$sql .= " total_ht = " . price2num($this->total_ht) . ",";
|
||||
$sql .= " total_ttc = " . price2num($this->total_ttc) . ",";
|
||||
$sql .= " currency = '" . $this->db->escape($this->currency) . "',";
|
||||
$sql .= " fk_soc = " . ($this->fk_soc > 0 ? (int) $this->fk_soc : "null") . ",";
|
||||
$sql .= " fk_facture_fourn = " . ($this->fk_facture_fourn > 0 ? (int) $this->fk_facture_fourn : "null") . ",";
|
||||
$sql .= " status = " . (int) $this->status . ",";
|
||||
$sql .= " date_import = " . ($this->date_import ? "'" . $this->db->idate($this->date_import) . "'" : "null") . ",";
|
||||
$sql .= " error_message = " . ($this->error_message ? "'" . $this->db->escape($this->error_message) . "'" : "null") . ",";
|
||||
$sql .= " fk_user_modif = " . (int) $this->fk_user_modif;
|
||||
$sql .= " WHERE rowid = " . (int) $this->id;
|
||||
|
||||
dol_syslog(get_class($this) . "::update sql=" . $sql, LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
|
||||
if (!$resql) {
|
||||
$this->error = $this->db->lasterror();
|
||||
dol_syslog(get_class($this) . "::update error=" . $this->error, LOG_ERR);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete object from database
|
||||
*
|
||||
* @param User $user User that deletes
|
||||
* @param bool $notrigger false=launch triggers, true=disable triggers
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function delete($user, $notrigger = false)
|
||||
{
|
||||
$sql = "DELETE FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE rowid = " . (int) $this->id;
|
||||
|
||||
dol_syslog(get_class($this) . "::delete", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
|
||||
if (!$resql) {
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file already imported (duplicate detection)
|
||||
*
|
||||
* @param string $file_hash SHA256 hash of file
|
||||
* @return bool true if already exists
|
||||
*/
|
||||
public function isDuplicate($file_hash)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE file_hash = '" . $this->db->escape($file_hash) . "'";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next reference number
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNextRef()
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$sql = "SELECT MAX(CAST(SUBSTRING(ref, 4) AS UNSIGNED)) as maxref";
|
||||
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
|
||||
$sql .= " WHERE ref LIKE 'ZI-%'";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$num = $obj->maxref ? $obj->maxref + 1 : 1;
|
||||
return 'ZI-' . str_pad($num, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return 'ZI-000001';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize XML for database storage
|
||||
* Removes whitespace between tags to store compact XML
|
||||
*
|
||||
* @param string $xml XML content
|
||||
* @return string Normalized XML
|
||||
*/
|
||||
public static function normalizeXml($xml)
|
||||
{
|
||||
if (empty($xml)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$dom->formatOutput = false;
|
||||
|
||||
// Try to load XML
|
||||
if (@$dom->loadXML($xml)) {
|
||||
// Return compact XML without declaration
|
||||
$result = $dom->saveXML($dom->documentElement);
|
||||
return $result ? $result : $xml;
|
||||
}
|
||||
|
||||
// Fallback: just remove whitespace between tags
|
||||
return preg_replace('/>\s+</', '><', trim($xml));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format XML for display
|
||||
* Takes compact XML and formats it with proper indentation
|
||||
*
|
||||
* @param string $xml Compact XML content
|
||||
* @return string Formatted XML
|
||||
*/
|
||||
public static function formatXmlForDisplay($xml)
|
||||
{
|
||||
if (empty($xml)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Clean up any escaped newlines from old data (literal \n strings)
|
||||
$xml = str_replace('\n', '', $xml);
|
||||
$xml = str_replace('\r', '', $xml);
|
||||
$xml = str_replace('\t', '', $xml);
|
||||
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$dom->formatOutput = true;
|
||||
|
||||
if (@$dom->loadXML($xml)) {
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
// Fallback: return as-is
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a ZUGFeRD invoice from PDF file
|
||||
* This is the main entry point for batch/automated imports
|
||||
*
|
||||
* @param User $user User performing import
|
||||
* @param string $file_path Path to PDF file
|
||||
* @param bool $auto_create_invoice Whether to auto-create supplier invoice
|
||||
* @return int >0 (import ID) if OK, -2 if duplicate, <0 if error
|
||||
*/
|
||||
public function importFromFile($user, $file_path, $auto_create_invoice = false)
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
dol_include_once('/importzugferd/class/zugferdparser.class.php');
|
||||
dol_include_once('/importzugferd/class/productmapping.class.php');
|
||||
dol_include_once('/importzugferd/class/importline.class.php');
|
||||
dol_include_once('/importzugferd/class/importnotification.class.php');
|
||||
|
||||
// Parse PDF
|
||||
$parser = new ZugferdParser($this->db);
|
||||
$result = $parser->extractFromPdf($file_path);
|
||||
if ($result < 0) {
|
||||
$this->error = $parser->error;
|
||||
return -1;
|
||||
}
|
||||
|
||||
$result = $parser->parse();
|
||||
if ($result < 0) {
|
||||
$this->error = $parser->error;
|
||||
return -1;
|
||||
}
|
||||
|
||||
$invoice_data = $parser->getInvoiceData();
|
||||
|
||||
// Check for duplicates
|
||||
$file_hash = $parser->getFileHash($file_path);
|
||||
if ($this->isDuplicate($file_hash)) {
|
||||
$this->error = $langs->trans('ErrorDuplicateInvoice');
|
||||
return -2; // Duplicate
|
||||
}
|
||||
|
||||
// Find supplier
|
||||
$supplier_id = $this->findSupplier($invoice_data);
|
||||
|
||||
// Set import record data
|
||||
$this->invoice_number = $invoice_data['invoice_number'];
|
||||
$this->invoice_date = $invoice_data['invoice_date'];
|
||||
$this->seller_name = $invoice_data['seller']['name'];
|
||||
$this->seller_vat = $invoice_data['seller']['vat_id'];
|
||||
$this->buyer_reference = $invoice_data['buyer']['reference'] ?: $invoice_data['buyer']['id'];
|
||||
$this->total_ht = $invoice_data['totals']['net'];
|
||||
$this->total_ttc = $invoice_data['totals']['gross'];
|
||||
$this->currency = $invoice_data['totals']['currency'] ?: 'EUR';
|
||||
$this->fk_soc = $supplier_id;
|
||||
$this->xml_content = $parser->getXmlContent();
|
||||
$this->pdf_filename = basename($file_path);
|
||||
$this->file_hash = $file_hash;
|
||||
$this->date_import = dol_now();
|
||||
|
||||
// Create import record
|
||||
$import_id = $this->create($user);
|
||||
if ($import_id < 0) {
|
||||
return -3;
|
||||
}
|
||||
|
||||
// Process and store line items
|
||||
$mapping = new ProductMapping($this->db);
|
||||
$unmatched_count = 0;
|
||||
$matched_count = 0;
|
||||
$total_lines = count($invoice_data['lines']);
|
||||
|
||||
foreach ($invoice_data['lines'] as $line_data) {
|
||||
$line = new ImportLine($this->db);
|
||||
$line->fk_import = $import_id;
|
||||
$line->line_id = $line_data['line_id'];
|
||||
$line->supplier_ref = $line_data['product']['seller_id'];
|
||||
$line->product_name = $line_data['product']['name'];
|
||||
$line->description = $line_data['product']['description'];
|
||||
$line->quantity = $line_data['quantity'];
|
||||
$line->unit_code = $line_data['unit_code'];
|
||||
$line->unit_price = $line_data['unit_price'];
|
||||
$line->unit_price_raw = isset($line_data['unit_price_raw']) ? $line_data['unit_price_raw'] : $line_data['unit_price'];
|
||||
$line->basis_quantity = isset($line_data['basis_quantity']) ? $line_data['basis_quantity'] : 1;
|
||||
$line->basis_quantity_unit = isset($line_data['basis_quantity_unit']) ? $line_data['basis_quantity_unit'] : '';
|
||||
$line->line_total = $line_data['line_total'];
|
||||
$line->tax_percent = $line_data['tax_percent'];
|
||||
$line->ean = $line_data['product']['global_id'];
|
||||
|
||||
// Copper surcharge (Kupferzuschlag) from ZUGFeRD - always set (0 if not present)
|
||||
if (isset($line_data['copper_surcharge']) && $line_data['copper_surcharge'] > 0) {
|
||||
$line->copper_surcharge = $line_data['copper_surcharge'];
|
||||
$line->copper_surcharge_basis_qty = isset($line_data['copper_surcharge_basis_qty']) ? $line_data['copper_surcharge_basis_qty'] : $line->basis_quantity;
|
||||
} else {
|
||||
$line->copper_surcharge = 0;
|
||||
$line->copper_surcharge_basis_qty = $line->basis_quantity;
|
||||
}
|
||||
|
||||
// Try to match product
|
||||
$fk_product = 0;
|
||||
$match_method = '';
|
||||
|
||||
if ($supplier_id > 0) {
|
||||
$match = $mapping->findProduct($supplier_id, $line_data['product']);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$line->fk_product = $fk_product;
|
||||
$line->match_method = $match_method;
|
||||
|
||||
if ($fk_product == 0) {
|
||||
$unmatched_count++;
|
||||
} else {
|
||||
$matched_count++;
|
||||
}
|
||||
|
||||
$line->create($user);
|
||||
}
|
||||
|
||||
// Determine status based on matching results
|
||||
// STATUS_IMPORTED only if: supplier found, has lines, and ALL lines have matched products
|
||||
if ($supplier_id == 0 || $total_lines == 0 || $unmatched_count > 0 || $matched_count == 0) {
|
||||
// Missing supplier, no lines, unmatched products, or no matches at all - needs manual intervention
|
||||
$this->status = self::STATUS_PENDING;
|
||||
} else {
|
||||
// All lines matched
|
||||
$this->status = self::STATUS_IMPORTED;
|
||||
}
|
||||
|
||||
// Copy PDF to documents
|
||||
$destdir = $conf->importzugferd->dir_output . '/imports';
|
||||
if (!is_dir($destdir)) {
|
||||
dol_mkdir($destdir);
|
||||
}
|
||||
$destfile = $destdir . '/' . $this->ref . '_' . basename($file_path);
|
||||
copy($file_path, $destfile);
|
||||
|
||||
// Update status
|
||||
$this->update($user);
|
||||
|
||||
// Send notification if manual intervention required
|
||||
if ($this->status == self::STATUS_PENDING && class_exists('ImportNotification')) {
|
||||
$notification = new ImportNotification($this->db);
|
||||
$importLine = new ImportLine($this->db);
|
||||
$storedLines = $importLine->fetchAllByImport($this->id);
|
||||
$notification->sendManualInterventionNotification($this, $storedLines);
|
||||
}
|
||||
|
||||
return $import_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find supplier by buyer reference or VAT ID
|
||||
*
|
||||
* @param array $invoice_data Parsed invoice data
|
||||
* @return int Supplier ID or 0
|
||||
*/
|
||||
protected function findSupplier($invoice_data)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$buyer_ref = $invoice_data['buyer']['reference'] ?: $invoice_data['buyer']['id'];
|
||||
$seller_vat = $invoice_data['seller']['vat_id'];
|
||||
$seller_name = $invoice_data['seller']['name'];
|
||||
|
||||
// 1. Search by buyer reference in extrafield
|
||||
if (!empty($buyer_ref)) {
|
||||
$sql = "SELECT fk_object FROM " . MAIN_DB_PREFIX . "societe_extrafields";
|
||||
$sql .= " WHERE supplier_customer_number = '" . $this->db->escape($buyer_ref) . "'";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->fk_object;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Search by VAT ID
|
||||
if (!empty($seller_vat)) {
|
||||
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "societe";
|
||||
$sql .= " WHERE tva_intra = '" . $this->db->escape($seller_vat) . "'";
|
||||
$sql .= " AND fournisseur = 1";
|
||||
$sql .= " AND entity IN (" . getEntity('societe') . ")";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->rowid;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Search by name (fuzzy)
|
||||
if (!empty($seller_name)) {
|
||||
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "societe";
|
||||
$sql .= " WHERE (nom LIKE '" . $this->db->escape($seller_name) . "%'";
|
||||
$sql .= " OR nom LIKE '%" . $this->db->escape(substr($seller_name, 0, 20)) . "%')";
|
||||
$sql .= " AND fournisseur = 1";
|
||||
$sql .= " AND entity IN (" . getEntity('societe') . ")";
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->rowid;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status label
|
||||
*
|
||||
* @param int $mode 0=short, 1=long
|
||||
* @return string
|
||||
*/
|
||||
public function getLibStatut($mode = 0)
|
||||
{
|
||||
return $this->LibStatut($this->status, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return status label for a given status
|
||||
*
|
||||
* @param int $status Status
|
||||
* @param int $mode 0=short, 1=long
|
||||
* @return string
|
||||
*/
|
||||
public function LibStatut($status, $mode = 0)
|
||||
{
|
||||
global $langs;
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
$statusLabels = array(
|
||||
self::STATUS_IMPORTED => array('short' => 'Imported', 'long' => 'StatusImported', 'class' => 'status4'),
|
||||
self::STATUS_PROCESSED => array('short' => 'Processed', 'long' => 'StatusProcessed', 'class' => 'status6'),
|
||||
self::STATUS_ERROR => array('short' => 'Error', 'long' => 'StatusError', 'class' => 'status8'),
|
||||
self::STATUS_PENDING => array('short' => 'Pending', 'long' => 'StatusPending', 'class' => 'status1'),
|
||||
);
|
||||
|
||||
$statusType = isset($statusLabels[$status]) ? $statusLabels[$status] : $statusLabels[0];
|
||||
$label = $mode == 0 ? $statusType['short'] : $statusType['long'];
|
||||
|
||||
return dolGetStatus($langs->trans($label), '', '', $statusType['class']);
|
||||
}
|
||||
}
|
||||
645
class/zugferdparser.class.php
Executable file
645
class/zugferdparser.class.php
Executable file
|
|
@ -0,0 +1,645 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file class/zugferdparser.class.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Parser for ZUGFeRD/Factur-X XML invoices
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class ZugferdParser
|
||||
* Parses ZUGFeRD XML from PDF attachments
|
||||
*/
|
||||
class ZugferdParser
|
||||
{
|
||||
/**
|
||||
* @var DoliDB Database handler
|
||||
*/
|
||||
public $db;
|
||||
|
||||
/**
|
||||
* @var string Error message
|
||||
*/
|
||||
public $error = '';
|
||||
|
||||
/**
|
||||
* @var array Error messages
|
||||
*/
|
||||
public $errors = array();
|
||||
|
||||
/**
|
||||
* @var string XML content
|
||||
*/
|
||||
public $xml_content = '';
|
||||
|
||||
/**
|
||||
* @var SimpleXMLElement Parsed XML
|
||||
*/
|
||||
public $xml;
|
||||
|
||||
/**
|
||||
* @var array Parsed invoice data
|
||||
*/
|
||||
public $invoice_data = array();
|
||||
|
||||
/**
|
||||
* @var array Namespace prefixes
|
||||
*/
|
||||
private $namespaces = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract XML from PDF file
|
||||
*
|
||||
* @param string $pdf_path Path to PDF file
|
||||
* @return int 1 if OK, -1 if error
|
||||
*/
|
||||
public function extractFromPdf($pdf_path)
|
||||
{
|
||||
if (!file_exists($pdf_path)) {
|
||||
$this->error = 'File not found: ' . $pdf_path;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read PDF content
|
||||
$pdf_content = file_get_contents($pdf_path);
|
||||
if ($pdf_content === false) {
|
||||
$this->error = 'Cannot read PDF file';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Try to find embedded XML using different methods
|
||||
$xml = $this->extractXmlFromPdfContent($pdf_content);
|
||||
|
||||
if (empty($xml)) {
|
||||
// Try using pdfdetach command
|
||||
$xml = $this->extractXmlUsingPdfdetach($pdf_path);
|
||||
}
|
||||
|
||||
if (empty($xml)) {
|
||||
$this->error = 'No ZUGFeRD/Factur-X XML found in PDF';
|
||||
return -1;
|
||||
}
|
||||
|
||||
$this->xml_content = $xml;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract XML from PDF content by searching for XML patterns
|
||||
*
|
||||
* @param string $content PDF binary content
|
||||
* @return string|null XML content or null
|
||||
*/
|
||||
private function extractXmlFromPdfContent($content)
|
||||
{
|
||||
// Look for embedded file streams
|
||||
// ZUGFeRD XML typically starts with <?xml and contains CrossIndustryInvoice or CrossIndustryDocument
|
||||
|
||||
// Method 1: Look for FlateDecode streams and decompress
|
||||
$pattern = '/stream\s*(.*?)\s*endstream/s';
|
||||
preg_match_all($pattern, $content, $matches);
|
||||
|
||||
foreach ($matches[1] as $stream) {
|
||||
// Try to decompress
|
||||
$decompressed = @gzuncompress($stream);
|
||||
if ($decompressed === false) {
|
||||
$decompressed = @gzinflate($stream);
|
||||
}
|
||||
if ($decompressed === false) {
|
||||
$decompressed = $stream;
|
||||
}
|
||||
|
||||
// Check if it's XML
|
||||
if (strpos($decompressed, '<?xml') !== false &&
|
||||
(strpos($decompressed, 'CrossIndustryDocument') !== false ||
|
||||
strpos($decompressed, 'CrossIndustryInvoice') !== false)) {
|
||||
// Extract just the XML part
|
||||
$start = strpos($decompressed, '<?xml');
|
||||
$xml = substr($decompressed, $start);
|
||||
// Find the end
|
||||
if (preg_match('/<\/[a-z]+:CrossIndustry(Document|Invoice)>/i', $xml, $endMatch, PREG_OFFSET_CAPTURE)) {
|
||||
$xml = substr($xml, 0, $endMatch[0][1] + strlen($endMatch[0][0]));
|
||||
return $xml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract XML using pdfdetach command line tool
|
||||
*
|
||||
* @param string $pdf_path Path to PDF
|
||||
* @return string|null XML content or null
|
||||
*/
|
||||
private function extractXmlUsingPdfdetach($pdf_path)
|
||||
{
|
||||
$tmp_file = sys_get_temp_dir() . '/zugferd_' . uniqid() . '.xml';
|
||||
|
||||
// Try to extract first attachment
|
||||
$cmd = 'pdfdetach -save 1 -o ' . escapeshellarg($tmp_file) . ' ' . escapeshellarg($pdf_path) . ' 2>&1';
|
||||
exec($cmd, $output, $return_code);
|
||||
|
||||
if ($return_code === 0 && file_exists($tmp_file)) {
|
||||
$xml = file_get_contents($tmp_file);
|
||||
unlink($tmp_file);
|
||||
|
||||
if (strpos($xml, 'CrossIndustryDocument') !== false ||
|
||||
strpos($xml, 'CrossIndustryInvoice') !== false) {
|
||||
return $xml;
|
||||
}
|
||||
}
|
||||
|
||||
// Try listing and extracting by name
|
||||
$cmd = 'pdfdetach -list ' . escapeshellarg($pdf_path) . ' 2>&1';
|
||||
exec($cmd, $list_output, $return_code);
|
||||
|
||||
foreach ($list_output as $line) {
|
||||
if (preg_match('/(ZUGFeRD|factur-x|xrechnung)/i', $line)) {
|
||||
if (preg_match('/(\d+):/', $line, $matches)) {
|
||||
$idx = $matches[1];
|
||||
$cmd = 'pdfdetach -save ' . $idx . ' -o ' . escapeshellarg($tmp_file) . ' ' . escapeshellarg($pdf_path) . ' 2>&1';
|
||||
exec($cmd, $output, $return_code);
|
||||
|
||||
if ($return_code === 0 && file_exists($tmp_file)) {
|
||||
$xml = file_get_contents($tmp_file);
|
||||
unlink($tmp_file);
|
||||
return $xml;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the XML content
|
||||
*
|
||||
* @param string $xml_content Optional XML content, uses $this->xml_content if not provided
|
||||
* @return int 1 if OK, -1 if error
|
||||
*/
|
||||
public function parse($xml_content = null)
|
||||
{
|
||||
if ($xml_content !== null) {
|
||||
$this->xml_content = $xml_content;
|
||||
}
|
||||
|
||||
if (empty($this->xml_content)) {
|
||||
$this->error = 'No XML content to parse';
|
||||
return -1;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$this->xml = simplexml_load_string($this->xml_content);
|
||||
|
||||
if ($this->xml === false) {
|
||||
$errors = libxml_get_errors();
|
||||
$this->error = 'XML parse error: ' . ($errors[0]->message ?? 'Unknown error');
|
||||
libxml_clear_errors();
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get namespaces
|
||||
$this->namespaces = $this->xml->getNamespaces(true);
|
||||
|
||||
// Determine ZUGFeRD version and parse accordingly
|
||||
if ($this->isZugferdV1()) {
|
||||
return $this->parseZugferdV1();
|
||||
} elseif ($this->isZugferdV2()) {
|
||||
return $this->parseZugferdV2();
|
||||
} else {
|
||||
$this->error = 'Unknown ZUGFeRD/Factur-X format';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ZUGFeRD v1 format
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isZugferdV1()
|
||||
{
|
||||
return strpos($this->xml_content, 'CrossIndustryDocument') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ZUGFeRD v2 / Factur-X format
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isZugferdV2()
|
||||
{
|
||||
return strpos($this->xml_content, 'CrossIndustryInvoice') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ZUGFeRD v1 format
|
||||
*
|
||||
* @return int 1 if OK, -1 if error
|
||||
*/
|
||||
private function parseZugferdV1()
|
||||
{
|
||||
$this->xml->registerXPathNamespace('rsm', 'urn:ferd:CrossIndustryDocument:invoice:1p0');
|
||||
$this->xml->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12');
|
||||
$this->xml->registerXPathNamespace('udt', 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15');
|
||||
|
||||
$data = array();
|
||||
|
||||
// Header information
|
||||
$header = $this->xml->xpath('//rsm:HeaderExchangedDocument');
|
||||
if (!empty($header)) {
|
||||
$data['invoice_number'] = $this->getXpathValue('//rsm:HeaderExchangedDocument/ram:ID');
|
||||
$data['invoice_type'] = $this->getXpathValue('//rsm:HeaderExchangedDocument/ram:TypeCode');
|
||||
$data['invoice_name'] = $this->getXpathValue('//rsm:HeaderExchangedDocument/ram:Name');
|
||||
|
||||
$dateStr = $this->getXpathValue('//rsm:HeaderExchangedDocument/ram:IssueDateTime/udt:DateTimeString');
|
||||
$data['invoice_date'] = $this->parseDate($dateStr);
|
||||
}
|
||||
|
||||
// Seller (Lieferant)
|
||||
$data['seller'] = array(
|
||||
'name' => $this->getXpathValue('//ram:SellerTradeParty/ram:Name'),
|
||||
'global_id' => $this->getXpathValue('//ram:SellerTradeParty/ram:GlobalID'),
|
||||
'vat_id' => $this->getXpathValue('//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]'),
|
||||
'address' => array(
|
||||
'street' => $this->getXpathValue('//ram:SellerTradeParty/ram:PostalTradeAddress/ram:LineOne'),
|
||||
'postcode' => $this->getXpathValue('//ram:SellerTradeParty/ram:PostalTradeAddress/ram:PostcodeCode'),
|
||||
'city' => $this->getXpathValue('//ram:SellerTradeParty/ram:PostalTradeAddress/ram:CityName'),
|
||||
'country' => $this->getXpathValue('//ram:SellerTradeParty/ram:PostalTradeAddress/ram:CountryID'),
|
||||
)
|
||||
);
|
||||
|
||||
// Buyer (Käufer - wir)
|
||||
$data['buyer'] = array(
|
||||
'id' => $this->getXpathValue('//ram:BuyerTradeParty/ram:ID'),
|
||||
'reference' => $this->getXpathValue('//ram:ApplicableSupplyChainTradeAgreement/ram:BuyerReference'),
|
||||
'name' => $this->getXpathValue('//ram:BuyerTradeParty/ram:Name'),
|
||||
);
|
||||
|
||||
// Totals
|
||||
$data['totals'] = array(
|
||||
'net' => (float) $this->getXpathValue('//ram:SpecifiedTradeSettlementMonetarySummation/ram:LineTotalAmount'),
|
||||
'tax' => (float) $this->getXpathValue('//ram:SpecifiedTradeSettlementMonetarySummation/ram:TaxTotalAmount'),
|
||||
'gross' => (float) $this->getXpathValue('//ram:SpecifiedTradeSettlementMonetarySummation/ram:GrandTotalAmount'),
|
||||
'currency' => $this->getXpathValue('//ram:ApplicableSupplyChainTradeSettlement/ram:InvoiceCurrencyCode'),
|
||||
);
|
||||
|
||||
// Due date
|
||||
$dueDateStr = $this->getXpathValue('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
|
||||
$data['due_date'] = $this->parseDate($dueDateStr);
|
||||
|
||||
// Line items
|
||||
$data['lines'] = array();
|
||||
$lines = $this->xml->xpath('//ram:IncludedSupplyChainTradeLineItem');
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12');
|
||||
|
||||
// Get price and basis quantity for correct unit price calculation
|
||||
$chargeAmount = (float) $this->getNodeValue($line->xpath('ram:SpecifiedSupplyChainTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount'));
|
||||
$basisQuantity = (float) $this->getNodeValue($line->xpath('ram:SpecifiedSupplyChainTradeAgreement/ram:NetPriceProductTradePrice/ram:BasisQuantity'));
|
||||
$basisQuantityUnit = (string) $this->getNodeAttribute($line->xpath('ram:SpecifiedSupplyChainTradeAgreement/ram:NetPriceProductTradePrice/ram:BasisQuantity'), 'unitCode');
|
||||
|
||||
// Calculate real unit price: if BasisQuantity is e.g. 100 (meters), price is for 100 units
|
||||
if ($basisQuantity > 0 && $basisQuantity != 1) {
|
||||
$unitPrice = $chargeAmount / $basisQuantity;
|
||||
} else {
|
||||
$unitPrice = $chargeAmount;
|
||||
}
|
||||
|
||||
// Extract copper surcharge (Kupferzuschlag) from AppliedTradeAllowanceCharge
|
||||
$copperSurcharge = null;
|
||||
$copperSurchargeBasisQty = null;
|
||||
$allowanceCharges = $line->xpath('ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:AppliedTradeAllowanceCharge');
|
||||
foreach ($allowanceCharges as $charge) {
|
||||
$charge->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12');
|
||||
$reason = (string) $this->getNodeValue($charge->xpath('ram:Reason'));
|
||||
if (stripos($reason, 'Kupfer') !== false || stripos($reason, 'copper') !== false || stripos($reason, 'Metall') !== false) {
|
||||
$copperSurcharge = (float) $this->getNodeValue($charge->xpath('ram:ActualAmount'));
|
||||
$copperSurchargeBasisQty = (float) $this->getNodeValue($charge->xpath('ram:BasisQuantity'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check ApplicableProductCharacteristic for copper surcharge
|
||||
if ($copperSurcharge === null) {
|
||||
$characteristics = $line->xpath('ram:SpecifiedTradeProduct/ram:ApplicableProductCharacteristic');
|
||||
foreach ($characteristics as $char) {
|
||||
$char->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12');
|
||||
$desc = (string) $this->getNodeValue($char->xpath('ram:Description'));
|
||||
if (stripos($desc, 'Kupfer') !== false || stripos($desc, 'copper') !== false || stripos($desc, 'Metall') !== false) {
|
||||
$copperSurcharge = (float) $this->getNodeValue($char->xpath('ram:Value'));
|
||||
// Usually refers to same basis quantity as the price
|
||||
$copperSurchargeBasisQty = $basisQuantity ?: 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate copper surcharge per single unit
|
||||
$copperSurchargePerUnit = null;
|
||||
if ($copperSurcharge !== null && $copperSurcharge > 0) {
|
||||
if ($copperSurchargeBasisQty > 0 && $copperSurchargeBasisQty != 1) {
|
||||
$copperSurchargePerUnit = $copperSurcharge / $copperSurchargeBasisQty;
|
||||
} else {
|
||||
$copperSurchargePerUnit = $copperSurcharge;
|
||||
}
|
||||
}
|
||||
|
||||
$lineData = array(
|
||||
'line_id' => (string) $this->getNodeValue($line->xpath('ram:AssociatedDocumentLineDocument/ram:LineID')),
|
||||
'product' => array(
|
||||
'seller_id' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:SellerAssignedID')),
|
||||
'buyer_id' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:BuyerAssignedID')),
|
||||
'global_id' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:GlobalID')),
|
||||
'name' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:Name')),
|
||||
'description' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:Description')),
|
||||
),
|
||||
'quantity' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedSupplyChainTradeDelivery/ram:BilledQuantity')),
|
||||
'unit_code' => (string) $this->getNodeAttribute($line->xpath('ram:SpecifiedSupplyChainTradeDelivery/ram:BilledQuantity'), 'unitCode'),
|
||||
'unit_price' => $unitPrice,
|
||||
'unit_price_raw' => $chargeAmount,
|
||||
'basis_quantity' => $basisQuantity ?: 1,
|
||||
'basis_quantity_unit' => $basisQuantityUnit,
|
||||
'line_total' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedSupplyChainTradeSettlement/ram:SpecifiedTradeSettlementMonetarySummation/ram:LineTotalAmount')),
|
||||
'tax_percent' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:ApplicablePercent')),
|
||||
// Copper surcharge data
|
||||
'copper_surcharge' => $copperSurcharge,
|
||||
'copper_surcharge_basis_qty' => $copperSurchargeBasisQty,
|
||||
'copper_surcharge_per_unit' => $copperSurchargePerUnit,
|
||||
);
|
||||
|
||||
$data['lines'][] = $lineData;
|
||||
}
|
||||
|
||||
$this->invoice_data = $data;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ZUGFeRD v2 / Factur-X format
|
||||
*
|
||||
* @return int 1 if OK, -1 if error
|
||||
*/
|
||||
private function parseZugferdV2()
|
||||
{
|
||||
$this->xml->registerXPathNamespace('rsm', 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100');
|
||||
$this->xml->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');
|
||||
$this->xml->registerXPathNamespace('qdt', 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100');
|
||||
$this->xml->registerXPathNamespace('udt', 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100');
|
||||
|
||||
$data = array();
|
||||
|
||||
// Header information
|
||||
$data['invoice_number'] = $this->getXpathValue('//rsm:ExchangedDocument/ram:ID');
|
||||
$data['invoice_type'] = $this->getXpathValue('//rsm:ExchangedDocument/ram:TypeCode');
|
||||
$data['invoice_name'] = $this->getXpathValue('//rsm:ExchangedDocument/ram:Name');
|
||||
|
||||
$dateStr = $this->getXpathValue('//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString');
|
||||
$data['invoice_date'] = $this->parseDate($dateStr);
|
||||
|
||||
// Seller (Lieferant)
|
||||
$data['seller'] = array(
|
||||
'name' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name'),
|
||||
'global_id' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:GlobalID'),
|
||||
'vat_id' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID'),
|
||||
'address' => array(
|
||||
'street' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:PostalTradeAddress/ram:LineOne'),
|
||||
'postcode' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:PostalTradeAddress/ram:PostcodeCode'),
|
||||
'city' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:PostalTradeAddress/ram:CityName'),
|
||||
'country' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:PostalTradeAddress/ram:CountryID'),
|
||||
)
|
||||
);
|
||||
|
||||
// Buyer (Käufer - wir)
|
||||
$data['buyer'] = array(
|
||||
'id' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:ID'),
|
||||
'reference' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:BuyerReference'),
|
||||
'name' => $this->getXpathValue('//ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:Name'),
|
||||
);
|
||||
|
||||
// Totals
|
||||
$data['totals'] = array(
|
||||
'net' => (float) $this->getXpathValue('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:LineTotalAmount'),
|
||||
'tax' => (float) $this->getXpathValue('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxTotalAmount'),
|
||||
'gross' => (float) $this->getXpathValue('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount'),
|
||||
'currency' => $this->getXpathValue('//ram:ApplicableHeaderTradeSettlement/ram:InvoiceCurrencyCode'),
|
||||
);
|
||||
|
||||
// Due date
|
||||
$dueDateStr = $this->getXpathValue('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
|
||||
$data['due_date'] = $this->parseDate($dueDateStr);
|
||||
|
||||
// Line items
|
||||
$data['lines'] = array();
|
||||
$lines = $this->xml->xpath('//ram:IncludedSupplyChainTradeLineItem');
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');
|
||||
|
||||
// Get price and basis quantity for correct unit price calculation
|
||||
$chargeAmount = (float) $this->getNodeValue($line->xpath('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount'));
|
||||
$basisQuantity = (float) $this->getNodeValue($line->xpath('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:BasisQuantity'));
|
||||
$basisQuantityUnit = (string) $this->getNodeAttribute($line->xpath('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:BasisQuantity'), 'unitCode');
|
||||
|
||||
// Calculate real unit price: if BasisQuantity is e.g. 100 (meters), price is for 100 units
|
||||
if ($basisQuantity > 0 && $basisQuantity != 1) {
|
||||
$unitPrice = $chargeAmount / $basisQuantity;
|
||||
} else {
|
||||
$unitPrice = $chargeAmount;
|
||||
}
|
||||
|
||||
// Extract copper surcharge (Kupferzuschlag) from AppliedTradeAllowanceCharge (v2)
|
||||
$copperSurcharge = null;
|
||||
$copperSurchargeBasisQty = null;
|
||||
$allowanceCharges = $line->xpath('ram:SpecifiedLineTradeAgreement/ram:GrossPriceProductTradePrice/ram:AppliedTradeAllowanceCharge');
|
||||
foreach ($allowanceCharges as $charge) {
|
||||
$charge->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');
|
||||
$reason = (string) $this->getNodeValue($charge->xpath('ram:Reason'));
|
||||
if (stripos($reason, 'Kupfer') !== false || stripos($reason, 'copper') !== false || stripos($reason, 'Metall') !== false) {
|
||||
$copperSurcharge = (float) $this->getNodeValue($charge->xpath('ram:ActualAmount'));
|
||||
$copperSurchargeBasisQty = (float) $this->getNodeValue($charge->xpath('ram:BasisQuantity'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check ApplicableProductCharacteristic for copper surcharge (v2)
|
||||
if ($copperSurcharge === null) {
|
||||
$characteristics = $line->xpath('ram:SpecifiedTradeProduct/ram:ApplicableProductCharacteristic');
|
||||
foreach ($characteristics as $char) {
|
||||
$char->registerXPathNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');
|
||||
$desc = (string) $this->getNodeValue($char->xpath('ram:Description'));
|
||||
if (stripos($desc, 'Kupfer') !== false || stripos($desc, 'copper') !== false || stripos($desc, 'Metall') !== false) {
|
||||
$copperSurcharge = (float) $this->getNodeValue($char->xpath('ram:Value'));
|
||||
$copperSurchargeBasisQty = $basisQuantity ?: 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate copper surcharge per single unit
|
||||
$copperSurchargePerUnit = null;
|
||||
if ($copperSurcharge !== null && $copperSurcharge > 0) {
|
||||
if ($copperSurchargeBasisQty > 0 && $copperSurchargeBasisQty != 1) {
|
||||
$copperSurchargePerUnit = $copperSurcharge / $copperSurchargeBasisQty;
|
||||
} else {
|
||||
$copperSurchargePerUnit = $copperSurcharge;
|
||||
}
|
||||
}
|
||||
|
||||
$lineData = array(
|
||||
'line_id' => (string) $this->getNodeValue($line->xpath('ram:AssociatedDocumentLineDocument/ram:LineID')),
|
||||
'product' => array(
|
||||
'seller_id' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:SellerAssignedID')),
|
||||
'buyer_id' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:BuyerAssignedID')),
|
||||
'global_id' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:GlobalID')),
|
||||
'name' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:Name')),
|
||||
'description' => (string) $this->getNodeValue($line->xpath('ram:SpecifiedTradeProduct/ram:Description')),
|
||||
),
|
||||
'quantity' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity')),
|
||||
'unit_code' => (string) $this->getNodeAttribute($line->xpath('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity'), 'unitCode'),
|
||||
'unit_price' => $unitPrice,
|
||||
'unit_price_raw' => $chargeAmount,
|
||||
'basis_quantity' => $basisQuantity ?: 1,
|
||||
'basis_quantity_unit' => $basisQuantityUnit,
|
||||
'line_total' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount')),
|
||||
'tax_percent' => (float) $this->getNodeValue($line->xpath('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent')),
|
||||
// Copper surcharge data
|
||||
'copper_surcharge' => $copperSurcharge,
|
||||
'copper_surcharge_basis_qty' => $copperSurchargeBasisQty,
|
||||
'copper_surcharge_per_unit' => $copperSurchargePerUnit,
|
||||
);
|
||||
|
||||
$data['lines'][] = $lineData;
|
||||
}
|
||||
|
||||
$this->invoice_data = $data;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from XPath result
|
||||
*
|
||||
* @param string $xpath XPath expression
|
||||
* @return string
|
||||
*/
|
||||
private function getXpathValue($xpath)
|
||||
{
|
||||
$result = $this->xml->xpath($xpath);
|
||||
if (!empty($result)) {
|
||||
return trim((string) $result[0]);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from node array
|
||||
*
|
||||
* @param array $nodes XPath result array
|
||||
* @return string
|
||||
*/
|
||||
private function getNodeValue($nodes)
|
||||
{
|
||||
if (!empty($nodes) && isset($nodes[0])) {
|
||||
return trim((string) $nodes[0]);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attribute from node
|
||||
*
|
||||
* @param array $nodes XPath result array
|
||||
* @param string $attr Attribute name
|
||||
* @return string
|
||||
*/
|
||||
private function getNodeAttribute($nodes, $attr)
|
||||
{
|
||||
if (!empty($nodes) && isset($nodes[0])) {
|
||||
$attributes = $nodes[0]->attributes();
|
||||
if (isset($attributes[$attr])) {
|
||||
return (string) $attributes[$attr];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse date string in format YYYYMMDD or ISO
|
||||
*
|
||||
* @param string $dateStr Date string
|
||||
* @return string Date in Y-m-d format
|
||||
*/
|
||||
private function parseDate($dateStr)
|
||||
{
|
||||
if (empty($dateStr)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Format: YYYYMMDD
|
||||
if (preg_match('/^(\d{4})(\d{2})(\d{2})$/', $dateStr, $matches)) {
|
||||
return $matches[1] . '-' . $matches[2] . '-' . $matches[3];
|
||||
}
|
||||
|
||||
// Format: YYYY-MM-DD or ISO
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateStr, $matches)) {
|
||||
return $matches[1] . '-' . $matches[2] . '-' . $matches[3];
|
||||
}
|
||||
|
||||
return $dateStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file hash for duplicate detection
|
||||
*
|
||||
* @param string $file_path Path to file
|
||||
* @return string SHA256 hash
|
||||
*/
|
||||
public function getFileHash($file_path)
|
||||
{
|
||||
if (!file_exists($file_path)) {
|
||||
return '';
|
||||
}
|
||||
return hash_file('sha256', $file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getInvoiceData()
|
||||
{
|
||||
return $this->invoice_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get XML content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getXmlContent()
|
||||
{
|
||||
return $this->xml_content;
|
||||
}
|
||||
}
|
||||
98
core/boxes/box_new_products.php
Executable file
98
core/boxes/box_new_products.php
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
include_once DOL_DOCUMENT_ROOT.'/core/boxes/modules_boxes.php';
|
||||
|
||||
class box_new_products extends ModeleBoxes
|
||||
{
|
||||
public $boxcode = "newproductsreview";
|
||||
public $boximg = "product";
|
||||
public $boxlabel = "BoxNewProductsToReview";
|
||||
public $depends = array("product", "importzugferd");
|
||||
|
||||
public function __construct($db, $param = '')
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
public function loadBox($max = 5)
|
||||
{
|
||||
global $langs;
|
||||
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
// Auf Produktseite alle Einträge zeigen
|
||||
if (strpos($_SERVER['PHP_SELF'], '/product/index.php') !== false) {
|
||||
$max = 0; // 0 = kein Limit
|
||||
}
|
||||
|
||||
include_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
$productstatic = new Product($this->db);
|
||||
|
||||
// Anzahl zählen
|
||||
$sql = "SELECT COUNT(*) as total FROM ".MAIN_DB_PREFIX."product WHERE ref LIKE 'New%'";
|
||||
$resql = $this->db->query($sql);
|
||||
$total = 0;
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$total = $obj->total;
|
||||
}
|
||||
|
||||
$this->info_box_head = array(
|
||||
'text' => $langs->trans("BoxNewProductsToReview").' <span class="badge">'.$total.'</span>',
|
||||
'sublink' => dol_buildpath('/importzugferd/new_products.php', 1),
|
||||
'subtext' => $langs->trans("ShowAll"),
|
||||
'subpicto' => 'object_product',
|
||||
);
|
||||
|
||||
// Produkte laden
|
||||
$sql = "SELECT rowid, ref, label, datec FROM ".MAIN_DB_PREFIX."product";
|
||||
$sql .= " WHERE ref LIKE 'New%'";
|
||||
$sql .= " ORDER BY datec DESC";
|
||||
if ($max > 0) {
|
||||
$sql .= " LIMIT ".((int) $max);
|
||||
}
|
||||
|
||||
$result = $this->db->query($sql);
|
||||
if ($result) {
|
||||
$num = $this->db->num_rows($result);
|
||||
$line = 0;
|
||||
|
||||
while ($line < $num) {
|
||||
$objp = $this->db->fetch_object($result);
|
||||
|
||||
$productstatic->id = $objp->rowid;
|
||||
$productstatic->ref = $objp->ref;
|
||||
$productstatic->label = $objp->label;
|
||||
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="tdoverflowmax150"',
|
||||
'text' => $productstatic->getNomUrl(1),
|
||||
'asis' => 1,
|
||||
);
|
||||
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="tdoverflowmax150"',
|
||||
'text' => $objp->label,
|
||||
);
|
||||
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="right"',
|
||||
'text' => dol_print_date($this->db->jdate($objp->datec), 'day'),
|
||||
);
|
||||
|
||||
$line++;
|
||||
}
|
||||
|
||||
if ($num == 0) {
|
||||
$this->info_box_contents[0][0] = array(
|
||||
'td' => 'class="center"',
|
||||
'text' => $langs->trans("NoNewProductsToReview"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function showBox($head = null, $contents = null, $nooutput = 0)
|
||||
{
|
||||
return parent::showBox($this->info_box_head, $this->info_box_contents, $nooutput);
|
||||
}
|
||||
}
|
||||
787
core/modules/modImportZugferd.class.php
Executable file
787
core/modules/modImportZugferd.class.php
Executable file
|
|
@ -0,0 +1,787 @@
|
|||
<?php
|
||||
/* Copyright (C) 2004-2018 Laurent Destailleur <eldy@users.sourceforge.net>
|
||||
* Copyright (C) 2018-2019 Nicolas ZABOURI <info@inovea-conseil.com>
|
||||
* Copyright (C) 2019-2024 Frédéric France <frederic.france@free.fr>
|
||||
* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \defgroup importzugferd Module ImportZugferd
|
||||
* \brief ImportZugferd module descriptor.
|
||||
*
|
||||
* \file htdocs/importzugferd/core/modules/modImportZugferd.class.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Description and activation file for module ImportZugferd
|
||||
*/
|
||||
include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php';
|
||||
|
||||
|
||||
/**
|
||||
* Description and activation class for module ImportZugferd
|
||||
*/
|
||||
class modImportZugferd extends DolibarrModules
|
||||
{
|
||||
/**
|
||||
* Constructor. Define names, constants, directories, boxes, permissions
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
$this->db = $db;
|
||||
|
||||
// Id for module (must be unique).
|
||||
// Use here a free id (See in Home -> System information -> Dolibarr for list of used modules id).
|
||||
$this->numero = 500016; // TODO Go on page https://wiki.dolibarr.org/index.php/List_of_modules_id to reserve an id number for your module
|
||||
|
||||
// Key text used to identify module (for permissions, menus, etc...)
|
||||
$this->rights_class = 'importzugferd';
|
||||
|
||||
// Family can be 'base' (core modules),'crm','financial','hr','projects','products','ecm','technic' (transverse modules),'interface' (link with external tools),'other','...'
|
||||
// It is used to group modules by family in module setup page
|
||||
$this->family = "other";
|
||||
|
||||
// Module position in the family on 2 digits ('01', '10', '20', ...)
|
||||
$this->module_position = '90';
|
||||
|
||||
// Gives the possibility for the module, to provide his own family info and position of this family (Overwrite $this->family and $this->module_position. Avoid this)
|
||||
//$this->familyinfo = array('myownfamily' => array('position' => '01', 'label' => $langs->trans("MyOwnFamily")));
|
||||
// Module label (no space allowed), used if translation string 'ModuleImportZugferdName' not found (ImportZugferd is name of module).
|
||||
$this->name = preg_replace('/^mod/i', '', get_class($this));
|
||||
|
||||
// DESCRIPTION_FLAG
|
||||
// Module description, used if translation string 'ModuleImportZugferdDesc' not found (ImportZugferd is name of module).
|
||||
$this->description = "ImportZugferdDescription";
|
||||
// Used only if file README.md and README-LL.md not found.
|
||||
$this->descriptionlong = "ImportZugferdDescription";
|
||||
|
||||
// Author
|
||||
$this->editor_name = 'Alles Watt läuft (Testsystem)';
|
||||
$this->editor_url = ''; // Must be an external online web site
|
||||
$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 = '5.5';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
// Key used in llx_const table to save module status enabled/disabled (where IMPORTZUGFERD is value of property name of module in uppercase)
|
||||
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
|
||||
|
||||
// Name of image file used for this module.
|
||||
// If file is in theme/yourtheme/img directory under name object_pictovalue.png, use this->picto='pictovalue'
|
||||
// If file is in module/img directory under name object_pictovalue.png, use this->picto='pictovalue@module'
|
||||
// To use a supported fa-xxx css style of font awesome, use this->picto='xxx'
|
||||
$this->picto = 'fa-file-invoice';
|
||||
|
||||
// Define some features supported by module (triggers, login, substitutions, menus, css, etc...)
|
||||
$this->module_parts = array(
|
||||
// Set this to 1 if module has its own trigger directory (core/triggers)
|
||||
'triggers' => 0,
|
||||
// Set this to 1 if module has its own login method file (core/login)
|
||||
'login' => 0,
|
||||
// Set this to 1 if module has its own substitution function file (core/substitutions)
|
||||
'substitutions' => 0,
|
||||
// Set this to 1 if module has its own menus handler directory (core/menus)
|
||||
'menus' => 0,
|
||||
// Set this to 1 if module overwrite template dir (core/tpl)
|
||||
'tpl' => 0,
|
||||
// Set this to 1 if module has its own barcode directory (core/modules/barcode)
|
||||
'barcode' => 0,
|
||||
// Set this to 1 if module has its own models directory (core/modules/xxx)
|
||||
'models' => 0,
|
||||
// Set this to 1 if module has its own printing directory (core/modules/printing)
|
||||
'printing' => 0,
|
||||
// Set this to 1 if module has its own theme directory (theme)
|
||||
'theme' => 0,
|
||||
// Set this to relative path of css file if module has its own css file
|
||||
'css' => array(
|
||||
'/importzugferd/css/importzugferd.css.php',
|
||||
),
|
||||
// Set this to relative path of js file if module must load a js on all pages
|
||||
'js' => array(
|
||||
// '/importzugferd/js/importzugferd.js.php',
|
||||
),
|
||||
// Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context to 'all'
|
||||
/* BEGIN MODULEBUILDER HOOKSCONTEXTS */
|
||||
'hooks' => array(
|
||||
'data' => array(
|
||||
'index',
|
||||
'productindex',
|
||||
),
|
||||
'entity' => '0',
|
||||
),
|
||||
/* END MODULEBUILDER HOOKSCONTEXTS */
|
||||
// Set this to 1 if features of module are opened to external users
|
||||
'moduleforexternal' => 0,
|
||||
// Set this to 1 if the module provides a website template into doctemplates/websites/website_template-mytemplate
|
||||
'websitetemplates' => 0,
|
||||
// Set this to 1 if the module provides a captcha driver
|
||||
'captcha' => 0
|
||||
);
|
||||
|
||||
// Data directories to create when module is enabled.
|
||||
$this->dirs = array("/importzugferd/temp", "/importzugferd/imports", "/importzugferd/datanorm");
|
||||
|
||||
// Config pages. Put here list of php page, stored into importzugferd/admin directory, to use to setup module.
|
||||
$this->config_page_url = array("setup.php@importzugferd");
|
||||
|
||||
// Dependencies
|
||||
// A condition to hide module
|
||||
$this->hidden = getDolGlobalInt('MODULE_IMPORTZUGFERD_DISABLED'); // A condition to disable module;
|
||||
// List of module class names that must be enabled if this module is enabled. Example: array('always'=>array('modModuleToEnable1','modModuleToEnable2'), 'FR'=>array('modModuleToEnableFR')...)
|
||||
$this->depends = array();
|
||||
// List of module class names to disable if this one is disabled. Example: array('modModuleToDisable1', ...)
|
||||
$this->requiredby = array();
|
||||
// List of module class names this module is in conflict with. Example: array('modModuleToDisable1', ...)
|
||||
$this->conflictwith = array();
|
||||
|
||||
// The language file dedicated to your module
|
||||
$this->langfiles = array("importzugferd@importzugferd");
|
||||
|
||||
// Prerequisites
|
||||
$this->phpmin = array(7, 1); // Minimum version of PHP required by module
|
||||
// $this->phpmax = array(8, 0); // Maximum version of PHP required by module
|
||||
$this->need_dolibarr_version = array(19, -3); // Minimum version of Dolibarr required by module
|
||||
// $this->max_dolibarr_version = array(19, -3); // Maximum version of Dolibarr required by module
|
||||
$this->need_javascript_ajax = 0;
|
||||
|
||||
// Messages at activation
|
||||
$this->warnings_activation = array(); // Warning to show when we activate module. array('always'='text') or array('FR'='textfr','MX'='textmx'...)
|
||||
$this->warnings_activation_ext = array(); // Warning to show when we activate an external module. array('always'='text') or array('FR'='textfr','MX'='textmx'...)
|
||||
//$this->automatic_activation = array('FR'=>'ImportZugferdWasAutomaticallyActivatedBecauseOfYourCountryChoice');
|
||||
//$this->always_enabled = true; // If true, can't be disabled
|
||||
|
||||
// Constants
|
||||
// List of particular constants to add when module is enabled (key, 'chaine', value, desc, visible, 'current' or 'allentities', deleteonunactive)
|
||||
// Example: $this->const=array(1 => array('IMPORTZUGFERD_MYNEWCONST1', 'chaine', 'myvalue', 'This is a constant to add', 1),
|
||||
// 2 => array('IMPORTZUGFERD_MYNEWCONST2', 'chaine', 'myvalue', 'This is another constant to add', 0, 'current', 1)
|
||||
// );
|
||||
$this->const = array();
|
||||
|
||||
// Some keys to add into the overwriting translation tables
|
||||
/*$this->overwrite_translation = array(
|
||||
'en_US:ParentCompany'=>'Parent company or reseller',
|
||||
'fr_FR:ParentCompany'=>'Maison mère ou revendeur'
|
||||
)*/
|
||||
|
||||
if (!isModEnabled("importzugferd")) {
|
||||
$conf->importzugferd = new stdClass();
|
||||
$conf->importzugferd->enabled = 0;
|
||||
}
|
||||
|
||||
// Array to add new pages in new tabs
|
||||
/* BEGIN MODULEBUILDER TABS */
|
||||
$this->tabs = array();
|
||||
/* END MODULEBUILDER TABS */
|
||||
// Example:
|
||||
// To add a new tab identified by code tabname1
|
||||
// $this->tabs[] = array('data' => 'objecttype:+tabname1:Title1:mylangfile@importzugferd:$user->hasRight(\'importzugferd\', \'read\'):/importzugferd/mynewtab1.php?id=__ID__');
|
||||
// To add another new tab identified by code tabname2. Label will be result of calling all substitution functions on 'Title2' key.
|
||||
// $this->tabs[] = array('data' => 'objecttype:+tabname2:SUBSTITUTION_Title2:mylangfile@importzugferd:$user->hasRight(\'othermodule\', \'read\'):/importzugferd/mynewtab2.php?id=__ID__',
|
||||
// To remove an existing tab identified by code tabname
|
||||
// $this->tabs[] = array('data' => 'objecttype:-tabname:NU:conditiontoremove');
|
||||
//
|
||||
// Where objecttype can be
|
||||
// 'categories_x' to add a tab in category view (replace 'x' by type of category (0=product, 1=supplier, 2=customer, 3=member)
|
||||
// 'contact' to add a tab in contact view
|
||||
// 'contract' to add a tab in contract view
|
||||
// 'delivery' to add a tab in delivery view
|
||||
// 'group' to add a tab in group view
|
||||
// 'intervention' to add a tab in intervention view
|
||||
// 'invoice' to add a tab in customer invoice view
|
||||
// 'supplier_invoice' to add a tab in supplier invoice view
|
||||
// 'member' to add a tab in foundation member view
|
||||
// 'opensurveypoll' to add a tab in opensurvey poll view
|
||||
// 'order' to add a tab in sale order view
|
||||
// 'supplier_order' to add a tab in supplier order view
|
||||
// 'payment' to add a tab in payment view
|
||||
// 'supplier_payment' to add a tab in supplier payment view
|
||||
// 'product' to add a tab in product view
|
||||
// 'propal' to add a tab in propal view
|
||||
// 'project' to add a tab in project view
|
||||
// 'stock' to add a tab in stock view
|
||||
// 'thirdparty' to add a tab in third party view
|
||||
// 'user' to add a tab in user view
|
||||
|
||||
|
||||
// Dictionaries
|
||||
/* Example:
|
||||
$this->dictionaries=array(
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
// List of tables we want to see into dictionary editor
|
||||
'tabname' => array("table1", "table2", "table3"),
|
||||
// Label of tables
|
||||
'tablib' => array("Table1", "Table2", "Table3"),
|
||||
// Request to select fields
|
||||
'tabsql' => array('SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.$this->db->prefix().'table1 as f', 'SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.$this->db->prefix().'table2 as f', 'SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.$this->db->prefix().'table3 as f'),
|
||||
// Sort order
|
||||
'tabsqlsort' => array("label ASC", "label ASC", "label ASC"),
|
||||
// List of fields (result of select to show dictionary)
|
||||
'tabfield' => array("code,label", "code,label", "code,label"),
|
||||
// List of fields (list of fields to edit a record)
|
||||
'tabfieldvalue' => array("code,label", "code,label", "code,label"),
|
||||
// List of fields (list of fields for insert)
|
||||
'tabfieldinsert' => array("code,label", "code,label", "code,label"),
|
||||
// Name of columns with primary key (try to always name it 'rowid')
|
||||
'tabrowid' => array("rowid", "rowid", "rowid"),
|
||||
// Condition to show each dictionary
|
||||
'tabcond' => array(isModEnabled('importzugferd'), isModEnabled('importzugferd'), isModEnabled('importzugferd')),
|
||||
// Tooltip for every fields of dictionaries: DO NOT PUT AN EMPTY ARRAY
|
||||
'tabhelp' => array(array('code' => $langs->trans('CodeTooltipHelp'), 'field2' => 'field2tooltip'), array('code' => $langs->trans('CodeTooltipHelp'), 'field2' => 'field2tooltip'), ...),
|
||||
);
|
||||
*/
|
||||
/* BEGIN MODULEBUILDER DICTIONARIES */
|
||||
$this->dictionaries = array();
|
||||
/* END MODULEBUILDER DICTIONARIES */
|
||||
|
||||
// Boxes/Widgets
|
||||
// Add here list of php file(s) stored in importzugferd/core/boxes that contains a class to show a widget.
|
||||
/* BEGIN MODULEBUILDER WIDGETS */
|
||||
$this->boxes = array(
|
||||
0 => array(
|
||||
'file' => 'box_new_products.php@importzugferd',
|
||||
'note' => 'Widget showing new products to review',
|
||||
'enabledbydefaulton' => 'Home',
|
||||
),
|
||||
);
|
||||
/* END MODULEBUILDER WIDGETS */
|
||||
|
||||
// Cronjobs (List of cron jobs entries to add when module is enabled)
|
||||
// unit_frequency must be 60 for minute, 3600 for hour, 86400 for day, 604800 for week
|
||||
/* BEGIN MODULEBUILDER CRON */
|
||||
$this->cronjobs = array(
|
||||
0 => array(
|
||||
'label' => 'ImportZugferdScheduled',
|
||||
'jobtype' => 'method',
|
||||
'class' => '/importzugferd/class/cron_importzugferd.class.php',
|
||||
'objectname' => 'CronImportZugferd',
|
||||
'method' => 'runScheduledImport',
|
||||
'parameters' => '',
|
||||
'comment' => 'Scheduled import from folder and mailbox (frequency controlled by module settings)',
|
||||
'frequency' => 15,
|
||||
'unitfrequency' => 60,
|
||||
'status' => 1,
|
||||
'test' => 'isModEnabled("importzugferd")',
|
||||
'priority' => 50,
|
||||
),
|
||||
);
|
||||
/* END MODULEBUILDER CRON */
|
||||
// Example: $this->cronjobs=array(
|
||||
// 0=>array('label'=>'My label', 'jobtype'=>'method', 'class'=>'/dir/class/file.class.php', 'objectname'=>'MyClass', 'method'=>'myMethod', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>2, 'unitfrequency'=>3600, 'status'=>0, 'test'=>'isModEnabled("importzugferd")', 'priority'=>50),
|
||||
// 1=>array('label'=>'My label', 'jobtype'=>'command', 'command'=>'', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>1, 'unitfrequency'=>3600*24, 'status'=>0, 'test'=>'isModEnabled("importzugferd")', 'priority'=>50)
|
||||
// );
|
||||
|
||||
// Permissions provided by this module
|
||||
$this->rights = array();
|
||||
$r = 0;
|
||||
|
||||
$this->rights[$r][0] = $this->numero . sprintf("%02d", 1);
|
||||
$this->rights[$r][1] = 'Read ZUGFeRD imports';
|
||||
$this->rights[$r][4] = 'import';
|
||||
$this->rights[$r][5] = 'read';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = $this->numero . sprintf("%02d", 2);
|
||||
$this->rights[$r][1] = 'Create/Import ZUGFeRD invoices';
|
||||
$this->rights[$r][4] = 'import';
|
||||
$this->rights[$r][5] = 'write';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = $this->numero . sprintf("%02d", 3);
|
||||
$this->rights[$r][1] = 'Delete ZUGFeRD imports';
|
||||
$this->rights[$r][4] = 'import';
|
||||
$this->rights[$r][5] = 'delete';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = $this->numero . sprintf("%02d", 4);
|
||||
$this->rights[$r][1] = 'Manage product mappings';
|
||||
$this->rights[$r][4] = 'mapping';
|
||||
$this->rights[$r][5] = 'write';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = $this->numero . sprintf("%02d", 5);
|
||||
$this->rights[$r][1] = 'Manage Datanorm catalogs';
|
||||
$this->rights[$r][4] = 'datanorm';
|
||||
$this->rights[$r][5] = 'write';
|
||||
$r++;
|
||||
|
||||
|
||||
// Main menu entries to add
|
||||
$this->menu = array();
|
||||
$r = 0;
|
||||
// Add here entries to declare new menus
|
||||
/* BEGIN MODULEBUILDER TOPMENU */
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => '', // Will be stored into mainmenu + leftmenu. Use '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode
|
||||
'type' => 'top', // This is a Top menu entry
|
||||
'titre' => 'ModuleImportZugferdName',
|
||||
'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => '',
|
||||
'url' => '/importzugferd/importzugferdindex.php',
|
||||
'langs' => 'importzugferd@importzugferd', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory.
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")', // Define condition to show or hide menu entry. Use 'isModEnabled("importzugferd")' if entry must be visible if module is enabled.
|
||||
'perms' => '1', // Use 'perms'=>'$user->hasRight("importzugferd", "myobject", "read")' if you want your menu with a permission rules
|
||||
'target' => '',
|
||||
'user' => 2, // 0=Menu for internal users, 1=external users, 2=both
|
||||
);
|
||||
/* END MODULEBUILDER TOPMENU */
|
||||
|
||||
// Left menu: Import
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=importzugferd',
|
||||
'type' => 'left',
|
||||
'titre' => 'ZugferdImport',
|
||||
'prefix' => img_picto('', 'fa-file-import', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => 'zugferd_import',
|
||||
'url' => '/importzugferd/import.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")',
|
||||
'perms' => '$user->hasRight("importzugferd", "import", "write")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
// Left menu: Import list
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=importzugferd',
|
||||
'type' => 'left',
|
||||
'titre' => 'ImportList',
|
||||
'prefix' => img_picto('', 'fa-list', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => 'zugferd_list',
|
||||
'url' => '/importzugferd/list.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")',
|
||||
'perms' => '$user->hasRight("importzugferd", "import", "read")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
// Left menu: Product Mapping
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=importzugferd',
|
||||
'type' => 'left',
|
||||
'titre' => 'ProductMapping',
|
||||
'prefix' => img_picto('', 'fa-exchange-alt', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => 'zugferd_mapping',
|
||||
'url' => '/importzugferd/mapping.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")',
|
||||
'perms' => '$user->hasRight("importzugferd", "mapping", "write")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
// Left menu: Batch Import
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=importzugferd',
|
||||
'type' => 'left',
|
||||
'titre' => 'BatchImport',
|
||||
'prefix' => img_picto('', 'fa-folder-open', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => 'zugferd_batch',
|
||||
'url' => '/importzugferd/batch.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")',
|
||||
'perms' => '$user->hasRight("importzugferd", "import", "write")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
// Left menu: Datanorm Catalogs
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=importzugferd',
|
||||
'type' => 'left',
|
||||
'titre' => 'DatanormCatalogs',
|
||||
'prefix' => img_picto('', 'fa-database', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => 'zugferd_datanorm',
|
||||
'url' => '/importzugferd/datanorm.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")',
|
||||
'perms' => '$user->hasRight("importzugferd", "datanorm", "write")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
// Left menu: Datanorm Mass Update
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=importzugferd',
|
||||
'type' => 'left',
|
||||
'titre' => 'DatanormMassUpdate',
|
||||
'prefix' => img_picto('', 'fa-sync', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => 'zugferd_datanorm_update',
|
||||
'url' => '/importzugferd/datanorm_update.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")',
|
||||
'perms' => '$user->hasRight("produit", "creer")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
// Left menu: Datanorm Change Log
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=importzugferd',
|
||||
'type' => 'left',
|
||||
'titre' => 'DatanormChangeLog',
|
||||
'prefix' => img_picto('', 'fa-history', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'importzugferd',
|
||||
'leftmenu' => 'zugferd_datanorm_log',
|
||||
'url' => '/importzugferd/datanorm_changelog.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 1000 + $r,
|
||||
'enabled' => 'isModEnabled("importzugferd")',
|
||||
'perms' => '$user->hasRight("produit", "lire")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
// Left menu entry under Products main menu: New Products to Review (after Statistics)
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=products,fk_leftmenu=product',
|
||||
'type' => 'left',
|
||||
'titre' => 'NewProductsToReview',
|
||||
'prefix' => img_picto('', 'fa-star', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'products',
|
||||
'leftmenu' => 'new_products_review',
|
||||
'url' => '/importzugferd/new_products.php',
|
||||
'langs' => 'importzugferd@importzugferd',
|
||||
'position' => 650,
|
||||
'enabled' => 'isModEnabled("importzugferd") && isModEnabled("product")',
|
||||
'perms' => '$user->hasRight("produit", "lire")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
|
||||
// Exports profiles provided by this module
|
||||
$r = 0;
|
||||
/* BEGIN MODULEBUILDER EXPORT MYOBJECT */
|
||||
/*
|
||||
$langs->load("importzugferd@importzugferd");
|
||||
$this->export_code[$r] = $this->rights_class.'_'.$r;
|
||||
$this->export_label[$r] = 'MyObjectLines'; // Translation key (used only if key ExportDataset_xxx_z not found)
|
||||
$this->export_icon[$r] = $this->picto;
|
||||
// Define $this->export_fields_array, $this->export_TypeFields_array and $this->export_entities_array
|
||||
$keyforclass = 'MyObject'; $keyforclassfile='/importzugferd/class/myobject.class.php'; $keyforelement='myobject@importzugferd';
|
||||
include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php';
|
||||
//$this->export_fields_array[$r]['t.fieldtoadd']='FieldToAdd'; $this->export_TypeFields_array[$r]['t.fieldtoadd']='Text';
|
||||
//unset($this->export_fields_array[$r]['t.fieldtoremove']);
|
||||
//$keyforclass = 'MyObjectLine'; $keyforclassfile='/importzugferd/class/myobject.class.php'; $keyforelement='myobjectline@importzugferd'; $keyforalias='tl';
|
||||
//include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php';
|
||||
$keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@importzugferd';
|
||||
include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php';
|
||||
//$keyforselect='myobjectline'; $keyforaliasextra='extraline'; $keyforelement='myobjectline@importzugferd';
|
||||
//include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php';
|
||||
//$this->export_dependencies_array[$r] = array('myobjectline' => array('tl.rowid','tl.ref')); // To force to activate one or several fields if we select some fields that need same (like to select a unique key if we ask a field of a child to avoid the DISTINCT to discard them, or for computed field than need several other fields)
|
||||
//$this->export_special_array[$r] = array('t.field' => '...');
|
||||
//$this->export_examplevalues_array[$r] = array('t.field' => 'Example');
|
||||
//$this->export_help_array[$r] = array('t.field' => 'FieldDescHelp');
|
||||
$this->export_sql_start[$r]='SELECT DISTINCT ';
|
||||
$this->export_sql_end[$r] =' FROM '.$this->db->prefix().'importzugferd_myobject as t';
|
||||
//$this->export_sql_end[$r] .=' LEFT JOIN '.$this->db->prefix().'importzugferd_myobject_line as tl ON tl.fk_myobject = t.rowid';
|
||||
$this->export_sql_end[$r] .=' WHERE 1 = 1';
|
||||
$this->export_sql_end[$r] .=' AND t.entity IN ('.getEntity('myobject').')';
|
||||
$r++; */
|
||||
/* END MODULEBUILDER EXPORT MYOBJECT */
|
||||
|
||||
// Imports profiles provided by this module
|
||||
$r = 0;
|
||||
/* BEGIN MODULEBUILDER IMPORT MYOBJECT */
|
||||
/*
|
||||
$langs->load("importzugferd@importzugferd");
|
||||
$this->import_code[$r] = $this->rights_class.'_'.$r;
|
||||
$this->import_label[$r] = 'MyObjectLines'; // Translation key (used only if key ExportDataset_xxx_z not found)
|
||||
$this->import_icon[$r] = $this->picto;
|
||||
$this->import_tables_array[$r] = array('t' => $this->db->prefix().'importzugferd_myobject', 'extra' => $this->db->prefix().'importzugferd_myobject_extrafields');
|
||||
$this->import_tables_creator_array[$r] = array('t' => 'fk_user_author'); // Fields to store import user id
|
||||
$import_sample = array();
|
||||
$keyforclass = 'MyObject'; $keyforclassfile='/importzugferd/class/myobject.class.php'; $keyforelement='myobject@importzugferd';
|
||||
include DOL_DOCUMENT_ROOT.'/core/commonfieldsinimport.inc.php';
|
||||
$import_extrafield_sample = array();
|
||||
$keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@importzugferd';
|
||||
include DOL_DOCUMENT_ROOT.'/core/extrafieldsinimport.inc.php';
|
||||
$this->import_fieldshidden_array[$r] = array('extra.fk_object' => 'lastrowid-'.$this->db->prefix().'importzugferd_myobject');
|
||||
$this->import_regex_array[$r] = array();
|
||||
$this->import_examplevalues_array[$r] = array_merge($import_sample, $import_extrafield_sample);
|
||||
$this->import_updatekeys_array[$r] = array('t.ref' => 'Ref');
|
||||
$this->import_convertvalue_array[$r] = array(
|
||||
't.ref' => array(
|
||||
'rule'=>'getrefifauto',
|
||||
'class'=>(!getDolGlobalString('IMPORTZUGFERD_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('IMPORTZUGFERD_MYOBJECT_ADDON')),
|
||||
'path'=>"/core/modules/importzugferd/".(!getDolGlobalString('IMPORTZUGFERD_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('IMPORTZUGFERD_MYOBJECT_ADDON')).'.php',
|
||||
'classobject'=>'MyObject',
|
||||
'pathobject'=>'/importzugferd/class/myobject.class.php',
|
||||
),
|
||||
't.fk_soc' => array('rule' => 'fetchidfromref', 'file' => '/societe/class/societe.class.php', 'class' => 'Societe', 'method' => 'fetch', 'element' => 'ThirdParty'),
|
||||
't.fk_user_valid' => array('rule' => 'fetchidfromref', 'file' => '/user/class/user.class.php', 'class' => 'User', 'method' => 'fetch', 'element' => 'user'),
|
||||
't.fk_mode_reglement' => array('rule' => 'fetchidfromcodeorlabel', 'file' => '/compta/paiement/class/cpaiement.class.php', 'class' => 'Cpaiement', 'method' => 'fetch', 'element' => 'cpayment'),
|
||||
);
|
||||
$this->import_run_sql_after_array[$r] = array();
|
||||
$r++; */
|
||||
/* END MODULEBUILDER IMPORT MYOBJECT */
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when module is enabled.
|
||||
* The init function add constants, boxes, permissions and menus (defined in constructor) into Dolibarr database.
|
||||
* It also creates data directories
|
||||
*
|
||||
* @param string $options Options when enabling module ('', 'noboxes')
|
||||
* @return int<-1,1> 1 if OK, <=0 if KO
|
||||
*/
|
||||
public function init($options = '')
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
// Create tables of module at module activation
|
||||
//$result = $this->_load_tables('/install/mysql/', 'importzugferd');
|
||||
$result = $this->_load_tables('/importzugferd/sql/');
|
||||
if ($result < 0) {
|
||||
return -1; // Do not activate module if error 'not allowed' returned when loading module SQL queries (the _load_table run sql with run_sql with the error allowed parameter set to 'default')
|
||||
}
|
||||
|
||||
// Create extrafields during init
|
||||
include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
|
||||
$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', // 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', // 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', // 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', // 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', // 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
|
||||
$this->remove($options);
|
||||
|
||||
$sql = array();
|
||||
|
||||
// Run standard init first (creates box definitions)
|
||||
$result = $this->_init($sql, $options);
|
||||
|
||||
// Now activate widget for product index page (area 4)
|
||||
// Box definition is now created by _init(), so we can find it
|
||||
$sql_box = "SELECT rowid FROM ".$this->db->prefix()."boxes_def WHERE file = 'box_new_products.php@importzugferd'";
|
||||
$resql = $this->db->query($sql_box);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$box_id = $obj->rowid;
|
||||
|
||||
// Check if already activated for area 4 (product index)
|
||||
$sql_check = "SELECT rowid FROM ".$this->db->prefix()."boxes WHERE box_id = ".(int)$box_id." AND position = 4 AND entity = ".(int)$conf->entity;
|
||||
$resql2 = $this->db->query($sql_check);
|
||||
if ($resql2 && $this->db->num_rows($resql2) == 0) {
|
||||
// Not yet activated, add it
|
||||
$sql_insert = "INSERT INTO ".$this->db->prefix()."boxes (box_id, position, box_order, fk_user, entity) VALUES (".(int)$box_id.", 4, 'A01', 0, ".(int)$conf->entity.")";
|
||||
$this->db->query($sql_insert);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when module is disabled.
|
||||
* Remove from database constants, boxes and permissions from Dolibarr database.
|
||||
* Data directories are not deleted
|
||||
*
|
||||
* @param string $options Options when enabling module ('', 'noboxes')
|
||||
* @return int<-1,1> 1 if OK, <=0 if KO
|
||||
*/
|
||||
public function remove($options = '')
|
||||
{
|
||||
$sql = array();
|
||||
return $this->_remove($sql, $options);
|
||||
}
|
||||
}
|
||||
48
css/importzugferd.css.php
Executable file
48
css/importzugferd.css.php
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
/* Copyright (C) 2024 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file css/importzugferd.css.php
|
||||
* \ingroup importzugferd
|
||||
* \brief CSS file for importzugferd module
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
if (!defined('NOREQUIRESOC')) {
|
||||
define('NOREQUIRESOC', '1');
|
||||
}
|
||||
if (!defined('NOTOKENRENEWAL')) {
|
||||
define('NOTOKENRENEWAL', '1');
|
||||
}
|
||||
if (!defined('NOLOGIN')) {
|
||||
define('NOLOGIN', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREHTML')) {
|
||||
define('NOREQUIREHTML', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREAJAX')) {
|
||||
define('NOREQUIREAJAX', '1');
|
||||
}
|
||||
|
||||
session_cache_limiter('public');
|
||||
|
||||
require_once '../../../main.inc.php';
|
||||
|
||||
header('Content-Type: text/css');
|
||||
|
||||
?>
|
||||
/* Icon for importzugferd new products dashboard box */
|
||||
.fa-dol-importzugferd_newproducts:before {
|
||||
content: "\f1b3"; /* FontAwesome cubes icon for products */
|
||||
}
|
||||
|
||||
/* Background color for the dashboard box */
|
||||
.bg-infobox-importzugferd_newproducts {
|
||||
background: linear-gradient(135deg, #6c5ce7 0%, #a29bfe 100%) !important;
|
||||
}
|
||||
309
datanorm.php
Executable file
309
datanorm.php
Executable file
|
|
@ -0,0 +1,309 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file datanorm.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Datanorm catalog management page
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../main.inc.php")) {
|
||||
$res = @include "../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
||||
require_once './class/datanorm.class.php';
|
||||
require_once './class/datanormparser.class.php';
|
||||
require_once './lib/importzugferd.lib.php';
|
||||
|
||||
$langs->loadLangs(array('importzugferd@importzugferd', 'companies', 'products'));
|
||||
|
||||
// Access control
|
||||
if (!$user->hasRight('importzugferd', 'datanorm', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
$fk_soc = GETPOSTINT('fk_soc');
|
||||
$id = GETPOSTINT('id');
|
||||
|
||||
// Objects
|
||||
$form = new Form($db);
|
||||
$datanorm = new Datanorm($db);
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// Upload Datanorm file
|
||||
if ($action == 'upload' && !empty($_FILES['datanormfile']['name']) && $fk_soc > 0) {
|
||||
$error = 0;
|
||||
|
||||
// Check supplier exists and is a supplier
|
||||
$supplier = new Societe($db);
|
||||
if ($supplier->fetch($fk_soc) <= 0 || $supplier->fournisseur != 1) {
|
||||
setEventMessages($langs->trans('ErrorSupplierNotFound'), null, 'errors');
|
||||
$error++;
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
// Create upload directory
|
||||
$upload_dir = $conf->importzugferd->dir_output.'/datanorm/'.$fk_soc;
|
||||
if (!dol_is_dir($upload_dir)) {
|
||||
dol_mkdir($upload_dir);
|
||||
}
|
||||
|
||||
// Handle file upload
|
||||
$uploaded_files = array();
|
||||
|
||||
// Check if multiple files or single file
|
||||
if (is_array($_FILES['datanormfile']['name'])) {
|
||||
$file_count = count($_FILES['datanormfile']['name']);
|
||||
for ($i = 0; $i < $file_count; $i++) {
|
||||
if ($_FILES['datanormfile']['error'][$i] == UPLOAD_ERR_OK) {
|
||||
$tmp_name = $_FILES['datanormfile']['tmp_name'][$i];
|
||||
$name = $_FILES['datanormfile']['name'][$i];
|
||||
$dest = $upload_dir.'/'.$name;
|
||||
|
||||
if (dol_move_uploaded_file($tmp_name, $dest, 1) > 0) {
|
||||
$uploaded_files[] = $dest;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($_FILES['datanormfile']['error'] == UPLOAD_ERR_OK) {
|
||||
$tmp_name = $_FILES['datanormfile']['tmp_name'];
|
||||
$name = $_FILES['datanormfile']['name'];
|
||||
$dest = $upload_dir.'/'.$name;
|
||||
|
||||
if (dol_move_uploaded_file($tmp_name, $dest, 1) > 0) {
|
||||
$uploaded_files[] = $dest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($uploaded_files)) {
|
||||
setEventMessages($langs->trans('ErrorUploadFailed'), null, 'errors');
|
||||
$error++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error && !empty($uploaded_files)) {
|
||||
// Use streaming import for large files (directory-based)
|
||||
$delete_existing = GETPOST('delete_existing', 'int') ? true : false;
|
||||
$imported = $datanorm->importFromDirectoryStreaming($user, $fk_soc, $upload_dir, $delete_existing);
|
||||
|
||||
if ($imported > 0) {
|
||||
setEventMessages($langs->trans('DatanormImportSuccess', $imported), null, 'mesgs');
|
||||
} elseif ($imported == 0) {
|
||||
setEventMessages($langs->trans('DatanormNoArticlesFound'), null, 'warnings');
|
||||
} else {
|
||||
setEventMessages($langs->trans('DatanormImportFailed').': '.$datanorm->error, null, 'errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all articles for supplier
|
||||
if ($action == 'delete' && $confirm == 'yes' && $fk_soc > 0) {
|
||||
$result = $datanorm->deleteAllBySupplier($user, $fk_soc);
|
||||
if ($result >= 0) {
|
||||
setEventMessages($langs->trans('DatanormDeleted', $result), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans('DatanormDeleteFailed').': '.$datanorm->error, null, 'errors');
|
||||
}
|
||||
$action = '';
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$title = $langs->trans('DatanormCatalogs');
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-datanorm');
|
||||
|
||||
print load_fiche_titre($title, '', 'fa-database');
|
||||
|
||||
// Confirmation dialog for delete
|
||||
if ($action == 'delete' && $fk_soc > 0) {
|
||||
$supplier = new Societe($db);
|
||||
$supplier->fetch($fk_soc);
|
||||
|
||||
$formconfirm = $form->formconfirm(
|
||||
$_SERVER["PHP_SELF"].'?fk_soc='.$fk_soc,
|
||||
$langs->trans('DeleteDatanorm'),
|
||||
$langs->trans('ConfirmDeleteDatanorm', $supplier->name),
|
||||
'delete',
|
||||
'',
|
||||
0,
|
||||
1
|
||||
);
|
||||
print $formconfirm;
|
||||
}
|
||||
|
||||
// Upload form
|
||||
print '<div class="fichecenter">';
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans('UploadDatanorm').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="2">';
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" enctype="multipart/form-data">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="upload">';
|
||||
|
||||
print '<table class="nobordernopadding">';
|
||||
|
||||
// Supplier selection
|
||||
print '<tr>';
|
||||
print '<td class="titlefield">'.$langs->trans('Supplier').' <span class="fieldrequired">*</span></td>';
|
||||
print '<td>';
|
||||
print $form->select_company($fk_soc, 'fk_soc', 's.fournisseur = 1', 1, 0, 0, array(), 0, 'minwidth300');
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// File upload
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('DatanormFiles').' <span class="fieldrequired">*</span></td>';
|
||||
print '<td>';
|
||||
print '<input type="file" name="datanormfile[]" multiple accept=".001,.002,.003,.004,.005,.wrg,.rab,.xml" class="flat">';
|
||||
print '<br><span class="opacitymedium small">'.$langs->trans('DatanormFileHelp').'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Delete existing option
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans('DeleteExisting').'</td>';
|
||||
print '<td>';
|
||||
print '<input type="checkbox" name="delete_existing" value="1" checked>';
|
||||
print ' <span class="opacitymedium">'.$langs->trans('DeleteExistingHelp').'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Submit button
|
||||
print '<tr>';
|
||||
print '<td></td>';
|
||||
print '<td>';
|
||||
print '<input type="submit" class="button" value="'.$langs->trans('Upload').'">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</form>';
|
||||
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '<br>';
|
||||
|
||||
// List of suppliers with Datanorm data
|
||||
$suppliers = $datanorm->getSuppliersWithData();
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>'.$langs->trans('Supplier').'</td>';
|
||||
print '<td class="right">'.$langs->trans('ArticleCount').'</td>';
|
||||
print '<td class="center">'.$langs->trans('LastImport').'</td>';
|
||||
print '<td class="center">'.$langs->trans('Actions').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
if (!empty($suppliers)) {
|
||||
foreach ($suppliers as $sup) {
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Supplier name with link
|
||||
print '<td>';
|
||||
$supplier = new Societe($db);
|
||||
$supplier->fetch($sup['fk_soc']);
|
||||
print $supplier->getNomUrl(1, 'supplier');
|
||||
print '</td>';
|
||||
|
||||
// Article count
|
||||
print '<td class="right">';
|
||||
print '<span class="badge badge-info">'.$sup['article_count'].'</span>';
|
||||
print '</td>';
|
||||
|
||||
// Last import
|
||||
print '<td class="center">';
|
||||
print dol_print_date($sup['last_import'], 'dayhour');
|
||||
print '</td>';
|
||||
|
||||
// Actions
|
||||
print '<td class="center nowraponall">';
|
||||
|
||||
// View articles button
|
||||
print '<a class="paddingright" href="datanorm_list.php?fk_soc='.$sup['fk_soc'].'" title="'.$langs->trans('ViewArticles').'">';
|
||||
print img_picto($langs->trans('ViewArticles'), 'list');
|
||||
print '</a>';
|
||||
|
||||
// Delete button
|
||||
print '<a class="paddingright" href="'.$_SERVER['PHP_SELF'].'?action=delete&fk_soc='.$sup['fk_soc'].'&token='.newToken().'" title="'.$langs->trans('Delete').'">';
|
||||
print img_picto($langs->trans('Delete'), 'delete');
|
||||
print '</a>';
|
||||
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="4" class="opacitymedium center">'.$langs->trans('NoDatanormData').'</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>';
|
||||
|
||||
// Settings info
|
||||
print '<br>';
|
||||
print '<div class="info">';
|
||||
print '<i class="fas fa-info-circle paddingright"></i>';
|
||||
print $langs->trans('DatanormSettingsInfo');
|
||||
print ' <a href="'.dol_buildpath('/importzugferd/admin/setup.php', 1).'">'.$langs->trans('Settings').'</a>';
|
||||
print '</div>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
333
datanorm_changelog.php
Executable file
333
datanorm_changelog.php
Executable file
|
|
@ -0,0 +1,333 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file datanorm_changelog.php
|
||||
* \ingroup importzugferd
|
||||
* \brief View Datanorm update change log
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../main.inc.php")) {
|
||||
$res = @include "../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formcompany.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
||||
dol_include_once('/importzugferd/lib/importzugferd.lib.php');
|
||||
|
||||
// Load translations
|
||||
$langs->loadLangs(array("importzugferd@importzugferd", "products", "bills"));
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('produit', 'lire')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$batch_id = GETPOST('batch_id', 'alphanohtml');
|
||||
$fk_product = GETPOSTINT('fk_product');
|
||||
$fk_soc = GETPOSTINT('fk_soc');
|
||||
$date_start = GETPOST('date_start', 'alpha');
|
||||
$date_end = GETPOST('date_end', 'alpha');
|
||||
|
||||
// Pagination
|
||||
$sortfield = GETPOST('sortfield', 'aZ09comma');
|
||||
$sortorder = GETPOST('sortorder', 'aZ09comma');
|
||||
$page = GETPOSTISSET('pageplusone') ? (GETPOSTINT('pageplusone') - 1) : GETPOSTINT('page');
|
||||
$limit = GETPOSTINT('limit') ? GETPOSTINT('limit') : $conf->liste_limit;
|
||||
$offset = $limit * $page;
|
||||
|
||||
if (!$sortfield) $sortfield = 'l.date_change';
|
||||
if (!$sortorder) $sortorder = 'DESC';
|
||||
|
||||
// Initialize objects
|
||||
$form = new Form($db);
|
||||
$formcompany = new FormCompany($db);
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$title = $langs->trans('DatanormChangeLog');
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-datanorm-changelog');
|
||||
|
||||
print load_fiche_titre($title, '<a href="'.dol_buildpath('/importzugferd/datanorm_update.php', 1).'" class="button">'.$langs->trans('DatanormMassUpdate').'</a>', 'fa-history');
|
||||
|
||||
// Filter form
|
||||
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="6">'.$langs->trans('Filters').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Batch ID filter
|
||||
print '<td class="titlefield">'.$langs->trans('BatchUpdate').'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" name="batch_id" value="'.dol_escape_htmltag($batch_id).'" class="minwidth200" placeholder="batch_...">';
|
||||
print '</td>';
|
||||
|
||||
// Supplier filter
|
||||
print '<td>'.$langs->trans('Supplier').'</td>';
|
||||
print '<td>';
|
||||
$sql = "SELECT DISTINCT s.rowid, s.nom FROM ".MAIN_DB_PREFIX."societe s";
|
||||
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."importzugferd_datanorm_log l ON l.fk_soc = s.rowid";
|
||||
$sql .= " ORDER BY s.nom";
|
||||
$resql = $db->query($sql);
|
||||
print '<select name="fk_soc" class="flat minwidth200">';
|
||||
print '<option value="">'.$langs->trans('All').'</option>';
|
||||
if ($resql) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
$selected = ($obj->rowid == $fk_soc) ? 'selected' : '';
|
||||
print '<option value="'.$obj->rowid.'" '.$selected.'>'.dol_escape_htmltag($obj->nom).'</option>';
|
||||
}
|
||||
}
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
|
||||
// Date range
|
||||
print '<td>'.$langs->trans('DateRange').'</td>';
|
||||
print '<td>';
|
||||
print '<input type="date" name="date_start" value="'.dol_escape_htmltag($date_start).'" class="minwidth100">';
|
||||
print ' - ';
|
||||
print '<input type="date" name="date_end" value="'.dol_escape_htmltag($date_end).'" class="minwidth100">';
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="6" class="center">';
|
||||
print '<input type="submit" class="button" value="'.$langs->trans('Search').'">';
|
||||
print ' <a href="'.$_SERVER['PHP_SELF'].'" class="button">'.$langs->trans('Reset').'</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
// Build SQL query
|
||||
$sql = "SELECT l.*, p.ref as product_ref, p.label as product_label, s.nom as supplier_name, u.login as user_login";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."importzugferd_datanorm_log l";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = l.fk_product";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe s ON s.rowid = l.fk_soc";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user u ON u.rowid = l.fk_user";
|
||||
$sql .= " WHERE l.entity IN (".getEntity('product').")";
|
||||
|
||||
if (!empty($batch_id)) {
|
||||
$sql .= " AND l.batch_id = '".$db->escape($batch_id)."'";
|
||||
}
|
||||
if ($fk_soc > 0) {
|
||||
$sql .= " AND l.fk_soc = ".((int)$fk_soc);
|
||||
}
|
||||
if ($fk_product > 0) {
|
||||
$sql .= " AND l.fk_product = ".((int)$fk_product);
|
||||
}
|
||||
if (!empty($date_start)) {
|
||||
$sql .= " AND l.date_change >= '".$db->escape($date_start)." 00:00:00'";
|
||||
}
|
||||
if (!empty($date_end)) {
|
||||
$sql .= " AND l.date_change <= '".$db->escape($date_end)." 23:59:59'";
|
||||
}
|
||||
|
||||
// Count total
|
||||
$sqlcount = preg_replace('/SELECT.*FROM/', 'SELECT COUNT(*) as total FROM', $sql);
|
||||
$resqlcount = $db->query($sqlcount);
|
||||
$total = 0;
|
||||
if ($resqlcount) {
|
||||
$objcount = $db->fetch_object($resqlcount);
|
||||
$total = $objcount->total;
|
||||
}
|
||||
|
||||
$sql .= $db->order($sortfield, $sortorder);
|
||||
$sql .= $db->plimit($limit + 1, $offset);
|
||||
|
||||
$resql = $db->query($sql);
|
||||
|
||||
print '<br>';
|
||||
|
||||
// Batch summary if filtering by batch
|
||||
if (!empty($batch_id)) {
|
||||
$sql_batch = "SELECT MIN(date_change) as start_date, MAX(date_change) as end_date, COUNT(DISTINCT fk_product) as product_count, COUNT(*) as change_count";
|
||||
$sql_batch .= " FROM ".MAIN_DB_PREFIX."importzugferd_datanorm_log";
|
||||
$sql_batch .= " WHERE batch_id = '".$db->escape($batch_id)."'";
|
||||
$res_batch = $db->query($sql_batch);
|
||||
if ($res_batch) {
|
||||
$batch_info = $db->fetch_object($res_batch);
|
||||
print '<div class="info">';
|
||||
print '<strong>'.$langs->trans('BatchUpdate').':</strong> '.$batch_id.'<br>';
|
||||
print '<strong>'.$langs->trans('DateChange').':</strong> '.dol_print_date($db->jdate($batch_info->start_date), 'dayhour').'<br>';
|
||||
print '<strong>'.$langs->trans('Products').':</strong> '.$batch_info->product_count.' | ';
|
||||
print '<strong>'.$langs->trans('Changes').':</strong> '.$batch_info->change_count;
|
||||
print '</div><br>';
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
print_barre_liste($langs->trans('ChangeHistory'), $page, $_SERVER['PHP_SELF'], '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc.'&date_start='.$date_start.'&date_end='.$date_end, $sortfield, $sortorder, '', $resql ? $db->num_rows($resql) : 0, $total, '', 0, '', '', $limit);
|
||||
|
||||
if ($resql) {
|
||||
$num = $db->num_rows($resql);
|
||||
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="noborder centpercent">';
|
||||
|
||||
// Header
|
||||
print '<tr class="liste_titre">';
|
||||
print_liste_field_titre($langs->trans('DateChange'), $_SERVER['PHP_SELF'], 'l.date_change', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('Product'), $_SERVER['PHP_SELF'], 'p.ref', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('Supplier'), $_SERVER['PHP_SELF'], 's.nom', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('DatanormArticle'), $_SERVER['PHP_SELF'], 'l.datanorm_ref', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('FieldChanged'), $_SERVER['PHP_SELF'], 'l.field_changed', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans('OldValue'), $_SERVER['PHP_SELF'], '', '', '', '');
|
||||
print_liste_field_titre($langs->trans('NewValue'), $_SERVER['PHP_SELF'], '', '', '', '');
|
||||
print_liste_field_titre($langs->trans('ChangedBy'), $_SERVER['PHP_SELF'], 'u.login', '', '&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print '</tr>';
|
||||
|
||||
if ($num > 0) {
|
||||
$i = 0;
|
||||
while ($i < min($num, $limit)) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Date
|
||||
print '<td class="nowraponall">'.dol_print_date($db->jdate($obj->date_change), 'dayhour').'</td>';
|
||||
|
||||
// Product
|
||||
print '<td>';
|
||||
$product = new Product($db);
|
||||
if ($product->fetch($obj->fk_product) > 0) {
|
||||
print $product->getNomUrl(1);
|
||||
print '<br><span class="opacitymedium">'.$obj->product_label.'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Supplier
|
||||
print '<td>'.dol_escape_htmltag($obj->supplier_name).'</td>';
|
||||
|
||||
// Datanorm ref
|
||||
print '<td>'.dol_escape_htmltag($obj->datanorm_ref).'</td>';
|
||||
|
||||
// Field changed
|
||||
print '<td>';
|
||||
$field_label = '';
|
||||
switch ($obj->field_changed) {
|
||||
case 'price':
|
||||
$field_label = $langs->trans('Price');
|
||||
print '<i class="fas fa-euro-sign paddingright"></i>';
|
||||
break;
|
||||
case 'description':
|
||||
$field_label = $langs->trans('Description');
|
||||
print '<i class="fas fa-align-left paddingright"></i>';
|
||||
break;
|
||||
case 'label':
|
||||
$field_label = $langs->trans('Label');
|
||||
print '<i class="fas fa-tag paddingright"></i>';
|
||||
break;
|
||||
default:
|
||||
$field_label = $obj->field_changed;
|
||||
}
|
||||
print $field_label;
|
||||
print '</td>';
|
||||
|
||||
// Old value
|
||||
print '<td class="tdoverflowmax200" style="background-color: #fff3cd;">';
|
||||
if ($obj->field_changed == 'price') {
|
||||
print price($obj->old_value);
|
||||
} else {
|
||||
print dol_escape_htmltag(dol_trunc($obj->old_value, 100));
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// New value
|
||||
print '<td class="tdoverflowmax200" style="background-color: #d4edda;">';
|
||||
if ($obj->field_changed == 'price') {
|
||||
print price($obj->new_value);
|
||||
// Show difference
|
||||
$diff = $obj->new_value - $obj->old_value;
|
||||
if ($obj->old_value > 0) {
|
||||
$diff_percent = ($diff / $obj->old_value) * 100;
|
||||
print '<br>';
|
||||
if ($diff > 0) {
|
||||
print '<span style="color: #d9534f;">+'.number_format($diff_percent, 1).'%</span>';
|
||||
} else {
|
||||
print '<span style="color: #5cb85c;">'.number_format($diff_percent, 1).'%</span>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print dol_escape_htmltag(dol_trunc($obj->new_value, 100));
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// User
|
||||
print '<td>'.dol_escape_htmltag($obj->user_login).'</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
$i++;
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="8" class="opacitymedium center">'.$langs->trans('NoChangesRecorded').'</td></tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
$db->free($resql);
|
||||
} else {
|
||||
dol_print_error($db);
|
||||
}
|
||||
|
||||
// Export buttons
|
||||
if ($total > 0) {
|
||||
print '<br><div class="center">';
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=export&batch_id='.urlencode($batch_id).'&fk_soc='.$fk_soc.'&date_start='.$date_start.'&date_end='.$date_end.'" class="button">';
|
||||
print '<i class="fas fa-download paddingright"></i>'.$langs->trans('Export');
|
||||
print '</a>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
258
datanorm_list.php
Executable file
258
datanorm_list.php
Executable file
|
|
@ -0,0 +1,258 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file datanorm_list.php
|
||||
* \ingroup importzugferd
|
||||
* \brief List of Datanorm articles for a supplier
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../main.inc.php")) {
|
||||
$res = @include "../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
require_once './class/datanorm.class.php';
|
||||
require_once './lib/importzugferd.lib.php';
|
||||
|
||||
$langs->loadLangs(array('importzugferd@importzugferd', 'companies', 'products'));
|
||||
|
||||
// Access control
|
||||
if (!$user->hasRight('importzugferd', 'datanorm', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Parameters
|
||||
$fk_soc = GETPOSTINT('fk_soc');
|
||||
$search_article = GETPOST('search_article', 'alpha');
|
||||
$search_text = GETPOST('search_text', 'alpha');
|
||||
$limit = GETPOSTINT('limit') ?: $conf->liste_limit;
|
||||
$page = GETPOSTINT('page');
|
||||
$offset = $limit * $page;
|
||||
$sortfield = GETPOST('sortfield', 'aZ09comma');
|
||||
$sortorder = GETPOST('sortorder', 'aZ09comma');
|
||||
|
||||
if (empty($sortfield)) {
|
||||
$sortfield = 'article_number';
|
||||
}
|
||||
if (empty($sortorder)) {
|
||||
$sortorder = 'ASC';
|
||||
}
|
||||
|
||||
// Check supplier
|
||||
if ($fk_soc <= 0) {
|
||||
header('Location: datanorm.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$supplier = new Societe($db);
|
||||
if ($supplier->fetch($fk_soc) <= 0) {
|
||||
header('Location: datanorm.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Objects
|
||||
$form = new Form($db);
|
||||
$datanorm = new Datanorm($db);
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$title = $langs->trans('DatanormArticles').' - '.$supplier->name;
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-datanorm-list');
|
||||
|
||||
// Build SQL
|
||||
$sql = "SELECT rowid, article_number, short_text1, short_text2, ean,";
|
||||
$sql .= " manufacturer_ref, manufacturer_name, unit_code, price, price_unit,";
|
||||
$sql .= " discount_group, product_group";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."importzugferd_datanorm";
|
||||
$sql .= " WHERE fk_soc = ".(int)$fk_soc;
|
||||
$sql .= " AND entity = ".(int)$conf->entity;
|
||||
$sql .= " AND active = 1";
|
||||
|
||||
// Search filters
|
||||
if (!empty($search_article)) {
|
||||
$sql .= " AND (article_number LIKE '%".$db->escape($search_article)."%'";
|
||||
$sql .= " OR ean LIKE '%".$db->escape($search_article)."%'";
|
||||
$sql .= " OR manufacturer_ref LIKE '%".$db->escape($search_article)."%')";
|
||||
}
|
||||
if (!empty($search_text)) {
|
||||
$sql .= " AND (short_text1 LIKE '%".$db->escape($search_text)."%'";
|
||||
$sql .= " OR short_text2 LIKE '%".$db->escape($search_text)."%')";
|
||||
}
|
||||
|
||||
// Count total
|
||||
$sqlcount = preg_replace('/^SELECT .* FROM/', 'SELECT COUNT(*) as nb FROM', $sql);
|
||||
$resqlcount = $db->query($sqlcount);
|
||||
$total = 0;
|
||||
if ($resqlcount) {
|
||||
$objcount = $db->fetch_object($resqlcount);
|
||||
$total = $objcount->nb;
|
||||
}
|
||||
|
||||
// Sort and limit
|
||||
$sql .= $db->order($sortfield, $sortorder);
|
||||
$sql .= $db->plimit($limit + 1, $offset);
|
||||
|
||||
// Header with back link
|
||||
$linkback = '<a href="datanorm.php">'.$langs->trans("Back").'</a>';
|
||||
print load_fiche_titre($title, $linkback, 'fa-database');
|
||||
|
||||
// Search form
|
||||
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
print '<input type="hidden" name="fk_soc" value="'.$fk_soc.'">';
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
|
||||
// Header row
|
||||
print '<tr class="liste_titre">';
|
||||
print_liste_field_titre('ArticleNumber', $_SERVER['PHP_SELF'], 'article_number', '', '&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('Description', $_SERVER['PHP_SELF'], 'short_text1', '', '&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('EAN', $_SERVER['PHP_SELF'], 'ean', '', '&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('Manufacturer', $_SERVER['PHP_SELF'], 'manufacturer_name', '', '&fk_soc='.$fk_soc, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('Price', $_SERVER['PHP_SELF'], 'price', '', '&fk_soc='.$fk_soc, 'class="right"', $sortfield, $sortorder);
|
||||
print_liste_field_titre('Unit', $_SERVER['PHP_SELF'], 'unit_code', '', '&fk_soc='.$fk_soc, 'class="center"', $sortfield, $sortorder);
|
||||
print '<td class="liste_titre"></td>';
|
||||
print '</tr>';
|
||||
|
||||
// Search row
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td><input type="text" name="search_article" value="'.dol_escape_htmltag($search_article).'" class="flat width150"></td>';
|
||||
print '<td><input type="text" name="search_text" value="'.dol_escape_htmltag($search_text).'" class="flat width200"></td>';
|
||||
print '<td></td>';
|
||||
print '<td></td>';
|
||||
print '<td></td>';
|
||||
print '<td></td>';
|
||||
print '<td class="center">';
|
||||
print '<input type="submit" class="button small" value="'.$langs->trans('Search').'">';
|
||||
print ' <a href="'.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'" class="button small">'.$langs->trans('Reset').'</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Data rows
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
$num = $db->num_rows($resql);
|
||||
$i = 0;
|
||||
|
||||
while ($i < min($num, $limit)) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Article number
|
||||
print '<td class="nowrap">';
|
||||
print '<strong>'.dol_escape_htmltag($obj->article_number).'</strong>';
|
||||
print '</td>';
|
||||
|
||||
// Description
|
||||
print '<td>';
|
||||
print dol_escape_htmltag($obj->short_text1);
|
||||
if (!empty($obj->short_text2)) {
|
||||
print '<br><span class="opacitymedium small">'.dol_escape_htmltag($obj->short_text2).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// EAN
|
||||
print '<td>';
|
||||
if (!empty($obj->ean)) {
|
||||
print '<span class="opacitymedium"><i class="fas fa-barcode paddingright"></i></span>';
|
||||
print dol_escape_htmltag($obj->ean);
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Manufacturer
|
||||
print '<td>';
|
||||
if (!empty($obj->manufacturer_name)) {
|
||||
print dol_escape_htmltag($obj->manufacturer_name);
|
||||
}
|
||||
if (!empty($obj->manufacturer_ref)) {
|
||||
print '<br><span class="opacitymedium small">'.dol_escape_htmltag($obj->manufacturer_ref).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Price
|
||||
print '<td class="right nowrap">';
|
||||
$price = $obj->price;
|
||||
if ($obj->price_unit > 1) {
|
||||
print price($price).' / '.$obj->price_unit;
|
||||
} else {
|
||||
print price($price);
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Unit
|
||||
print '<td class="center">';
|
||||
print dol_escape_htmltag($obj->unit_code);
|
||||
print '</td>';
|
||||
|
||||
// Actions placeholder
|
||||
print '<td class="center">';
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($num == 0) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="7" class="opacitymedium center">'.$langs->trans('NoRecordsFound').'</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
$db->free($resql);
|
||||
} else {
|
||||
dol_print_error($db);
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
// Pagination
|
||||
print_barre_liste('', $page, $_SERVER['PHP_SELF'], '&fk_soc='.$fk_soc.'&search_article='.urlencode($search_article).'&search_text='.urlencode($search_text), $sortfield, $sortorder, '', $num, $total, '', 0, '', '', $limit);
|
||||
|
||||
// Stats
|
||||
print '<br>';
|
||||
print '<div class="opacitymedium">';
|
||||
print $langs->trans('TotalArticles').': <strong>'.$total.'</strong>';
|
||||
print '</div>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
1799
datanorm_update.php
Executable file
1799
datanorm_update.php
Executable file
File diff suppressed because it is too large
Load diff
215
docs/DATANORM_FORMAT.md
Executable file
215
docs/DATANORM_FORMAT.md
Executable file
|
|
@ -0,0 +1,215 @@
|
|||
# Datanorm Format Dokumentation
|
||||
|
||||
## Allgemeines
|
||||
|
||||
Datanorm ist ein Dateiformat für den Datenaustausch von Artikelstammdaten zwischen Produktlieferant, Fachgroßhandel und Handwerksbetrieb. Es wird vornehmlich im Baunebengewerbe (Sanitär, Heizung, Elektro, Maler) verwendet.
|
||||
|
||||
**Wichtig:** Datanorm ist kein offener Standard. Die offizielle Spezifikation ist kostenpflichtig über den Krammer Verlag erhältlich.
|
||||
|
||||
## Datanorm Versionen
|
||||
|
||||
- **Datanorm 3.0**: Feste Feldbreiten (128 Zeichen pro Satz), ASCII
|
||||
- **Datanorm 4.0**: Semikolon-getrennte Felder, erweiterte Funktionen
|
||||
- **Datanorm 5.0**: XML-basiert
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
Eine Datanorm-Lieferung besteht aus mehreren Dateien:
|
||||
|
||||
| Datei | Inhalt |
|
||||
|-------|--------|
|
||||
| `DATANORM.001` - `.999` | Artikelstammdaten (A/B-Sätze) |
|
||||
| `DATPREIS.001` - `.999` | Preisdaten (P-Sätze) |
|
||||
| `DATANORM.WRG` | Warengruppen |
|
||||
| `DATANORM.RAB` | Rabattgruppen |
|
||||
|
||||
## Satzarten
|
||||
|
||||
| Kennzeichen | Typ | Beschreibung |
|
||||
|-------------|-----|--------------|
|
||||
| A | Artikelsatz | Stammdaten des Artikels |
|
||||
| B | Ergänzungssatz | Zusatzinfos, EAN, VPE, Langtext |
|
||||
| P | Preissatz | Preisinformationen |
|
||||
| T | Textsatz | Mehrzeilige Texte |
|
||||
| G | Grafiksatz | Bildverknüpfungen |
|
||||
|
||||
## A-Satz (Artikelstammdaten) - Datanorm 4.0 Semikolon-Format
|
||||
|
||||
### Sonepar-Format
|
||||
|
||||
```
|
||||
A;N;ArtNr;TextKz;Kurztext1;Kurztext2;PreisKz;PE;ME;Preis;RabGrp;WG;LangTextKey
|
||||
```
|
||||
|
||||
| Index | Feld | Beschreibung | Beispiel |
|
||||
|-------|------|--------------|----------|
|
||||
| 0 | Satzart | Immer "A" | `A` |
|
||||
| 1 | **Aktionscode** | **N=Neu, A=Ändern, L=Löschen** | `N` |
|
||||
| 2 | Artikelnummer | Eindeutige Nummer | `0480145` |
|
||||
| 3 | Textkennzeichen | Text-Typ | `00` |
|
||||
| 4 | Kurztext 1 | Erste Bezeichnung (max 40 Z.) | `OBO BETT. Verschraubung` |
|
||||
| 5 | Kurztext 2 | Zweite Bezeichnung (max 40 Z.) | `V-TEC PG21 LGR` |
|
||||
| 6 | Preiskennzeichen | 1=Brutto, 2=Netto | `1` |
|
||||
| 7 | **PE (Preiseinheit)** | **CODE** (siehe unten) | `2` |
|
||||
| 8 | ME (Mengeneinheit) | Einheit | `Stck` |
|
||||
| 9 | Preis | In Cent (wenn vorhanden) | `59085` |
|
||||
| 10 | Rabattgruppe | Rabatt-Code | `A12N` |
|
||||
| 11 | Warengruppe | Waren-Code | `303` |
|
||||
| 12 | Langtextschlüssel | Verknüpfung zu Texten | ` ` |
|
||||
|
||||
### Preiseinheit-Codes (PE) - WICHTIG!
|
||||
|
||||
**Die Preiseinheit ist ein CODE, nicht die tatsächliche Menge!**
|
||||
|
||||
| Code | Bedeutung | Divisor |
|
||||
|------|-----------|---------|
|
||||
| 0 (oder leer) | Preis pro 1 Stück | 1 |
|
||||
| 1 | Preis pro 10 Stück | 10 |
|
||||
| 2 | Preis pro 100 Stück | 100 |
|
||||
| 3 | Preis pro 1000 Stück | 1000 |
|
||||
|
||||
**Beispiel:**
|
||||
```
|
||||
A;N;0480145;00;OBO BETT. Verschraubung;V-TEC PG21 LGR;1;2;Stck;...
|
||||
↑
|
||||
PE-Code 2 = pro 100 Stück
|
||||
```
|
||||
|
||||
Wenn DATPREIS den Preis 9997 (= 99,97 €) liefert:
|
||||
- Stückpreis = 99,97 € / 100 = **0,9997 €**
|
||||
|
||||
### Aktionscode - Artikelstatus
|
||||
|
||||
Der Aktionscode gibt an, ob ein Artikel neu ist, geändert wurde oder nicht mehr verfügbar ist:
|
||||
|
||||
| Code | Bedeutung | Verhalten |
|
||||
|------|-----------|-----------|
|
||||
| N | Neu | Artikel wird angelegt |
|
||||
| A | Ändern | Artikel wird aktualisiert |
|
||||
| L | Löschen | Artikel wird als inaktiv markiert (`active=0`) |
|
||||
|
||||
**Wichtig:** Bei `L`-Artikeln wird das Feld `active` auf `0` gesetzt. Diese Artikel erscheinen nicht mehr in Suchergebnissen und können beim Massenupdate als "nicht mehr verfügbar" gekennzeichnet werden.
|
||||
|
||||
## B-Satz (Ergänzungssatz) - Sonepar-Format
|
||||
|
||||
```
|
||||
B;N;ArtNr;Matchcode; ; ;;;;EAN; ; ;0;VPE;;;
|
||||
```
|
||||
|
||||
| Index | Feld | Beschreibung |
|
||||
|-------|------|--------------|
|
||||
| 0 | Satzart | `B` |
|
||||
| 1 | Aktion | N/L/A |
|
||||
| 2 | Artikelnummer | Bezug zum A-Satz |
|
||||
| 3 | Matchcode | Suchbegriff |
|
||||
| 8 | EAN | 13-stellige EAN/GTIN |
|
||||
| 13 | VPE | Verpackungseinheit (tatsächliche Menge) |
|
||||
|
||||
**Hinweis:** Die VPE im B-Satz ist die Verpackungseinheit (z.B. 100 Stück pro Packung), während der PE-Code im A-Satz die Preisbasis definiert. Diese können unterschiedlich sein!
|
||||
|
||||
## P-Satz (Preissatz) - DATPREIS-Datei
|
||||
|
||||
### Format: Mehrere Artikel pro Zeile
|
||||
|
||||
```
|
||||
P;A;ArtNr1;PreisKz1;Preis1;x;Zuschlag1;x;x;x;ArtNr2;PreisKz2;Preis2;x;Zuschlag2;...
|
||||
```
|
||||
|
||||
| Index | Feld | Beschreibung |
|
||||
|-------|------|--------------|
|
||||
| 0 | P | Satzkennung |
|
||||
| 1 | A | Aktionskennung |
|
||||
| 2 | ArtNr | Artikelnummer |
|
||||
| 3 | PreisKz | Preiskennzeichen (2=Nettopreis) |
|
||||
| 4 | Preis | Materialpreis in **Cent** (für A-Satz PE-Einheit!) |
|
||||
| 5 | x | Unbekannt (immer 1 bei Sonepar) |
|
||||
| 6 | Zuschlag | **Metallzuschlag** in Cent (Kupfer/Aluminium) |
|
||||
| 7-10 | x | Weitere Felder (Flags) |
|
||||
|
||||
**Wichtig:** Der Preis in DATPREIS ist bereits für die PE-Einheit aus dem A-Satz angegeben! Keine Normalisierung nötig.
|
||||
|
||||
### Metallzuschlag (für Kabel)
|
||||
|
||||
Bei Kabeln und metallhaltigen Produkten gibt es oft zwei Preiskomponenten:
|
||||
- **Materialpreis** (Preis): Grundpreis des Produkts
|
||||
- **Metallzuschlag** (Zuschlag): Zusatzkosten für Kupfer/Aluminium
|
||||
|
||||
**Gesamtpreis = Materialpreis + Metallzuschlag**
|
||||
|
||||
**Beispiel Kabel NYM-J 5x1,5:**
|
||||
```
|
||||
P;A;0110350;2;2920;2;7629;0;1;0;...
|
||||
```
|
||||
- Materialpreis: 2920 Cent = 29,20 €/100m
|
||||
- Metallzuschlag: 7629 Cent = 76,29 €/100m
|
||||
- **Gesamtpreis: 105,49 €/100m = 1,05 €/m**
|
||||
|
||||
**Beispiel ohne Metallzuschlag:**
|
||||
```
|
||||
P;A;0480145;2;9997;1;0;1;0;1;0;0480146;2;20689;1;0;1;0;1;0;
|
||||
```
|
||||
- Artikel 0480145: Preis = 9997 Cent = 99,97 €
|
||||
|
||||
## Preisberechnung
|
||||
|
||||
### Formel für Stückpreis
|
||||
|
||||
```
|
||||
Stückpreis = Preis / PE_Divisor
|
||||
```
|
||||
|
||||
Wobei PE_Divisor aus dem PE-Code berechnet wird:
|
||||
- Code 0 → Divisor 1
|
||||
- Code 1 → Divisor 10
|
||||
- Code 2 → Divisor 100
|
||||
- Code 3 → Divisor 1000
|
||||
|
||||
### Beispiel
|
||||
|
||||
```
|
||||
Artikel: 0480145
|
||||
DATPREIS: 9997 (Cent) = 99,97 €
|
||||
A-Satz PE-Code: 2 → Divisor 100
|
||||
|
||||
Stückpreis = 99,97 € / 100 = 0,9997 €
|
||||
```
|
||||
|
||||
## Datenbankfelder
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|--------------|
|
||||
| `price` | DOUBLE | Materialpreis aus DATPREIS (in Euro) |
|
||||
| `price_unit` | INT | Konvertierter PE-Divisor (1, 10, 100, 1000) |
|
||||
| `price_unit_code` | TINYINT | Originaler PE-Code (0, 1, 2, 3) |
|
||||
| `price_type` | TINYINT | Preiskennzeichen (1=Brutto, 2=Netto) |
|
||||
| `metal_surcharge` | DOUBLE | Metallzuschlag (Kupfer/Aluminium) in Euro |
|
||||
| `vpe` | INT | VPE aus B-Satz (Verpackungseinheit) |
|
||||
| `action_code` | CHAR(1) | Aktionscode (N=Neu, A=Ändern, L=Löschen) |
|
||||
| `active` | TINYINT | Artikelstatus (1=aktiv, 0=gelöscht bei L) |
|
||||
|
||||
### Preisberechnung mit Metallzuschlag
|
||||
|
||||
```
|
||||
Gesamtpreis = price + metal_surcharge
|
||||
Stückpreis = Gesamtpreis / price_unit
|
||||
```
|
||||
|
||||
**Beispiel:**
|
||||
```
|
||||
price = 29.20 €
|
||||
metal_surcharge = 76.29 €
|
||||
price_unit = 100
|
||||
|
||||
Gesamtpreis = 29.20 + 76.29 = 105.49 €
|
||||
Stückpreis = 105.49 / 100 = 1.0549 €
|
||||
```
|
||||
|
||||
## Quellen
|
||||
|
||||
- [Datanorm Wikipedia](https://de.wikipedia.org/wiki/Datanorm)
|
||||
- [DATANORM.de](https://www.datanorm.de/)
|
||||
- [Comtech Hilfe](https://hilfe.comtech.at/ce/773/html/datanorm_datei.htm)
|
||||
|
||||
## Hinweis
|
||||
|
||||
Diese Dokumentation basiert auf der Analyse von Sonepar-Datanorm-Dateien und öffentlich verfügbaren Informationen. Für die vollständige offizielle Spezifikation wird das Datanorm-Taschenbuch vom Krammer Verlag empfohlen.
|
||||
14
img/README.md
Executable file
14
img/README.md
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
Directory for module image files
|
||||
--------------------------------
|
||||
|
||||
You can put here the .png files of your module:
|
||||
|
||||
|
||||
If the picto of your module is an image (property $picto has been set to 'importzugferd.png@importzugferd', you can put into this
|
||||
directory a .png file called *object_importzugferd.png* (16x16 or 32x32 pixels)
|
||||
|
||||
|
||||
If the picto of an object is an image (property $picto of the object.class.php has been set to 'myobject.png@importzugferd', then you can put into this
|
||||
directory a .png file called *object_myobject.png* (16x16 or 32x32 pixels)
|
||||
|
||||
14
img/object_importzugferd.svg
Executable file
14
img/object_importzugferd.svg
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<!-- Document/Invoice -->
|
||||
<path d="M6 2h14l6 6v20c0 1.1-.9 2-2 2H6c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2z" fill="#4a90d9" stroke="#3a7fc9" stroke-width="1"/>
|
||||
<!-- Folded corner -->
|
||||
<path d="M20 2v6h6" fill="#3a7fc9"/>
|
||||
<!-- Invoice lines -->
|
||||
<rect x="7" y="12" width="12" height="2" rx="1" fill="#fff"/>
|
||||
<rect x="7" y="17" width="10" height="2" rx="1" fill="#fff"/>
|
||||
<rect x="7" y="22" width="8" height="2" rx="1" fill="#fff"/>
|
||||
<!-- Import arrow circle -->
|
||||
<circle cx="24" cy="24" r="7" fill="#27ae60" stroke="#1e8449" stroke-width="1"/>
|
||||
<!-- Import arrow -->
|
||||
<path d="M24 20v6M21 24l3 3 3-3" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 792 B |
3423
import.php
Executable file
3423
import.php
Executable file
File diff suppressed because it is too large
Load diff
194
importzugferdindex.php
Executable file
194
importzugferdindex.php
Executable file
|
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file importzugferdindex.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Home page of the ZUGFeRD Import module
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../main.inc.php")) {
|
||||
$res = @include "../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
|
||||
|
||||
dol_include_once('/importzugferd/class/zugferdimport.class.php');
|
||||
dol_include_once('/importzugferd/lib/importzugferd.lib.php');
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array("importzugferd@importzugferd"));
|
||||
|
||||
// Security check
|
||||
if (!isModEnabled('importzugferd')) {
|
||||
accessforbidden('Module not enabled');
|
||||
}
|
||||
if (!$user->hasRight('importzugferd', 'import', 'read')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
$title = $langs->trans('ModuleImportZugferdName');
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-index');
|
||||
|
||||
print load_fiche_titre($title, '', 'fa-file-import');
|
||||
|
||||
print '<div class="fichecenter">';
|
||||
|
||||
// Statistics box
|
||||
print '<div class="fichethirdleft">';
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans('Statistics').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Count imports by status
|
||||
$sql = "SELECT status, COUNT(*) as nb FROM ".MAIN_DB_PREFIX."importzugferd_import";
|
||||
$sql .= " WHERE entity = ".(int)$conf->entity;
|
||||
$sql .= " GROUP BY status";
|
||||
|
||||
$stats = array(0 => 0, 1 => 0, 2 => 0);
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
$stats[$obj->status] = $obj->nb;
|
||||
}
|
||||
}
|
||||
|
||||
$import = new ZugferdImport($db);
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans('TotalImported').'</td>';
|
||||
print '<td class="right">'.array_sum($stats).'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$import->LibStatut(0, 1).'</td>';
|
||||
print '<td class="right">'.$stats[0].'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$import->LibStatut(1, 1).'</td>';
|
||||
print '<td class="right">'.$stats[1].'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$import->LibStatut(2, 1).'</td>';
|
||||
print '<td class="right">'.$stats[2].'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>'; // fichethirdleft
|
||||
|
||||
// Quick actions and recent imports
|
||||
print '<div class="fichetwothirdright">';
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>'.$langs->trans('QuickActions').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>';
|
||||
print '<a class="button" href="'.dol_buildpath('/importzugferd/import.php', 1).'">';
|
||||
print '<span class="fa fa-file-import paddingright"></span> '.$langs->trans('ZugferdImport');
|
||||
print '</a>';
|
||||
print ' ';
|
||||
print '<a class="button" href="'.dol_buildpath('/importzugferd/list.php', 1).'">';
|
||||
print '<span class="fa fa-list paddingright"></span> '.$langs->trans('ImportList');
|
||||
print '</a>';
|
||||
print ' ';
|
||||
print '<a class="button" href="'.dol_buildpath('/importzugferd/mapping.php', 1).'">';
|
||||
print '<span class="fa fa-exchange-alt paddingright"></span> '.$langs->trans('ProductMapping');
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
// Recent imports
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="6">'.$langs->trans('RecentImports').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
$sql = "SELECT i.rowid, i.ref, i.invoice_number, i.invoice_date, i.seller_name, i.total_ttc, i.status";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."importzugferd_import as i";
|
||||
$sql .= " WHERE i.entity = ".(int)$conf->entity;
|
||||
$sql .= " ORDER BY i.date_creation DESC";
|
||||
$sql .= " LIMIT 10";
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
$num = $db->num_rows($resql);
|
||||
if ($num > 0) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td><a href="'.dol_buildpath('/importzugferd/card.php', 1).'?id='.$obj->rowid.'">'.$obj->ref.'</a></td>';
|
||||
print '<td>'.dol_escape_htmltag($obj->invoice_number).'</td>';
|
||||
print '<td>'.dol_print_date($db->jdate($obj->invoice_date), 'day').'</td>';
|
||||
print '<td>'.dol_escape_htmltag($obj->seller_name).'</td>';
|
||||
print '<td class="right">'.price($obj->total_ttc).' EUR</td>';
|
||||
print '<td>'.$import->LibStatut($obj->status, 0).'</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="6" class="opacitymedium">'.$langs->trans('NoRecordFound').'</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>'; // fichetwothirdright
|
||||
|
||||
print '</div>'; // fichecenter
|
||||
|
||||
print '<div class="clearboth"></div>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
497
langs/de_DE/importzugferd.lang
Executable file
497
langs/de_DE/importzugferd.lang
Executable file
|
|
@ -0,0 +1,497 @@
|
|||
# Übersetzungsdatei für ImportZugferd Modul
|
||||
|
||||
#
|
||||
# Allgemein
|
||||
#
|
||||
ModuleImportZugferdName = ZUGFeRD Import
|
||||
ModuleImportZugferdDesc = Import von ZUGFeRD/Factur-X Rechnungen als Lieferantenrechnungen
|
||||
|
||||
#
|
||||
# Admin-Seite
|
||||
#
|
||||
ImportZugferdSetup = ZUGFeRD Import Einstellungen
|
||||
Settings = Einstellungen
|
||||
ImportZugferdSetupPage = Konfiguration des ZUGFeRD Import Moduls
|
||||
|
||||
# E-Mail Einstellungen
|
||||
IMPORTZUGFERD_IMAP_HOST = IMAP Server
|
||||
IMPORTZUGFERD_IMAP_HOSTTooltip = IMAP Server Hostname (z.B. imap.example.com)
|
||||
IMPORTZUGFERD_IMAP_PORT = IMAP Port
|
||||
IMPORTZUGFERD_IMAP_PORTTooltip = IMAP Server Port (993 für SSL, 143 für STARTTLS)
|
||||
IMPORTZUGFERD_IMAP_USER = IMAP Benutzername
|
||||
IMPORTZUGFERD_IMAP_USERTooltip = E-Mail-Adresse oder Benutzername für IMAP Login
|
||||
IMPORTZUGFERD_IMAP_PASSWORD = IMAP Passwort
|
||||
IMPORTZUGFERD_IMAP_PASSWORDTooltip = Passwort für IMAP Login
|
||||
IMPORTZUGFERD_IMAP_FOLDER = Postfach-Ordner
|
||||
IMPORTZUGFERD_IMAP_FOLDERTooltip = Ordner für Rechnungs-E-Mails (Standard: INBOX)
|
||||
IMPORTZUGFERD_IMAP_SSL = SSL verwenden
|
||||
IMPORTZUGFERD_IMAP_SSLTooltip = SSL-Verschlüsselung für IMAP-Verbindung aktivieren
|
||||
IMPORTZUGFERD_AUTO_CREATE_INVOICE = Rechnungen automatisch erstellen
|
||||
IMPORTZUGFERD_AUTO_CREATE_INVOICETooltip = Lieferantenrechnungen beim Import automatisch erstellen
|
||||
|
||||
# Ordner Import Einstellungen
|
||||
FolderImportSettings = Ordner Import Einstellungen
|
||||
IMPORTZUGFERD_WATCH_FOLDER = Überwachungsordner
|
||||
IMPORTZUGFERD_WATCH_FOLDERTooltip = Ordner für eingehende ZUGFeRD-Rechnungen (lokaler Pfad)
|
||||
IMPORTZUGFERD_ARCHIVE_FOLDER = Archivordner
|
||||
IMPORTZUGFERD_ARCHIVE_FOLDERTooltip = Ordner für erfolgreich importierte Rechnungen
|
||||
IMPORTZUGFERD_ERROR_FOLDER = Fehlerordner
|
||||
IMPORTZUGFERD_ERROR_FOLDERTooltip = Ordner für fehlerhafte Rechnungen (nicht ZUGFeRD oder Importfehler)
|
||||
IMPORTZUGFERD_IMAP_ARCHIVE_FOLDER = IMAP Archivordner
|
||||
IMPORTZUGFERD_IMAP_ARCHIVE_FOLDERTooltip = IMAP-Ordner für archivierte E-Mails nach Import
|
||||
|
||||
#
|
||||
# Über-Seite
|
||||
#
|
||||
About = Über
|
||||
ImportZugferdAbout = Über ZUGFeRD Import
|
||||
ImportZugferdAboutPage = Dieses Modul ermöglicht den Import von ZUGFeRD/Factur-X Rechnungen aus PDF-Dateien.
|
||||
|
||||
#
|
||||
# Menü
|
||||
#
|
||||
ZugferdImport = Rechnung importieren
|
||||
ImportList = Import-Liste
|
||||
ProductMapping = Artikelzuordnung
|
||||
|
||||
#
|
||||
# Import-Seite
|
||||
#
|
||||
UploadZugferdInvoice = ZUGFeRD Rechnung hochladen
|
||||
InvoiceData = Rechnungsdaten
|
||||
InvoiceNumber = Rechnungsnummer
|
||||
InvoiceDate = Rechnungsdatum
|
||||
BuyerReference = Käuferreferenz (Kundennummer)
|
||||
DueDate = Fälligkeitsdatum
|
||||
SupplierAssignment = Lieferantenzuordnung
|
||||
SelectSupplier = Lieferant auswählen
|
||||
AutomaticallyDetected = automatisch erkannt
|
||||
CreateSupplierInvoice = Lieferantenrechnung erstellen
|
||||
CreateSupplierInvoiceAfterImport = Lieferantenrechnung nach Import erstellen
|
||||
MatchedProduct = Zugeordnetes Produkt
|
||||
MatchMethod = Zuordnungsmethode
|
||||
NoProductMatch = Kein Produkt gefunden
|
||||
CreateProduct = Produkt anlegen
|
||||
ImportSuccessful = Rechnung erfolgreich importiert
|
||||
ImportAnother = Weitere importieren
|
||||
ViewInvoice = Rechnung anzeigen
|
||||
ImportedFromZugferd = Importiert aus ZUGFeRD
|
||||
|
||||
#
|
||||
# Status
|
||||
#
|
||||
StatusImported = Importiert
|
||||
StatusProcessed = Verarbeitet
|
||||
StatusError = Fehler
|
||||
Imported = Importiert
|
||||
Processed = Verarbeitet
|
||||
Error = Fehler
|
||||
|
||||
#
|
||||
# Zuordnung
|
||||
#
|
||||
AddMapping = Zuordnung hinzufügen
|
||||
SupplierRef = Lieferanten-Artikelnr.
|
||||
ManufacturerRef = Hersteller-Artikelnr.
|
||||
MappingCreated = Zuordnung erstellt
|
||||
MappingDeleted = Zuordnung gelöscht
|
||||
DeleteMapping = Zuordnung löschen
|
||||
ConfirmDeleteMapping = Möchten Sie diese Zuordnung wirklich löschen?
|
||||
NoMappingsFound = Keine Zuordnungen für diesen Lieferanten gefunden
|
||||
Active = Aktiv
|
||||
Inactive = Inaktiv
|
||||
|
||||
#
|
||||
# Extrafeld
|
||||
#
|
||||
SupplierCustomerNumber = Kundennummer beim Lieferant
|
||||
SupplierCustomerNumberHelp = Ihre Kundennummer bei diesem Lieferanten (für automatische Lieferantenerkennung)
|
||||
|
||||
#
|
||||
# Cronjob
|
||||
#
|
||||
ImportZugferdFromMailbox = ZUGFeRD Rechnungen aus Postfach importieren
|
||||
ImportZugferdScheduled = ZUGFeRD geplanter Import (Ordner und E-Mail)
|
||||
|
||||
#
|
||||
# Fehler
|
||||
#
|
||||
ErrorSupplierRequired = Bitte wählen Sie einen Lieferanten aus
|
||||
ErrorNoFileUploaded = Keine Datei hochgeladen
|
||||
ErrorFileUploadFailed = Datei-Upload fehlgeschlagen
|
||||
ErrorDuplicateInvoice = Rechnung wurde bereits importiert (Duplikat erkannt)
|
||||
ErrorProductNotFound = Produkt nicht gefunden
|
||||
ErrorLineNotFound = Rechnungsposition nicht gefunden
|
||||
|
||||
#
|
||||
# Statistiken / Startseite
|
||||
#
|
||||
Statistics = Statistiken
|
||||
TotalImported = Gesamt importiert
|
||||
QuickActions = Schnellaktionen
|
||||
RecentImports = Letzte Importe
|
||||
ImportRecord = Import-Datensatz
|
||||
|
||||
#
|
||||
# Admin
|
||||
#
|
||||
IMAPSettings = IMAP Einstellungen
|
||||
ImportSettings = Import Einstellungen
|
||||
TestConnection = Verbindung testen
|
||||
ConnectionSuccessful = Verbindung erfolgreich
|
||||
ConnectionFailed = Verbindung fehlgeschlagen
|
||||
ClickTestToCheck = Klicken Sie auf "Verbindung testen" um die Einstellungen zu prüfen
|
||||
SelectFolder = Ordner auswählen
|
||||
FolderSelected = Ordner ausgewählt
|
||||
FoundFolders = Gefundene Ordner
|
||||
IMAPExtensionNotInstalled = PHP IMAP-Erweiterung ist nicht installiert
|
||||
IMAPExtensionHelp = Bitte installieren Sie die PHP IMAP-Erweiterung: sudo pacman -S php-imap (Arch) oder sudo apt install php-imap (Debian/Ubuntu)
|
||||
|
||||
#
|
||||
# Validierung
|
||||
#
|
||||
ValidationResult = Validierung
|
||||
SumValidationOk = OK
|
||||
SumValidationError = Summenabweichung: ZUGFeRD %s € / Dolibarr %s € (Differenz: %s €)
|
||||
BasisQuantityInfo = Preis für %s %s
|
||||
Difference = Differenz
|
||||
ImportResult = Import Ergebnis
|
||||
|
||||
#
|
||||
# Karte / Löschen
|
||||
#
|
||||
DeleteImportRecord = Import-Datensatz löschen
|
||||
ConfirmDeleteImportRecord = Möchten Sie den Import-Datensatz %s wirklich löschen? Dies ermöglicht das erneute Importieren der gleichen Rechnung.
|
||||
RecordDeleted = Datensatz gelöscht
|
||||
XMLContent = XML-Inhalt
|
||||
ClickToExpand = Klicken zum Anzeigen
|
||||
ErrorMessage = Fehlermeldung
|
||||
ForceReimport = Erneuter Import erzwingen
|
||||
ForceReimportHelp = Aktivieren, um Duplikatsprüfung zu umgehen (falls Rechnung bereits importiert wurde)
|
||||
|
||||
#
|
||||
# Produkt Vorlage
|
||||
#
|
||||
ProductTemplate = Vorlage
|
||||
ProductTemplateHelp = Bestehendes Produkt als Vorlage duplizieren und ZUGFeRD-Daten übernehmen
|
||||
ProductCreated = Produkt erfolgreich erstellt
|
||||
|
||||
#
|
||||
# Batch Import
|
||||
#
|
||||
BatchImport = Stapel-Import
|
||||
SelectSource = Quelle auswählen
|
||||
ImportFromFolder = Import aus Ordner
|
||||
ImportFromIMAP = Import aus E-Mail Postfach
|
||||
StartImport = Import starten
|
||||
Files = Dateien
|
||||
BatchImportComplete = Import abgeschlossen: %s erfolgreich, %s fehlerhaft, %s übersprungen
|
||||
BatchImportNotConfigured = Kein Überwachungsordner oder IMAP konfiguriert
|
||||
ConfigureModule = Modul konfigurieren
|
||||
ErrorWatchFolderNotConfigured = Überwachungsordner nicht konfiguriert oder nicht vorhanden
|
||||
ErrorIMAPNotConfigured = IMAP nicht konfiguriert
|
||||
NoFilesFound = Keine PDF-Dateien gefunden
|
||||
NoEmailsFound = Keine E-Mails gefunden
|
||||
Success = Erfolgreich
|
||||
Skipped = Übersprungen
|
||||
Archived = Archiviert
|
||||
|
||||
#
|
||||
# Manueller Workflow
|
||||
#
|
||||
StatusPending = Manueller Eingriff
|
||||
PendingImports = Ausstehende Importe
|
||||
NoPendingImports = Keine ausstehenden Importe
|
||||
ManualInterventionRequired = Manueller Eingriff erforderlich
|
||||
ProductsNotAssigned = Produkte nicht zugeordnet
|
||||
SupplierNotAssigned = Lieferant nicht zugeordnet
|
||||
ReadyToCreateInvoice = Bereit zur Rechnungserstellung
|
||||
AssignProduct = Produkt zuordnen
|
||||
ProductAssigned = Produkt zugeordnet
|
||||
ProductRemoved = Produktzuordnung entfernt
|
||||
SupplierUpdated = Lieferant aktualisiert
|
||||
ManualAssignment = Manuelle Zuordnung
|
||||
InvoiceCreatedSuccessfully = Rechnung erfolgreich erstellt
|
||||
ImportRecordCreated = Import-Datensatz erstellt
|
||||
ErrorNotAllProductsAssigned = Nicht alle Produkte zugeordnet
|
||||
BackToList = Zurück zur Liste
|
||||
ErrorRecordNotFound = Datensatz nicht gefunden
|
||||
FinishImport = Abschließen
|
||||
ImportFinished = Import abgeschlossen
|
||||
ImportLinkedToExistingInvoice = Import mit bestehender Rechnung %s verknüpft
|
||||
|
||||
#
|
||||
# Datanorm
|
||||
#
|
||||
DatanormCatalogs = Datanorm Kataloge
|
||||
DatanormSettings = Datanorm Einstellungen
|
||||
IMPORTZUGFERD_DATANORM_MARKUP = Preisaufschlag (%)
|
||||
IMPORTZUGFERD_DATANORM_MARKUPTooltip = Prozentualer Aufschlag auf den Datanorm-Einkaufspreis für den Verkaufspreis
|
||||
IMPORTZUGFERD_DATANORM_SEARCH_ALL = In allen Lieferanten-Katalogen suchen
|
||||
IMPORTZUGFERD_DATANORM_SEARCH_ALLTooltip = Bei Aktivierung wird nicht nur im Katalog des aktuellen Lieferanten gesucht, sondern in allen Datanorm-Katalogen
|
||||
|
||||
# Accounting Settings (Standard-Konten für neue Produkte)
|
||||
AccountingSettings = Buchungskonten für neue Produkte
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL = Erlöskonto (Verkauf)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELLTooltip = Standard-Erlöskonto für neue Produkte aus Datanorm (z.B. 700000)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA = Erlöskonto (innergemeinschaftlich)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRATooltip = Erlöskonto für innergemeinschaftliche Lieferungen (z.B. 700100)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT = Erlöskonto (Export)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORTTooltip = Erlöskonto für Exporte außerhalb EU (z.B. 700200)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY = Aufwandskonto (Einkauf)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUYTooltip = Standard-Aufwandskonto für neue Produkte aus Datanorm (z.B. 400000)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA = Aufwandskonto (innergemeinschaftlich)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRATooltip = Aufwandskonto für innergemeinschaftliche Erwerbe (z.B. 400100)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT = Aufwandskonto (Import)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORTTooltip = Aufwandskonto für Importe von außerhalb EU (z.B. 400200)
|
||||
|
||||
UploadDatanorm = Datanorm hochladen
|
||||
DatanormFiles = Datanorm Dateien
|
||||
DatanormFileHelp = DATANORM.001, DATANORM.WRG oder XML-Dateien (Datanorm 4.0/5.0)
|
||||
DeleteExisting = Vorhandene Artikel löschen
|
||||
DeleteExistingHelp = Löscht alle vorhandenen Artikel dieses Lieferanten vor dem Import
|
||||
DatanormImportSuccess = %s Artikel erfolgreich importiert
|
||||
DatanormImportFailed = Datanorm Import fehlgeschlagen
|
||||
DatanormNoArticlesFound = Keine Artikel in der Datanorm-Datei gefunden
|
||||
NoDatanormData = Keine Datanorm-Daten vorhanden
|
||||
DatanormDeleted = %s Artikel gelöscht
|
||||
DatanormDeleteFailed = Löschen fehlgeschlagen
|
||||
DeleteDatanorm = Datanorm-Katalog löschen
|
||||
ConfirmDeleteDatanorm = Möchten Sie alle Datanorm-Artikel von %s löschen?
|
||||
DatanormArticles = Datanorm Artikel
|
||||
ArticleNumber = Artikelnummer
|
||||
ArticleCount = Artikelanzahl
|
||||
LastImport = Letzter Import
|
||||
ViewArticles = Artikel anzeigen
|
||||
TotalArticles = Gesamtanzahl Artikel
|
||||
DatanormSettingsInfo = Preisaufschlag und Suchverhalten können in den Moduleinstellungen konfiguriert werden:
|
||||
CreateFromDatanorm = Aus Datanorm
|
||||
CreateFromDatanormHelp = Neues Produkt aus Datanorm-Daten anlegen
|
||||
ProductCreatedFromDatanorm = Produkt %s aus Datanorm erstellt
|
||||
DatanormArticleNotFound = Kein Datanorm-Artikel für Artikelnummer '%s' gefunden
|
||||
CreateAllFromDatanorm = Alle aus Datanorm
|
||||
CreateAllFromDatanormHelp = Alle fehlenden Produkte aus Datanorm-Daten anlegen
|
||||
DatanormBatchCreated = %s Produkte aus Datanorm erstellt
|
||||
DatanormBatchAssigned = %s vorhandene Produkte zugeordnet
|
||||
DatanormBatchErrors = %s Produkte konnten nicht erstellt werden
|
||||
DatanormBatchNoMatches = Keine passenden Datanorm-Artikel gefunden
|
||||
PreviewDatanormMatches = Datanorm Vorschau
|
||||
DatanormPreview = Datanorm Vorschau - Gefundene Übereinstimmungen
|
||||
Matches = Treffer
|
||||
InvoiceProductName = Rechnung Bezeichnung
|
||||
DatanormProductName = Datanorm Bezeichnung
|
||||
InvoicePrice = Rechnungspreis
|
||||
DatanormPrice = Datanorm EK
|
||||
PurchasePrice = Einkaufspreis
|
||||
SellingPrice = Verkaufspreis
|
||||
ProductAlreadyExists = Produkt existiert bereits
|
||||
Assign = Zuordnen
|
||||
Create = Anlegen
|
||||
ToCreate = anzulegen
|
||||
ToAssign = zuzuordnen
|
||||
ConfirmAndCreateProducts = Bestätigen und Produkte anlegen
|
||||
CreateAllWithoutPreview = Direkt anlegen
|
||||
ConfirmCreateAllWithoutPreview = Alle passenden Produkte aus Datanorm anlegen (ohne Vorschau)?
|
||||
AssignAllFromDatanorm = Alle zuordnen
|
||||
ConfirmAssignAllFromDatanorm = Alle vorhandenen Produkte aus Datanorm zuordnen?
|
||||
NoProductsToAssign = Keine vorhandenen Produkte zum Zuordnen gefunden
|
||||
ProductsAssignedFromDatanorm = %s Produkte wurden aus Datanorm zugeordnet
|
||||
DatanormMatchesFoundNotAssigned = %s Datanorm-Treffer gefunden (Produkte können mit "Direkt anlegen" erstellt werden)
|
||||
ShowRawDatanorm = Rohdaten anzeigen
|
||||
|
||||
#
|
||||
# Scheduling
|
||||
#
|
||||
SchedulingSettings = Zeitplanung
|
||||
IMPORTZUGFERD_IMPORT_FREQUENCY = Import-Häufigkeit
|
||||
IMPORTZUGFERD_IMPORT_FREQUENCYTooltip = Wie oft sollen Ordner und E-Mails automatisch auf neue Rechnungen geprüft werden
|
||||
FrequencyManual = Nur manuell
|
||||
FrequencyHourly = Stündlich
|
||||
FrequencyDaily = Täglich
|
||||
FrequencyWeekly = Wöchentlich
|
||||
ManualImportTrigger = Manueller Import
|
||||
|
||||
#
|
||||
# Folder Browser
|
||||
#
|
||||
FolderBrowser = Ordner-Auswahl
|
||||
Browse = Durchsuchen
|
||||
SelectFolder = Ordner auswählen
|
||||
SelectThisFolder = Diesen Ordner wählen
|
||||
CurrentPath = Aktueller Pfad
|
||||
ParentFolder = Übergeordneter Ordner
|
||||
NoSubfolders = Keine Unterordner
|
||||
NotConfigured = Nicht konfiguriert
|
||||
ErrorFolderNotFound = Ordner nicht gefunden
|
||||
Go = Los
|
||||
QuickLinks = Schnellzugriff
|
||||
|
||||
#
|
||||
# Folder Validation
|
||||
#
|
||||
FolderValidation = Ordner-Prüfung
|
||||
FolderOK = OK
|
||||
FolderNotFound = Ordner nicht gefunden
|
||||
FolderNotReadable = Ordner nicht lesbar
|
||||
FolderNotWritable = Ordner nicht beschreibbar
|
||||
|
||||
#
|
||||
# Email Notifications
|
||||
#
|
||||
NotificationSettings = E-Mail-Benachrichtigungen
|
||||
IMPORTZUGFERD_NOTIFY_ENABLED = Benachrichtigungen aktivieren
|
||||
IMPORTZUGFERD_NOTIFY_ENABLEDTooltip = E-Mail-Benachrichtigungen für Import-Ereignisse aktivieren
|
||||
IMPORTZUGFERD_NOTIFY_EMAIL = Benachrichtigungs-E-Mail
|
||||
IMPORTZUGFERD_NOTIFY_EMAILTooltip = E-Mail-Adresse für Import-Benachrichtigungen
|
||||
IMPORTZUGFERD_NOTIFY_MANUAL = Bei manuellem Eingriff
|
||||
IMPORTZUGFERD_NOTIFY_MANUALTooltip = E-Mail senden wenn ein Import manuellen Eingriff benötigt
|
||||
IMPORTZUGFERD_NOTIFY_ERROR = Bei Fehlern
|
||||
IMPORTZUGFERD_NOTIFY_ERRORTooltip = E-Mail senden wenn beim Import ein Fehler auftritt
|
||||
IMPORTZUGFERD_NOTIFY_PRICE_DIFF = Bei Preisabweichungen
|
||||
IMPORTZUGFERD_NOTIFY_PRICE_DIFFTooltip = E-Mail senden wenn Produktpreise um mehr als den Schwellenwert abweichen
|
||||
IMPORTZUGFERD_PRICE_DIFF_THRESHOLD = Preisabweichung Schwelle (%)
|
||||
IMPORTZUGFERD_PRICE_DIFF_THRESHOLDTooltip = Prozentuale Preisabweichung ab der eine Benachrichtigung gesendet wird
|
||||
|
||||
# Email content
|
||||
NotifySubjectManualIntervention = Manueller Eingriff erforderlich: Rechnung %s
|
||||
NotifySubjectError = Import-Fehler: %s
|
||||
NotifySubjectPriceDiff = Preisabweichungen erkannt: Rechnung %s (%s Produkte)
|
||||
NotifyBodyManualIntervention = Der Import der Rechnung %s von %s erfordert manuellen Eingriff.
|
||||
NotifyBodyError = Beim Import der Rechnung/Datei %s ist ein Fehler aufgetreten.
|
||||
NotifyBodyPriceDiff = Bei der Rechnung %s von %s wurden Preisabweichungen von mehr als %s%% erkannt.
|
||||
NotifyLinkToImport = Link zum Import
|
||||
OldPrice = Alter Preis
|
||||
NewPrice = Neuer Preis
|
||||
File = Datei
|
||||
|
||||
# Price comparison
|
||||
DolibarrPrice = Dolibarr Preis
|
||||
PriceIncrease = Preiserhöhung
|
||||
PriceDecrease = Preissenkung
|
||||
NoPriceFound = Kein Preis
|
||||
|
||||
# Test Email
|
||||
TestEmailNotification = E-Mail-Benachrichtigung testen
|
||||
SendTestEmail = Test-E-Mail senden
|
||||
TestEmailSent = Test-E-Mail erfolgreich gesendet an %s
|
||||
TestEmailFailed = Test-E-Mail konnte nicht gesendet werden
|
||||
SendTo = Senden an
|
||||
NotifySubjectTest = Test-E-Mail Benachrichtigung
|
||||
NotifyBodyTest = Dies ist eine Test-E-Mail vom ZUGFeRD Import Modul.
|
||||
NotifyTestInfo = Diese E-Mail bestätigt, dass die E-Mail-Benachrichtigungen korrekt konfiguriert sind.
|
||||
NotifyTestSuccess = Die E-Mail-Konfiguration funktioniert einwandfrei!
|
||||
CurrentSettings = Aktuelle Einstellungen
|
||||
NotificationsNotEnabled = Benachrichtigungen sind nicht aktiviert oder keine E-Mail-Adresse konfiguriert
|
||||
NotifyEmail = Empfänger-E-Mail
|
||||
|
||||
#
|
||||
# Datanorm Massenaktualisierung
|
||||
#
|
||||
DatanormMassUpdate = Datanorm Massenaktualisierung
|
||||
SelectSupplier = Lieferant auswählen
|
||||
SelectASupplier = -- Lieferant wählen --
|
||||
SearchMode = Suchmodus
|
||||
SearchBySupplierProducts = Nach Lieferanten-Produkten suchen
|
||||
ManualSearch = Manuelle Suche
|
||||
SearchTerm = Suchbegriff
|
||||
ArticleNumberOrName = Artikelnummer oder Name
|
||||
AdditionalSearchOptions = Zusätzliche Suchoptionen
|
||||
AlsoSearchByName = Auch nach Name suchen
|
||||
AlsoSearchByEAN = Auch nach EAN suchen
|
||||
AlsoSearchByRef = Auch nach Artikelref. suchen
|
||||
FieldsToCompare = Felder zum Vergleichen
|
||||
OnlyShowDifferences = Nur Unterschiede anzeigen
|
||||
CurrentPrice = Aktueller Preis
|
||||
DatanormPrice = Datanorm Preis
|
||||
CurrentDescription = Aktuelle Beschreibung
|
||||
DatanormDescription = Datanorm Beschreibung
|
||||
CurrentLabel = Aktueller Name
|
||||
DatanormLabel = Datanorm Name
|
||||
DatanormArticle = Datanorm Artikel
|
||||
ProductNotInDatabase = Produkt nicht in Datenbank
|
||||
ApplyChanges = Änderungen übernehmen
|
||||
AddToPending = Zur Liste hinzufügen
|
||||
Pending = Ausstehend
|
||||
PendingChanges = Ausstehende Änderungen
|
||||
NoChanges = Keine Änderungen
|
||||
ApplyAllPendingChanges = Alle ausstehenden Änderungen übernehmen
|
||||
ClearPendingChanges = Ausstehende Änderungen löschen
|
||||
AddedToPendingChanges = Zur Liste hinzugefügt
|
||||
PendingChangesCleared = Ausstehende Änderungen gelöscht
|
||||
ConfirmMassUpdate = Massenaktualisierung bestätigen
|
||||
FollowingProductsWillBeUpdated = Folgende Produkte werden aktualisiert
|
||||
Changes = Änderungen
|
||||
DatanormMassUpdateComplete = Massenaktualisierung abgeschlossen: %s erfolgreich, %s Fehler
|
||||
ProductUpdated = Produkt aktualisiert
|
||||
ErrorUpdatingProduct = Fehler beim Aktualisieren des Produkts
|
||||
NoResultsFound = Keine Ergebnisse gefunden
|
||||
Results = Ergebnisse
|
||||
WithDifferences = mit Unterschieden
|
||||
OnlyShowingDifferences = Nur Unterschiede werden angezeigt
|
||||
|
||||
#
|
||||
# Änderungsprotokoll
|
||||
#
|
||||
DatanormChangeLog = Änderungsprotokoll
|
||||
ChangeHistory = Änderungsverlauf
|
||||
FieldChanged = Geändertes Feld
|
||||
OldValue = Alter Wert
|
||||
NewValue = Neuer Wert
|
||||
DateChange = Änderungsdatum
|
||||
ChangedBy = Geändert von
|
||||
BatchUpdate = Stapelaktualisierung
|
||||
ViewChangeLog = Änderungsprotokoll anzeigen
|
||||
NoChangesRecorded = Keine Änderungen protokolliert
|
||||
PriceChange = Preisänderung
|
||||
DescriptionChange = Beschreibungsänderung
|
||||
LabelChange = Namensänderung
|
||||
|
||||
#
|
||||
# Kupferzuschlag / Metallzuschlag
|
||||
#
|
||||
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
|
||||
WarengruppeHelp = Produktgruppe aus Datanorm (für Rabattsteuerung und Kategorisierung)
|
||||
MetalSurchargeDetected = Metallzuschlag erkannt
|
||||
MetalSurchargeUpdated = Kupferzuschlag aktualisiert auf %s €/Einheit
|
||||
AddAllWithDifferences = Alle mit Unterschieden hinzufügen
|
||||
AddedAllToPendingChanges = %s Produkte zur Aktualisierungsliste hinzugefügt
|
||||
ConfirmAddAllToPending = Alle Produkte mit Unterschieden zur Aktualisierungsliste hinzufügen?
|
||||
Kupfergehalt = Kupfergehalt (kg/km)
|
||||
KupfergehaltHelp = Kupfergewicht pro Kilometer Kabel (konstant je Kabeltyp)
|
||||
CopperSurchargeFromInvoice = Kupferzuschlag aus Rechnung
|
||||
CopperSurchargePerUnit = Kupferzuschlag/Einheit
|
||||
|
||||
#
|
||||
# Widget / Dashboard Box
|
||||
#
|
||||
BoxNewProductsToReview = Neue Produkte prüfen
|
||||
NewProductsToReview = Neue Produkte prüfen
|
||||
NoNewProductsToReview = Keine neuen Produkte zur Überprüfung
|
||||
ShowAll = Alle anzeigen
|
||||
|
||||
#
|
||||
# Multi-Supplier Alternatives
|
||||
#
|
||||
SupplierAlternatives = Lieferanten-Alternativen
|
||||
Suppliers = Lieferanten
|
||||
AddAsPurchasePrice = Als Einkaufspreis hinzufügen
|
||||
SelectSuppliersForPurchasePrices = Wählen Sie die Lieferanten aus, bei denen ein Einkaufspreis hinterlegt werden soll
|
||||
ManufacturerRef = Hersteller-Art.Nr.
|
||||
MissingSupplierPrices = Fehlende Lieferantenpreise
|
||||
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
|
||||
428
langs/en_US/importzugferd.lang
Executable file
428
langs/en_US/importzugferd.lang
Executable file
|
|
@ -0,0 +1,428 @@
|
|||
# Translation file for ImportZugferd module
|
||||
|
||||
#
|
||||
# Generic
|
||||
#
|
||||
ModuleImportZugferdName = ZUGFeRD Import
|
||||
ModuleImportZugferdDesc = Import ZUGFeRD/Factur-X invoices as supplier invoices
|
||||
|
||||
#
|
||||
# Admin page
|
||||
#
|
||||
ImportZugferdSetup = ZUGFeRD Import Setup
|
||||
Settings = Settings
|
||||
ImportZugferdSetupPage = ZUGFeRD Import module configuration
|
||||
|
||||
# Email settings
|
||||
IMPORTZUGFERD_IMAP_HOST = IMAP Server
|
||||
IMPORTZUGFERD_IMAP_HOSTTooltip = IMAP server hostname (e.g. imap.example.com)
|
||||
IMPORTZUGFERD_IMAP_PORT = IMAP Port
|
||||
IMPORTZUGFERD_IMAP_PORTTooltip = IMAP server port (993 for SSL, 143 for STARTTLS)
|
||||
IMPORTZUGFERD_IMAP_USER = IMAP Username
|
||||
IMPORTZUGFERD_IMAP_USERTooltip = Email address or username for IMAP login
|
||||
IMPORTZUGFERD_IMAP_PASSWORD = IMAP Password
|
||||
IMPORTZUGFERD_IMAP_PASSWORDTooltip = Password for IMAP login
|
||||
IMPORTZUGFERD_IMAP_FOLDER = Mailbox Folder
|
||||
IMPORTZUGFERD_IMAP_FOLDERTooltip = Folder to monitor for invoices (default: INBOX)
|
||||
IMPORTZUGFERD_IMAP_SSL = Use SSL
|
||||
IMPORTZUGFERD_IMAP_SSLTooltip = Enable SSL encryption for IMAP connection
|
||||
IMPORTZUGFERD_AUTO_CREATE_INVOICE = Auto-create invoices
|
||||
IMPORTZUGFERD_AUTO_CREATE_INVOICETooltip = Automatically create supplier invoices when importing from mailbox
|
||||
|
||||
# Folder Import Settings
|
||||
FolderImportSettings = Folder Import Settings
|
||||
IMPORTZUGFERD_WATCH_FOLDER = Watch Folder
|
||||
IMPORTZUGFERD_WATCH_FOLDERTooltip = Folder for incoming ZUGFeRD invoices (local path)
|
||||
IMPORTZUGFERD_ARCHIVE_FOLDER = Archive Folder
|
||||
IMPORTZUGFERD_ARCHIVE_FOLDERTooltip = Folder for successfully imported invoices
|
||||
IMPORTZUGFERD_ERROR_FOLDER = Error Folder
|
||||
IMPORTZUGFERD_ERROR_FOLDERTooltip = Folder for failed invoices (not ZUGFeRD or import errors)
|
||||
IMPORTZUGFERD_IMAP_ARCHIVE_FOLDER = IMAP Archive Folder
|
||||
IMPORTZUGFERD_IMAP_ARCHIVE_FOLDERTooltip = IMAP folder for archived emails after import
|
||||
|
||||
#
|
||||
# About page
|
||||
#
|
||||
About = About
|
||||
ImportZugferdAbout = About ZUGFeRD Import
|
||||
ImportZugferdAboutPage = This module allows importing ZUGFeRD/Factur-X invoices from PDF files.
|
||||
|
||||
#
|
||||
# Menu
|
||||
#
|
||||
ZugferdImport = Import Invoice
|
||||
ImportList = Import List
|
||||
ProductMapping = Product Mapping
|
||||
|
||||
#
|
||||
# Import page
|
||||
#
|
||||
UploadZugferdInvoice = Upload ZUGFeRD Invoice
|
||||
InvoiceData = Invoice Data
|
||||
InvoiceNumber = Invoice Number
|
||||
InvoiceDate = Invoice Date
|
||||
BuyerReference = Buyer Reference (Customer No.)
|
||||
DueDate = Due Date
|
||||
SupplierAssignment = Supplier Assignment
|
||||
SelectSupplier = Select Supplier
|
||||
AutomaticallyDetected = automatically detected
|
||||
CreateSupplierInvoice = Create Supplier Invoice
|
||||
CreateSupplierInvoiceAfterImport = Create supplier invoice after import
|
||||
MatchedProduct = Matched Product
|
||||
MatchMethod = Match method
|
||||
NoProductMatch = No product match
|
||||
CreateProduct = Create Product
|
||||
ImportSuccessful = Invoice imported successfully
|
||||
ImportAnother = Import Another
|
||||
ViewInvoice = View Invoice
|
||||
ImportedFromZugferd = Imported from ZUGFeRD
|
||||
|
||||
#
|
||||
# Status
|
||||
#
|
||||
StatusImported = Imported
|
||||
StatusProcessed = Processed
|
||||
StatusError = Error
|
||||
Imported = Imported
|
||||
Processed = Processed
|
||||
Error = Error
|
||||
|
||||
#
|
||||
# Mapping
|
||||
#
|
||||
AddMapping = Add Mapping
|
||||
SupplierRef = Supplier Article No.
|
||||
ManufacturerRef = Manufacturer Ref
|
||||
MappingCreated = Mapping created
|
||||
MappingDeleted = Mapping deleted
|
||||
DeleteMapping = Delete Mapping
|
||||
ConfirmDeleteMapping = Are you sure you want to delete this mapping?
|
||||
NoMappingsFound = No mappings found for this supplier
|
||||
Active = Active
|
||||
Inactive = Inactive
|
||||
|
||||
#
|
||||
# Extrafield
|
||||
#
|
||||
SupplierCustomerNumber = Customer No. at Supplier
|
||||
SupplierCustomerNumberHelp = Your customer number at this supplier (used for automatic supplier detection)
|
||||
|
||||
#
|
||||
# Cronjob
|
||||
#
|
||||
ImportZugferdFromMailbox = Import ZUGFeRD invoices from mailbox
|
||||
ImportZugferdScheduled = ZUGFeRD scheduled import (folder and email)
|
||||
|
||||
#
|
||||
# Errors
|
||||
#
|
||||
ErrorSupplierRequired = Please select a supplier
|
||||
ErrorNoFileUploaded = No file uploaded
|
||||
ErrorFileUploadFailed = File upload failed
|
||||
ErrorDuplicateInvoice = Invoice already imported (duplicate detected)
|
||||
ErrorProductNotFound = Product not found
|
||||
ErrorLineNotFound = Invoice line not found
|
||||
|
||||
#
|
||||
# Statistics / Index
|
||||
#
|
||||
Statistics = Statistics
|
||||
TotalImported = Total Imported
|
||||
QuickActions = Quick Actions
|
||||
RecentImports = Recent Imports
|
||||
ImportRecord = Import Record
|
||||
|
||||
#
|
||||
# Admin
|
||||
#
|
||||
IMAPSettings = IMAP Settings
|
||||
ImportSettings = Import Settings
|
||||
TestConnection = Test Connection
|
||||
ConnectionSuccessful = Connection successful
|
||||
ConnectionFailed = Connection failed
|
||||
ClickTestToCheck = Click "Test Connection" to verify settings
|
||||
SelectFolder = Select Folder
|
||||
FolderSelected = Folder selected
|
||||
FoundFolders = Found folders
|
||||
IMAPExtensionNotInstalled = PHP IMAP extension is not installed
|
||||
IMAPExtensionHelp = Please install the PHP IMAP extension: sudo apt install php-imap (Debian/Ubuntu) or sudo pacman -S php-imap (Arch)
|
||||
|
||||
#
|
||||
# Validation
|
||||
#
|
||||
ValidationResult = Validation
|
||||
SumValidationOk = OK
|
||||
SumValidationError = Sum mismatch: ZUGFeRD %s € / Dolibarr %s € (Difference: %s €)
|
||||
BasisQuantityInfo = Price for %s %s
|
||||
Difference = Difference
|
||||
ImportResult = Import Result
|
||||
|
||||
#
|
||||
# Card / Delete
|
||||
#
|
||||
DeleteImportRecord = Delete import record
|
||||
ConfirmDeleteImportRecord = Are you sure you want to delete import record %s? This will allow re-importing the same invoice.
|
||||
RecordDeleted = Record deleted
|
||||
XMLContent = XML Content
|
||||
ClickToExpand = Click to expand
|
||||
ErrorMessage = Error message
|
||||
ForceReimport = Force reimport
|
||||
ForceReimportHelp = Enable to bypass duplicate check (if invoice was already imported)
|
||||
|
||||
#
|
||||
# Product Template
|
||||
#
|
||||
ProductTemplate = Template
|
||||
ProductTemplateHelp = Duplicate existing product as template and apply ZUGFeRD data
|
||||
ProductCreated = Product created successfully
|
||||
|
||||
#
|
||||
# Batch Import
|
||||
#
|
||||
BatchImport = Batch Import
|
||||
SelectSource = Select Source
|
||||
ImportFromFolder = Import from Folder
|
||||
ImportFromIMAP = Import from Email Mailbox
|
||||
StartImport = Start Import
|
||||
Files = Files
|
||||
BatchImportComplete = Import completed: %s successful, %s failed, %s skipped
|
||||
BatchImportNotConfigured = No watch folder or IMAP configured
|
||||
ConfigureModule = Configure Module
|
||||
ErrorWatchFolderNotConfigured = Watch folder not configured or not found
|
||||
ErrorIMAPNotConfigured = IMAP not configured
|
||||
NoFilesFound = No PDF files found
|
||||
NoEmailsFound = No emails found
|
||||
Success = Success
|
||||
Skipped = Skipped
|
||||
Archived = Archived
|
||||
|
||||
#
|
||||
# Manual Workflow
|
||||
#
|
||||
StatusPending = Manual Review
|
||||
PendingImports = Pending Imports
|
||||
NoPendingImports = No pending imports
|
||||
ManualInterventionRequired = Manual intervention required
|
||||
ProductsNotAssigned = products not assigned
|
||||
SupplierNotAssigned = Supplier not assigned
|
||||
ReadyToCreateInvoice = Ready to create invoice
|
||||
AssignProduct = Assign product
|
||||
ProductAssigned = Product assigned
|
||||
ProductRemoved = Product assignment removed
|
||||
SupplierUpdated = Supplier updated
|
||||
ManualAssignment = Manual assignment
|
||||
InvoiceCreatedSuccessfully = Invoice created successfully
|
||||
ImportRecordCreated = Import record created
|
||||
ErrorNotAllProductsAssigned = Not all products assigned
|
||||
BackToList = Back to list
|
||||
ErrorRecordNotFound = Record not found
|
||||
FinishImport = Finish Import
|
||||
ImportFinished = Import finished
|
||||
ImportLinkedToExistingInvoice = Import linked to existing invoice %s
|
||||
|
||||
#
|
||||
# Datanorm
|
||||
#
|
||||
DatanormCatalogs = Datanorm Catalogs
|
||||
DatanormSettings = Datanorm Settings
|
||||
IMPORTZUGFERD_DATANORM_MARKUP = Price Markup (%)
|
||||
IMPORTZUGFERD_DATANORM_MARKUPTooltip = Percentage markup on Datanorm purchase price for selling price
|
||||
IMPORTZUGFERD_DATANORM_SEARCH_ALL = Search in all supplier catalogs
|
||||
IMPORTZUGFERD_DATANORM_SEARCH_ALLTooltip = When enabled, search all Datanorm catalogs, not just the current supplier
|
||||
|
||||
# Accounting Settings (Default accounts for new products)
|
||||
AccountingSettings = Accounting Codes for New Products
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL = Sales Account (Domestic)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELLTooltip = Default sales account for new products from Datanorm (e.g. 700000)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA = Sales Account (Intra-EU)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRATooltip = Sales account for intra-community deliveries (e.g. 700100)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT = Sales Account (Export)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORTTooltip = Sales account for exports outside EU (e.g. 700200)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY = Purchase Account (Domestic)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUYTooltip = Default purchase account for new products from Datanorm (e.g. 400000)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA = Purchase Account (Intra-EU)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRATooltip = Purchase account for intra-community acquisitions (e.g. 400100)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT = Purchase Account (Import)
|
||||
IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORTTooltip = Purchase account for imports from outside EU (e.g. 400200)
|
||||
|
||||
UploadDatanorm = Upload Datanorm
|
||||
DatanormFiles = Datanorm Files
|
||||
DatanormFileHelp = DATANORM.001, DATANORM.WRG or XML files (Datanorm 4.0/5.0)
|
||||
DeleteExisting = Delete existing articles
|
||||
DeleteExistingHelp = Deletes all existing articles for this supplier before import
|
||||
DatanormImportSuccess = %s articles imported successfully
|
||||
DatanormImportFailed = Datanorm import failed
|
||||
DatanormNoArticlesFound = No articles found in Datanorm file
|
||||
NoDatanormData = No Datanorm data available
|
||||
DatanormDeleted = %s articles deleted
|
||||
DatanormDeleteFailed = Deletion failed
|
||||
DeleteDatanorm = Delete Datanorm catalog
|
||||
ConfirmDeleteDatanorm = Are you sure you want to delete all Datanorm articles from %s?
|
||||
DatanormArticles = Datanorm Articles
|
||||
ArticleNumber = Article Number
|
||||
ArticleCount = Article Count
|
||||
LastImport = Last Import
|
||||
ViewArticles = View Articles
|
||||
TotalArticles = Total Articles
|
||||
DatanormSettingsInfo = Price markup and search behavior can be configured in module settings:
|
||||
CreateFromDatanorm = From Datanorm
|
||||
CreateFromDatanormHelp = Create new product from Datanorm data
|
||||
ProductCreatedFromDatanorm = Product %s created from Datanorm
|
||||
DatanormArticleNotFound = No Datanorm article found for article number '%s'
|
||||
CreateAllFromDatanorm = All from Datanorm
|
||||
CreateAllFromDatanormHelp = Create all missing products from Datanorm data
|
||||
DatanormBatchCreated = %s products created from Datanorm
|
||||
DatanormBatchAssigned = %s existing products assigned
|
||||
DatanormBatchErrors = %s products could not be created
|
||||
DatanormBatchNoMatches = No matching Datanorm articles found
|
||||
PreviewDatanormMatches = Datanorm Preview
|
||||
DatanormPreview = Datanorm Preview - Found Matches
|
||||
Matches = matches
|
||||
InvoiceProductName = Invoice Product Name
|
||||
DatanormProductName = Datanorm Product Name
|
||||
InvoicePrice = Invoice Price
|
||||
DatanormPrice = Datanorm Price
|
||||
PurchasePrice = Purchase Price
|
||||
SellingPrice = Selling Price
|
||||
ProductAlreadyExists = Product already exists
|
||||
Assign = Assign
|
||||
Create = Create
|
||||
ToCreate = to create
|
||||
ToAssign = to assign
|
||||
ConfirmAndCreateProducts = Confirm and Create Products
|
||||
CreateAllWithoutPreview = Create directly
|
||||
ConfirmCreateAllWithoutPreview = Create all matching products from Datanorm (without preview)?
|
||||
AssignAllFromDatanorm = Assign all
|
||||
ConfirmAssignAllFromDatanorm = Assign all existing products from Datanorm?
|
||||
NoProductsToAssign = No existing products found to assign
|
||||
ProductsAssignedFromDatanorm = %s products have been assigned from Datanorm
|
||||
DatanormMatchesFoundNotAssigned = %s Datanorm matches found (products can be created with "Create directly")
|
||||
ShowRawDatanorm = Show raw data
|
||||
|
||||
#
|
||||
# Scheduling
|
||||
#
|
||||
SchedulingSettings = Scheduling
|
||||
IMPORTZUGFERD_IMPORT_FREQUENCY = Import Frequency
|
||||
IMPORTZUGFERD_IMPORT_FREQUENCYTooltip = How often should folders and emails be checked for new invoices automatically
|
||||
FrequencyManual = Manual only
|
||||
FrequencyHourly = Hourly
|
||||
FrequencyDaily = Daily
|
||||
FrequencyWeekly = Weekly
|
||||
ManualImportTrigger = Manual Import
|
||||
|
||||
#
|
||||
# Folder Browser
|
||||
#
|
||||
FolderBrowser = Folder Selection
|
||||
Browse = Browse
|
||||
SelectFolder = Select Folder
|
||||
SelectThisFolder = Select This Folder
|
||||
CurrentPath = Current Path
|
||||
ParentFolder = Parent Folder
|
||||
NoSubfolders = No subfolders
|
||||
NotConfigured = Not configured
|
||||
ErrorFolderNotFound = Folder not found
|
||||
Go = Go
|
||||
QuickLinks = Quick links
|
||||
|
||||
#
|
||||
# Folder Validation
|
||||
#
|
||||
FolderValidation = Folder Validation
|
||||
FolderOK = OK
|
||||
FolderNotFound = Folder not found
|
||||
FolderNotReadable = Folder not readable
|
||||
FolderNotWritable = Folder not writable
|
||||
|
||||
#
|
||||
# Email Notifications
|
||||
#
|
||||
NotificationSettings = Email Notifications
|
||||
IMPORTZUGFERD_NOTIFY_ENABLED = Enable notifications
|
||||
IMPORTZUGFERD_NOTIFY_ENABLEDTooltip = Enable email notifications for import events
|
||||
IMPORTZUGFERD_NOTIFY_EMAIL = Notification email
|
||||
IMPORTZUGFERD_NOTIFY_EMAILTooltip = Email address for import notifications
|
||||
IMPORTZUGFERD_NOTIFY_MANUAL = On manual intervention
|
||||
IMPORTZUGFERD_NOTIFY_MANUALTooltip = Send email when an import requires manual intervention
|
||||
IMPORTZUGFERD_NOTIFY_ERROR = On errors
|
||||
IMPORTZUGFERD_NOTIFY_ERRORTooltip = Send email when an import error occurs
|
||||
IMPORTZUGFERD_NOTIFY_PRICE_DIFF = On price differences
|
||||
IMPORTZUGFERD_NOTIFY_PRICE_DIFFTooltip = Send email when product prices differ by more than the threshold
|
||||
IMPORTZUGFERD_PRICE_DIFF_THRESHOLD = Price difference threshold (%)
|
||||
IMPORTZUGFERD_PRICE_DIFF_THRESHOLDTooltip = Percentage price difference that triggers a notification
|
||||
|
||||
# Email content
|
||||
NotifySubjectManualIntervention = Manual intervention required: Invoice %s
|
||||
NotifySubjectError = Import error: %s
|
||||
NotifySubjectPriceDiff = Price differences detected: Invoice %s (%s products)
|
||||
NotifyBodyManualIntervention = The import of invoice %s from %s requires manual intervention.
|
||||
NotifyBodyError = An error occurred while importing invoice/file %s.
|
||||
NotifyBodyPriceDiff = Invoice %s from %s has price differences of more than %s%%.
|
||||
NotifyLinkToImport = Link to import
|
||||
OldPrice = Old price
|
||||
NewPrice = New price
|
||||
File = File
|
||||
|
||||
# Price comparison
|
||||
DolibarrPrice = Dolibarr Price
|
||||
PriceIncrease = Price increase
|
||||
PriceDecrease = Price decrease
|
||||
NoPriceFound = No price
|
||||
|
||||
# Test Email
|
||||
TestEmailNotification = Test Email Notification
|
||||
SendTestEmail = Send Test Email
|
||||
TestEmailSent = Test email successfully sent to %s
|
||||
TestEmailFailed = Failed to send test email
|
||||
SendTo = Send to
|
||||
NotifySubjectTest = Test Email Notification
|
||||
NotifyBodyTest = This is a test email from the ZUGFeRD Import module.
|
||||
NotifyTestInfo = This email confirms that email notifications are correctly configured.
|
||||
NotifyTestSuccess = The email configuration is working properly!
|
||||
CurrentSettings = Current settings
|
||||
NotificationsNotEnabled = Notifications are not enabled or no email address configured
|
||||
NotifyEmail = Recipient email
|
||||
|
||||
#
|
||||
# Metal Surcharge
|
||||
#
|
||||
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
|
||||
WarengruppeHelp = Product group from Datanorm (for discount control and categorization)
|
||||
MetalSurchargeDetected = Metal surcharge detected
|
||||
MetalSurchargeUpdated = Metal surcharge updated to %s €/unit
|
||||
|
||||
#
|
||||
# Widget / Dashboard Box
|
||||
#
|
||||
BoxNewProductsToReview = New Products to Review
|
||||
NewProductsToReview = New Products to Review
|
||||
NoNewProductsToReview = No new products to review
|
||||
ShowAll = Show all
|
||||
|
||||
#
|
||||
# Multi-Supplier Alternatives
|
||||
#
|
||||
SupplierAlternatives = Supplier Alternatives
|
||||
Suppliers = Suppliers
|
||||
AddAsPurchasePrice = Add as Purchase Price
|
||||
SelectSuppliersForPurchasePrices = Select suppliers where a purchase price should be stored
|
||||
ManufacturerRef = Manufacturer Ref
|
||||
MissingSupplierPrices = Missing Supplier Prices
|
||||
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
|
||||
173
lib/importzugferd.lib.php
Executable file
173
lib/importzugferd.lib.php
Executable file
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file importzugferd/lib/importzugferd.lib.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Library files with common functions for ImportZugferd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Prepare admin pages header
|
||||
*
|
||||
* @return array<array{string,string,string}>
|
||||
*/
|
||||
function importzugferdAdminPrepareHead()
|
||||
{
|
||||
global $langs, $conf;
|
||||
|
||||
// global $db;
|
||||
// $extrafields = new ExtraFields($db);
|
||||
// $extrafields->fetch_name_optionals_label('myobject');
|
||||
|
||||
$langs->load("importzugferd@importzugferd");
|
||||
|
||||
$h = 0;
|
||||
$head = array();
|
||||
|
||||
$head[$h][0] = dol_buildpath("/importzugferd/admin/setup.php", 1);
|
||||
$head[$h][1] = $langs->trans("Settings");
|
||||
$head[$h][2] = 'settings';
|
||||
$h++;
|
||||
|
||||
/*
|
||||
$head[$h][0] = dol_buildpath("/importzugferd/admin/myobject_extrafields.php", 1);
|
||||
$head[$h][1] = $langs->trans("ExtraFields");
|
||||
$nbExtrafields = (isset($extrafields->attributes['myobject']['label']) && is_countable($extrafields->attributes['myobject']['label'])) ? count($extrafields->attributes['myobject']['label']) : 0;
|
||||
if ($nbExtrafields > 0) {
|
||||
$head[$h][1] .= '<span class="badge marginleftonlyshort">' . $nbExtrafields . '</span>';
|
||||
}
|
||||
$head[$h][2] = 'myobject_extrafields';
|
||||
$h++;
|
||||
|
||||
$head[$h][0] = dol_buildpath("/importzugferd/admin/myobjectline_extrafields.php", 1);
|
||||
$head[$h][1] = $langs->trans("ExtraFieldsLines");
|
||||
$nbExtrafields = (isset($extrafields->attributes['myobjectline']['label']) && is_countable($extrafields->attributes['myobjectline']['label'])) ? count($extrafields->attributes['myobject']['label']) : 0;
|
||||
if ($nbExtrafields > 0) {
|
||||
$head[$h][1] .= '<span class="badge marginleftonlyshort">' . $nbExtrafields . '</span>';
|
||||
}
|
||||
$head[$h][2] = 'myobject_extrafieldsline';
|
||||
$h++;
|
||||
*/
|
||||
|
||||
$head[$h][0] = dol_buildpath("/importzugferd/admin/about.php", 1);
|
||||
$head[$h][1] = $langs->trans("About");
|
||||
$head[$h][2] = 'about';
|
||||
$h++;
|
||||
|
||||
// Show more tabs from modules
|
||||
// Entries must be declared in modules descriptor with line
|
||||
//$this->tabs = array(
|
||||
// 'entity:+tabname:Title:@importzugferd:/importzugferd/mypage.php?id=__ID__'
|
||||
//); // to add new tab
|
||||
//$this->tabs = array(
|
||||
// 'entity:-tabname:Title:@importzugferd:/importzugferd/mypage.php?id=__ID__'
|
||||
//); // to remove a tab
|
||||
complete_head_from_modules($conf, $langs, null, $head, $h, 'importzugferd@importzugferd');
|
||||
|
||||
complete_head_from_modules($conf, $langs, null, $head, $h, 'importzugferd@importzugferd', 'remove');
|
||||
|
||||
return $head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get readable label for UN/ECE Recommendation 20 unit codes
|
||||
*
|
||||
* @param string $code UN/ECE unit code (e.g. C62, MTR, LTR)
|
||||
* @return string Readable label or original code if not found
|
||||
*/
|
||||
function zugferdGetUnitLabel($code)
|
||||
{
|
||||
// UN/ECE Recommendation 20 - Common unit codes used in ZUGFeRD/Factur-X
|
||||
$units = array(
|
||||
// Pieces / Count
|
||||
'C62' => 'Stk.', // One (piece/unit)
|
||||
'PCE' => 'Stk.', // Piece
|
||||
'EA' => 'Stk.', // Each
|
||||
'H87' => 'Stk.', // Piece
|
||||
'XPP' => 'Stk.', // Piece
|
||||
'NAR' => 'Stk.', // Number of articles
|
||||
'NMP' => 'Stk.', // Number of packs
|
||||
'NPR' => 'Paar', // Number of pairs
|
||||
'SET' => 'Set', // Set
|
||||
'PR' => 'Paar', // Pair
|
||||
|
||||
// Length
|
||||
'MTR' => 'm', // Metre
|
||||
'CMT' => 'cm', // Centimetre
|
||||
'MMT' => 'mm', // Millimetre
|
||||
'KMT' => 'km', // Kilometre
|
||||
'INH' => 'Zoll', // Inch
|
||||
'FOT' => 'Fuß', // Foot
|
||||
'LM' => 'lfm', // Linear metre
|
||||
|
||||
// Area
|
||||
'MTK' => 'm²', // Square metre
|
||||
'CMK' => 'cm²', // Square centimetre
|
||||
'MMK' => 'mm²', // Square millimetre
|
||||
|
||||
// Volume
|
||||
'MTQ' => 'm³', // Cubic metre
|
||||
'LTR' => 'l', // Litre
|
||||
'MLT' => 'ml', // Millilitre
|
||||
'HLT' => 'hl', // Hectolitre
|
||||
'CMQ' => 'cm³', // Cubic centimetre
|
||||
|
||||
// Mass / Weight
|
||||
'KGM' => 'kg', // Kilogram
|
||||
'GRM' => 'g', // Gram
|
||||
'MGM' => 'mg', // Milligram
|
||||
'TNE' => 't', // Tonne (metric ton)
|
||||
'LBR' => 'lb', // Pound
|
||||
|
||||
// Time
|
||||
'HUR' => 'Std.', // Hour
|
||||
'MIN' => 'Min.', // Minute
|
||||
'SEC' => 'Sek.', // Second
|
||||
'DAY' => 'Tag', // Day
|
||||
'WEE' => 'Woche', // Week
|
||||
'MON' => 'Monat', // Month
|
||||
'ANN' => 'Jahr', // Year
|
||||
|
||||
// Packaging
|
||||
'XBX' => 'Karton', // Box
|
||||
'XCT' => 'Karton', // Carton
|
||||
'XPK' => 'Paket', // Package
|
||||
'XPA' => 'Palette', // Pallet
|
||||
'XSA' => 'Sack', // Sack
|
||||
'XBG' => 'Beutel', // Bag
|
||||
'XBO' => 'Flasche', // Bottle
|
||||
'XCA' => 'Dose', // Can
|
||||
'XRO' => 'Rolle', // Roll
|
||||
'XTU' => 'Tube', // Tube
|
||||
|
||||
// Other
|
||||
'P1' => '%', // Percent
|
||||
'KWH' => 'kWh', // Kilowatt hour
|
||||
'MWH' => 'MWh', // Megawatt hour
|
||||
'WTT' => 'W', // Watt
|
||||
'KWT' => 'kW', // Kilowatt
|
||||
);
|
||||
|
||||
$code = strtoupper(trim($code));
|
||||
|
||||
if (isset($units[$code])) {
|
||||
return $units[$code];
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
311
list.php
Executable file
311
list.php
Executable file
|
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file list.php
|
||||
* \ingroup importzugferd
|
||||
* \brief List of imported ZUGFeRD invoices
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../main.inc.php")) {
|
||||
$res = @include "../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
|
||||
dol_include_once('/importzugferd/class/zugferdimport.class.php');
|
||||
dol_include_once('/importzugferd/lib/importzugferd.lib.php');
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array("importzugferd@importzugferd", "bills", "companies"));
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('importzugferd', 'import', 'read')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$massaction = GETPOST('massaction', 'alpha');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
$toselect = GETPOST('toselect', 'array');
|
||||
$contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'zugferdimportlist';
|
||||
|
||||
// Search parameters
|
||||
$search_ref = GETPOST('search_ref', 'alpha');
|
||||
$search_invoice_number = GETPOST('search_invoice_number', 'alpha');
|
||||
$search_seller_name = GETPOST('search_seller_name', 'alpha');
|
||||
$search_status = GETPOST('search_status', 'int');
|
||||
|
||||
$limit = GETPOST('limit', 'int') ? GETPOST('limit', 'int') : $conf->liste_limit;
|
||||
$sortfield = GETPOST('sortfield', 'aZ09comma');
|
||||
$sortorder = GETPOST('sortorder', 'aZ09comma');
|
||||
$page = GETPOSTISSET('pageplusone') ? (GETPOST('pageplusone') - 1) : GETPOST("page", 'int');
|
||||
if (empty($page) || $page < 0 || GETPOST('button_search', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
|
||||
$page = 0;
|
||||
}
|
||||
$offset = $limit * $page;
|
||||
$pageprev = $page - 1;
|
||||
$pagenext = $page + 1;
|
||||
|
||||
if (!$sortfield) {
|
||||
$sortfield = 'i.date_creation';
|
||||
}
|
||||
if (!$sortorder) {
|
||||
$sortorder = 'DESC';
|
||||
}
|
||||
|
||||
// Initialize objects
|
||||
$object = new ZugferdImport($db);
|
||||
$form = new Form($db);
|
||||
$formother = new FormOther($db);
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
if (GETPOST('button_removefilter_x', 'alpha') || GETPOST('button_removefilter.x', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
|
||||
$search_ref = '';
|
||||
$search_invoice_number = '';
|
||||
$search_seller_name = '';
|
||||
$search_status = '';
|
||||
$toselect = array();
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$title = $langs->trans('ImportList');
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-list');
|
||||
|
||||
// Build SQL query
|
||||
$sql = "SELECT i.rowid, i.ref, i.invoice_number, i.invoice_date, i.seller_name, i.seller_vat,";
|
||||
$sql .= " i.buyer_reference, i.total_ht, i.total_ttc, i.currency, i.fk_soc, i.fk_facture_fourn,";
|
||||
$sql .= " i.status, i.error_message, i.date_creation, i.pdf_filename,";
|
||||
$sql .= " s.nom as supplier_name";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."importzugferd_import as i";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = i.fk_soc";
|
||||
$sql .= " WHERE i.entity IN (".getEntity('importzugferd_import').")";
|
||||
|
||||
if (!empty($search_ref)) {
|
||||
$sql .= natural_search('i.ref', $search_ref);
|
||||
}
|
||||
if (!empty($search_invoice_number)) {
|
||||
$sql .= natural_search('i.invoice_number', $search_invoice_number);
|
||||
}
|
||||
if (!empty($search_seller_name)) {
|
||||
$sql .= natural_search('i.seller_name', $search_seller_name);
|
||||
}
|
||||
if ($search_status !== '' && $search_status >= 0) {
|
||||
$sql .= " AND i.status = ".(int)$search_status;
|
||||
}
|
||||
|
||||
// Count total
|
||||
$nbtotalofrecords = '';
|
||||
if (!getDolGlobalInt('MAIN_DISABLE_FULL_SCANLIST')) {
|
||||
$sqlforcount = preg_replace('/^SELECT[^FROM]*FROM/', 'SELECT COUNT(*) as nbtotalofrecords FROM', $sql);
|
||||
$sqlforcount = preg_replace('/ORDER BY .*$/', '', $sqlforcount);
|
||||
$resqlforcount = $db->query($sqlforcount);
|
||||
if ($resqlforcount) {
|
||||
$objforcount = $db->fetch_object($resqlforcount);
|
||||
$nbtotalofrecords = $objforcount->nbtotalofrecords;
|
||||
}
|
||||
$db->free($resqlforcount);
|
||||
|
||||
if (($page * $limit) > $nbtotalofrecords) {
|
||||
$page = 0;
|
||||
$offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$sql .= $db->order($sortfield, $sortorder);
|
||||
$sql .= $db->plimit($limit + 1, $offset);
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if (!$resql) {
|
||||
dol_print_error($db);
|
||||
exit;
|
||||
}
|
||||
|
||||
$num = $db->num_rows($resql);
|
||||
|
||||
// List header
|
||||
$param = '';
|
||||
if (!empty($search_ref)) $param .= '&search_ref='.urlencode($search_ref);
|
||||
if (!empty($search_invoice_number)) $param .= '&search_invoice_number='.urlencode($search_invoice_number);
|
||||
if (!empty($search_seller_name)) $param .= '&search_seller_name='.urlencode($search_seller_name);
|
||||
if ($search_status !== '') $param .= '&search_status='.urlencode($search_status);
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" name="formfilter">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
|
||||
print '<input type="hidden" name="action" value="list">';
|
||||
print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
|
||||
print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
|
||||
print '<input type="hidden" name="page" value="'.$page.'">';
|
||||
|
||||
$newcardbutton = dolGetButtonTitle($langs->trans('Import'), '', 'fa fa-plus-circle', dol_buildpath('/importzugferd/import.php', 1));
|
||||
|
||||
print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, 'fa-file-import', 0, $newcardbutton);
|
||||
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="tagtable nobottomiftotal liste">';
|
||||
|
||||
// Header line
|
||||
print '<tr class="liste_titre_filter">';
|
||||
print '<td class="liste_titre"><input class="flat maxwidth75" type="text" name="search_ref" value="'.dol_escape_htmltag($search_ref).'"></td>';
|
||||
print '<td class="liste_titre"><input class="flat maxwidth100" type="text" name="search_invoice_number" value="'.dol_escape_htmltag($search_invoice_number).'"></td>';
|
||||
print '<td class="liste_titre"></td>';
|
||||
print '<td class="liste_titre"><input class="flat maxwidth150" type="text" name="search_seller_name" value="'.dol_escape_htmltag($search_seller_name).'"></td>';
|
||||
print '<td class="liste_titre"></td>';
|
||||
print '<td class="liste_titre right"></td>';
|
||||
print '<td class="liste_titre">';
|
||||
$arrayofstatus = array(0 => $langs->trans('Imported'), 1 => $langs->trans('Processed'), 2 => $langs->trans('Error'), 3 => $langs->trans('StatusPending'));
|
||||
print $form->selectarray('search_status', $arrayofstatus, $search_status, 1, 0, 0, '', 0, 0, 0, '', 'minwidth75');
|
||||
print '</td>';
|
||||
print '<td class="liste_titre"></td>';
|
||||
print '<td class="liste_titre center">';
|
||||
print '<input type="image" class="liste_titre" name="button_search" src="'.img_picto($langs->trans("Search"), 'search.png', '', '', 1).'" value="'.dol_escape_htmltag($langs->trans("Search")).'" title="'.dol_escape_htmltag($langs->trans("Search")).'">';
|
||||
print '<input type="image" class="liste_titre" name="button_removefilter" src="'.img_picto($langs->trans("RemoveFilter"), 'searchclear.png', '', '', 1).'" value="'.dol_escape_htmltag($langs->trans("RemoveFilter")).'" title="'.dol_escape_htmltag($langs->trans("RemoveFilter")).'">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Column headers
|
||||
print '<tr class="liste_titre">';
|
||||
print_liste_field_titre('Ref', $_SERVER["PHP_SELF"], 'i.ref', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('InvoiceNumber', $_SERVER["PHP_SELF"], 'i.invoice_number', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('InvoiceDate', $_SERVER["PHP_SELF"], 'i.invoice_date', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('Supplier', $_SERVER["PHP_SELF"], 'i.seller_name', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('SupplierInvoice', $_SERVER["PHP_SELF"], '', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('TotalTTC', $_SERVER["PHP_SELF"], 'i.total_ttc', '', $param, '', $sortfield, $sortorder, 'right ');
|
||||
print_liste_field_titre('Status', $_SERVER["PHP_SELF"], 'i.status', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('ValidationResult', $_SERVER["PHP_SELF"], '', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('DateCreation', $_SERVER["PHP_SELF"], 'i.date_creation', '', $param, '', $sortfield, $sortorder, 'center ');
|
||||
print '</tr>';
|
||||
|
||||
// Data rows
|
||||
$i = 0;
|
||||
while ($i < min($num, $limit)) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Ref
|
||||
print '<td class="nowraponall">';
|
||||
print '<a href="'.dol_buildpath('/importzugferd/card.php', 1).'?id='.$obj->rowid.'">'.$obj->ref.'</a>';
|
||||
print '</td>';
|
||||
|
||||
// Invoice number
|
||||
print '<td>'.dol_escape_htmltag($obj->invoice_number).'</td>';
|
||||
|
||||
// Invoice date
|
||||
print '<td>'.dol_print_date($db->jdate($obj->invoice_date), 'day').'</td>';
|
||||
|
||||
// Seller/Supplier
|
||||
print '<td>';
|
||||
if ($obj->fk_soc > 0) {
|
||||
$supplier = new Societe($db);
|
||||
$supplier->fetch($obj->fk_soc);
|
||||
print $supplier->getNomUrl(1);
|
||||
} else {
|
||||
print '<span class="opacitymedium">'.dol_escape_htmltag($obj->seller_name).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Supplier invoice
|
||||
print '<td>';
|
||||
if ($obj->fk_facture_fourn > 0) {
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
||||
$invoice = new FactureFournisseur($db);
|
||||
$invoice->fetch($obj->fk_facture_fourn);
|
||||
print $invoice->getNomUrl(1);
|
||||
} else {
|
||||
print '-';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Total TTC
|
||||
print '<td class="right nowraponall">'.price($obj->total_ttc).'</td>';
|
||||
|
||||
// Status
|
||||
print '<td>';
|
||||
print $object->LibStatut($obj->status, 1);
|
||||
print '</td>';
|
||||
|
||||
// Validation result / Error message
|
||||
print '<td class="tdoverflowmax200">';
|
||||
if ($obj->status == 2 && !empty($obj->error_message)) {
|
||||
// Error status - show error message in red
|
||||
print '<span class="error" title="'.dol_escape_htmltag($obj->error_message).'">';
|
||||
print '<i class="fas fa-exclamation-triangle paddingright"></i>';
|
||||
print dol_trunc(dol_escape_htmltag($obj->error_message), 40);
|
||||
print '</span>';
|
||||
} elseif ($obj->status == 1) {
|
||||
// Processed - show OK
|
||||
print '<span class="ok">';
|
||||
print '<i class="fas fa-check paddingright"></i>';
|
||||
print $langs->trans('SumValidationOk');
|
||||
print '</span>';
|
||||
} else {
|
||||
print '-';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Date creation
|
||||
print '<td class="center">'.dol_print_date($db->jdate($obj->date_creation), 'dayhour').'</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($num == 0) {
|
||||
print '<tr class="oddeven"><td colspan="9" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
||||
}
|
||||
|
||||
$db->free($resql);
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
274
mapping.php
Executable file
274
mapping.php
Executable file
|
|
@ -0,0 +1,274 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ZUGFeRD Import Module
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file mapping.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Product mapping management
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../main.inc.php")) {
|
||||
$res = @include "../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
|
||||
dol_include_once('/importzugferd/class/productmapping.class.php');
|
||||
dol_include_once('/importzugferd/lib/importzugferd.lib.php');
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array("importzugferd@importzugferd", "products", "companies"));
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('importzugferd', 'mapping', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
$id = GETPOST('id', 'int');
|
||||
$supplier_id = GETPOST('supplier_id', 'int');
|
||||
|
||||
// Form fields
|
||||
$supplier_ref = GETPOST('supplier_ref', 'alpha');
|
||||
$product_id = GETPOST('product_id', 'int');
|
||||
$ean = GETPOST('ean', 'alpha');
|
||||
$manufacturer_ref = GETPOST('manufacturer_ref', 'alpha');
|
||||
$description = GETPOST('description', 'alpha');
|
||||
$priority = GETPOST('priority', 'int');
|
||||
|
||||
// Initialize objects
|
||||
$mapping = new ProductMapping($db);
|
||||
$form = new Form($db);
|
||||
|
||||
$error = 0;
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// Add mapping
|
||||
if ($action == 'add') {
|
||||
if (empty($supplier_id) || $supplier_id <= 0) {
|
||||
setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentities('Supplier')), null, 'errors');
|
||||
$error++;
|
||||
}
|
||||
if (empty($supplier_ref)) {
|
||||
setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentities('SupplierRef')), null, 'errors');
|
||||
$error++;
|
||||
}
|
||||
if (empty($product_id) || $product_id <= 0) {
|
||||
setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentities('Product')), null, 'errors');
|
||||
$error++;
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$mapping->fk_soc = $supplier_id;
|
||||
$mapping->supplier_ref = $supplier_ref;
|
||||
$mapping->fk_product = $product_id;
|
||||
$mapping->ean = $ean;
|
||||
$mapping->manufacturer_ref = $manufacturer_ref;
|
||||
$mapping->description = $description;
|
||||
$mapping->priority = $priority;
|
||||
|
||||
$result = $mapping->create($user);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('MappingCreated'), null, 'mesgs');
|
||||
header('Location: '.$_SERVER['PHP_SELF'].'?supplier_id='.$supplier_id);
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($mapping->error, null, 'errors');
|
||||
}
|
||||
}
|
||||
$action = '';
|
||||
}
|
||||
|
||||
// Delete mapping
|
||||
if ($action == 'confirm_delete' && $confirm == 'yes') {
|
||||
$mapping->fetch($id);
|
||||
$save_supplier_id = $mapping->fk_soc;
|
||||
|
||||
$result = $mapping->delete($user);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('MappingDeleted'), null, 'mesgs');
|
||||
header('Location: '.$_SERVER['PHP_SELF'].'?supplier_id='.$save_supplier_id);
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($mapping->error, null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$title = $langs->trans('ProductMapping');
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-mapping');
|
||||
|
||||
print load_fiche_titre($title, '', 'fa-exchange-alt');
|
||||
|
||||
// Confirm delete
|
||||
if ($action == 'delete') {
|
||||
print $form->formconfirm(
|
||||
$_SERVER['PHP_SELF'].'?id='.$id.'&supplier_id='.$supplier_id,
|
||||
$langs->trans('DeleteMapping'),
|
||||
$langs->trans('ConfirmDeleteMapping'),
|
||||
'confirm_delete',
|
||||
'',
|
||||
0,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
// Supplier selection
|
||||
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans('SelectSupplier').'</td>';
|
||||
print '</tr>';
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans('Supplier').'</td>';
|
||||
print '<td>';
|
||||
print $form->select_company($supplier_id, 'supplier_id', 's.fournisseur = 1', 'SelectThirdParty', 0, 0, null, 0, 'minwidth300');
|
||||
print ' <input type="submit" class="button smallpaddingimp" value="'.$langs->trans('Select').'">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
// If supplier selected, show mappings and add form
|
||||
if ($supplier_id > 0) {
|
||||
$supplier = new Societe($db);
|
||||
$supplier->fetch($supplier_id);
|
||||
|
||||
print '<br>';
|
||||
|
||||
// Add new mapping form
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="add">';
|
||||
print '<input type="hidden" name="supplier_id" value="'.$supplier_id.'">';
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="6">'.$langs->trans('AddMapping').' - '.$supplier->getNomUrl(1).'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield fieldrequired">'.$langs->trans('SupplierRef').'</td>';
|
||||
print '<td><input type="text" name="supplier_ref" value="'.dol_escape_htmltag($supplier_ref).'" class="minwidth200" required></td>';
|
||||
print '<td class="fieldrequired">'.$langs->trans('Product').'</td>';
|
||||
print '<td>'.$form->select_produits($product_id, 'product_id', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth300', 0, '', null, 1).'</td>';
|
||||
print '<td>'.$langs->trans('EAN').'</td>';
|
||||
print '<td><input type="text" name="ean" value="'.dol_escape_htmltag($ean).'" class="minwidth150"></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans('ManufacturerRef').'</td>';
|
||||
print '<td><input type="text" name="manufacturer_ref" value="'.dol_escape_htmltag($manufacturer_ref).'" class="minwidth200"></td>';
|
||||
print '<td>'.$langs->trans('Description').'</td>';
|
||||
print '<td><input type="text" name="description" value="'.dol_escape_htmltag($description).'" class="minwidth200"></td>';
|
||||
print '<td>'.$langs->trans('Priority').'</td>';
|
||||
print '<td><input type="number" name="priority" value="'.($priority ?: 0).'" class="width75"></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="6" class="center">';
|
||||
print '<input type="submit" class="button button-primary" value="'.$langs->trans('Add').'">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
// Existing mappings
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>'.$langs->trans('SupplierRef').'</td>';
|
||||
print '<td>'.$langs->trans('Product').'</td>';
|
||||
print '<td>'.$langs->trans('EAN').'</td>';
|
||||
print '<td>'.$langs->trans('ManufacturerRef').'</td>';
|
||||
print '<td>'.$langs->trans('Description').'</td>';
|
||||
print '<td class="center">'.$langs->trans('Priority').'</td>';
|
||||
print '<td class="center">'.$langs->trans('Active').'</td>';
|
||||
print '<td class="center">'.$langs->trans('Action').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
$mappings = $mapping->fetchAllBySupplier($supplier_id);
|
||||
|
||||
if (count($mappings) > 0) {
|
||||
foreach ($mappings as $m) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.dol_escape_htmltag($m['supplier_ref']).'</td>';
|
||||
print '<td>';
|
||||
$product = new Product($db);
|
||||
$product->fetch($m['fk_product']);
|
||||
print $product->getNomUrl(1);
|
||||
print '</td>';
|
||||
print '<td>'.dol_escape_htmltag($m['ean']).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($m['manufacturer_ref']).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($m['description']).'</td>';
|
||||
print '<td class="center">'.$m['priority'].'</td>';
|
||||
print '<td class="center">';
|
||||
print $m['active'] ? img_picto($langs->trans('Active'), 'statut4') : img_picto($langs->trans('Inactive'), 'statut5');
|
||||
print '</td>';
|
||||
print '<td class="center">';
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&id='.$m['id'].'&supplier_id='.$supplier_id.'&token='.newToken().'">';
|
||||
print img_picto($langs->trans('Delete'), 'delete');
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="8" class="opacitymedium">'.$langs->trans('NoMappingsFound').'</td></tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
3
modulebuilder.txt
Executable file
3
modulebuilder.txt
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
# DO NOT DELETE THIS FILE MANUALLY
|
||||
# File to flag module built using official module template.
|
||||
# When this file is present into a module directory, you can edit it with the module builder tool.
|
||||
291
new_products.php
Executable file
291
new_products.php
Executable file
|
|
@ -0,0 +1,291 @@
|
|||
<?php
|
||||
/* Copyright (C) 2024 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file new_products.php
|
||||
* \ingroup importzugferd
|
||||
* \brief List of products starting with "New" that need review after import
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
require '../../main.inc.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formcompany.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array('products', 'stocks', 'importzugferd@importzugferd'));
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('produit', 'lire')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$massaction = GETPOST('massaction', 'alpha');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
$toselect = GETPOST('toselect', 'array');
|
||||
$optioncss = GETPOST('optioncss', 'alpha');
|
||||
|
||||
// Search Criteria
|
||||
$search_ref = GETPOST("search_ref", 'alpha');
|
||||
$search_label = GETPOST("search_label", 'alpha');
|
||||
$search_tosell = GETPOST("search_tosell");
|
||||
$search_tobuy = GETPOST("search_tobuy");
|
||||
|
||||
// Load variable for pagination
|
||||
$limit = GETPOSTINT('limit') ? GETPOSTINT('limit') : $conf->liste_limit;
|
||||
$sortfield = GETPOST('sortfield', 'aZ09comma');
|
||||
$sortorder = GETPOST('sortorder', 'aZ09comma');
|
||||
$page = GETPOSTISSET('pageplusone') ? (GETPOSTINT('pageplusone') - 1) : GETPOSTINT("page");
|
||||
if (empty($page) || $page < 0 || GETPOST('button_search', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
|
||||
$page = 0;
|
||||
}
|
||||
$offset = $limit * $page;
|
||||
$pageprev = $page - 1;
|
||||
$pagenext = $page + 1;
|
||||
if (!$sortfield) {
|
||||
$sortfield = "p.datec";
|
||||
}
|
||||
if (!$sortorder) {
|
||||
$sortorder = "DESC";
|
||||
}
|
||||
|
||||
// Initialize objects
|
||||
$object = new Product($db);
|
||||
$form = new Form($db);
|
||||
|
||||
$arrayfields = array(
|
||||
'p.ref' => array('label' => 'Ref', 'checked' => 1, 'position' => 10),
|
||||
'p.label' => array('label' => 'Label', 'checked' => 1, 'position' => 20),
|
||||
'p.fk_product_type' => array('label' => 'Type', 'checked' => 1, 'position' => 30),
|
||||
'p.price' => array('label' => 'SellingPrice', 'checked' => 1, 'position' => 40),
|
||||
'p.price_ttc' => array('label' => 'SellingPriceTTC', 'checked' => 0, 'position' => 41),
|
||||
'p.tosell' => array('label' => 'OnSell', 'checked' => 1, 'position' => 50),
|
||||
'p.tobuy' => array('label' => 'OnBuy', 'checked' => 1, 'position' => 60),
|
||||
'p.datec' => array('label' => 'DateCreation', 'checked' => 1, 'position' => 70),
|
||||
);
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
if (GETPOST('button_removefilter_x', 'alpha') || GETPOST('button_removefilter.x', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
|
||||
$search_ref = '';
|
||||
$search_label = '';
|
||||
$search_tosell = '';
|
||||
$search_tobuy = '';
|
||||
$toselect = array();
|
||||
$search_array_options = array();
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$title = $langs->trans('NewProductsToReview');
|
||||
$help_url = '';
|
||||
|
||||
llxHeader('', $title, $help_url);
|
||||
|
||||
// Build SQL query
|
||||
$sql = "SELECT p.rowid, p.ref, p.label, p.fk_product_type, p.entity,";
|
||||
$sql .= " p.price, p.price_ttc, p.price_base_type, p.tva_tx,";
|
||||
$sql .= " p.tosell, p.tobuy, p.datec, p.tms";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."product as p";
|
||||
$sql .= " WHERE p.entity IN (".getEntity('product').")";
|
||||
$sql .= " AND p.ref LIKE 'New%'";
|
||||
|
||||
// Add search filters
|
||||
if ($search_ref) {
|
||||
$sql .= natural_search('p.ref', $search_ref);
|
||||
}
|
||||
if ($search_label) {
|
||||
$sql .= natural_search('p.label', $search_label);
|
||||
}
|
||||
if ($search_tosell != '' && $search_tosell >= 0) {
|
||||
$sql .= " AND p.tosell = ".((int) $search_tosell);
|
||||
}
|
||||
if ($search_tobuy != '' && $search_tobuy >= 0) {
|
||||
$sql .= " AND p.tobuy = ".((int) $search_tobuy);
|
||||
}
|
||||
|
||||
// Count total
|
||||
$sqlcount = preg_replace('/^SELECT[^F]*FROM/', 'SELECT COUNT(*) as nbtotalofrecords FROM', $sql);
|
||||
$sqlcount = preg_replace('/ORDER BY.*$/', '', $sqlcount);
|
||||
$resqlcount = $db->query($sqlcount);
|
||||
$nbtotalofrecords = 0;
|
||||
if ($resqlcount) {
|
||||
$objcount = $db->fetch_object($resqlcount);
|
||||
$nbtotalofrecords = $objcount->nbtotalofrecords;
|
||||
}
|
||||
|
||||
// Add sorting
|
||||
$sql .= $db->order($sortfield, $sortorder);
|
||||
$sql .= $db->plimit($limit + 1, $offset);
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if (!$resql) {
|
||||
dol_print_error($db);
|
||||
exit;
|
||||
}
|
||||
|
||||
$num = $db->num_rows($resql);
|
||||
|
||||
$param = '';
|
||||
if ($search_ref) {
|
||||
$param .= '&search_ref='.urlencode($search_ref);
|
||||
}
|
||||
if ($search_label) {
|
||||
$param .= '&search_label='.urlencode($search_label);
|
||||
}
|
||||
if ($search_tosell != '') {
|
||||
$param .= '&search_tosell='.urlencode($search_tosell);
|
||||
}
|
||||
if ($search_tobuy != '') {
|
||||
$param .= '&search_tobuy='.urlencode($search_tobuy);
|
||||
}
|
||||
if ($limit > 0 && $limit != $conf->liste_limit) {
|
||||
$param .= '&limit='.((int) $limit);
|
||||
}
|
||||
|
||||
// List header
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="list">';
|
||||
print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
|
||||
print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
|
||||
|
||||
print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, 'product', 0, '', '', $limit, 0, 0, 1);
|
||||
|
||||
// Info box
|
||||
print '<div class="info">';
|
||||
print $langs->trans('NewProductsToReviewDesc', 'New');
|
||||
print '</div><br>';
|
||||
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="tagtable liste'.($optioncss ? ' '.$optioncss : '').'">';
|
||||
|
||||
// Header row with search fields
|
||||
print '<tr class="liste_titre_filter">';
|
||||
print '<td class="liste_titre"><input type="text" class="flat maxwidth100" name="search_ref" value="'.dol_escape_htmltag($search_ref).'"></td>';
|
||||
print '<td class="liste_titre"><input type="text" class="flat maxwidth200" name="search_label" value="'.dol_escape_htmltag($search_label).'"></td>';
|
||||
print '<td class="liste_titre"></td>';
|
||||
print '<td class="liste_titre right"></td>';
|
||||
print '<td class="liste_titre center">'.$form->selectyesno('search_tosell', $search_tosell, 1, false, 1, 1).'</td>';
|
||||
print '<td class="liste_titre center">'.$form->selectyesno('search_tobuy', $search_tobuy, 1, false, 1, 1).'</td>';
|
||||
print '<td class="liste_titre"></td>';
|
||||
print '<td class="liste_titre center">';
|
||||
print '<input type="image" class="liste_titre" name="button_search" src="'.img_picto($langs->trans("Search"), 'search.png', '', 0, 1).'" value="'.dol_escape_htmltag($langs->trans("Search")).'" title="'.dol_escape_htmltag($langs->trans("Search")).'">';
|
||||
print '<input type="image" class="liste_titre" name="button_removefilter" src="'.img_picto($langs->trans("RemoveFilter"), 'searchclear.png', '', 0, 1).'" value="'.dol_escape_htmltag($langs->trans("RemoveFilter")).'" title="'.dol_escape_htmltag($langs->trans("RemoveFilter")).'">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Column headers
|
||||
print '<tr class="liste_titre">';
|
||||
print_liste_field_titre('Ref', $_SERVER["PHP_SELF"], 'p.ref', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('Label', $_SERVER["PHP_SELF"], 'p.label', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('Type', $_SERVER["PHP_SELF"], 'p.fk_product_type', '', $param, '', $sortfield, $sortorder);
|
||||
print_liste_field_titre('SellingPrice', $_SERVER["PHP_SELF"], 'p.price', '', $param, '', $sortfield, $sortorder, 'right ');
|
||||
print_liste_field_titre('OnSell', $_SERVER["PHP_SELF"], 'p.tosell', '', $param, '', $sortfield, $sortorder, 'center ');
|
||||
print_liste_field_titre('OnBuy', $_SERVER["PHP_SELF"], 'p.tobuy', '', $param, '', $sortfield, $sortorder, 'center ');
|
||||
print_liste_field_titre('DateCreation', $_SERVER["PHP_SELF"], 'p.datec', '', $param, '', $sortfield, $sortorder, 'center ');
|
||||
print_liste_field_titre('', $_SERVER["PHP_SELF"], '', '', $param, '', $sortfield, $sortorder, 'center ');
|
||||
print '</tr>';
|
||||
|
||||
// Data rows
|
||||
$i = 0;
|
||||
while ($i < min($num, $limit)) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
if (!$obj) {
|
||||
break;
|
||||
}
|
||||
|
||||
$product_static = new Product($db);
|
||||
$product_static->id = $obj->rowid;
|
||||
$product_static->ref = $obj->ref;
|
||||
$product_static->label = $obj->label;
|
||||
$product_static->type = $obj->fk_product_type;
|
||||
$product_static->entity = $obj->entity;
|
||||
$product_static->status = $obj->tosell;
|
||||
$product_static->status_buy = $obj->tobuy;
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Ref
|
||||
print '<td class="nowraponall">';
|
||||
print $product_static->getNomUrl(1);
|
||||
print '</td>';
|
||||
|
||||
// Label
|
||||
print '<td class="tdoverflowmax200" title="'.dol_escape_htmltag($obj->label).'">'.dol_escape_htmltag($obj->label).'</td>';
|
||||
|
||||
// Type
|
||||
print '<td>';
|
||||
if ($obj->fk_product_type == 0) {
|
||||
print $langs->trans('Product');
|
||||
} else {
|
||||
print $langs->trans('Service');
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Price
|
||||
print '<td class="right nowraponall">';
|
||||
if ($obj->price_base_type == 'TTC') {
|
||||
print '<span class="amount">'.price($obj->price_ttc).'</span>';
|
||||
} else {
|
||||
print '<span class="amount">'.price($obj->price).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// On sell
|
||||
print '<td class="center">';
|
||||
print $product_static->LibStatut($obj->tosell, 5, 0);
|
||||
print '</td>';
|
||||
|
||||
// On buy
|
||||
print '<td class="center">';
|
||||
print $product_static->LibStatut($obj->tobuy, 5, 1);
|
||||
print '</td>';
|
||||
|
||||
// Date creation
|
||||
print '<td class="center nowraponall">';
|
||||
print dol_print_date($db->jdate($obj->datec), 'dayhour');
|
||||
print '</td>';
|
||||
|
||||
// Action column
|
||||
print '<td class="center">';
|
||||
print '<a class="editfielda" href="'.DOL_URL_ROOT.'/product/card.php?id='.$obj->rowid.'&action=edit&token='.newToken().'">';
|
||||
print img_picto($langs->trans('Edit'), 'edit');
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($num == 0) {
|
||||
print '<tr><td colspan="8" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
// End of page
|
||||
llxFooter();
|
||||
$db->close();
|
||||
19
sql/dolibarr_allversions.sql
Executable file
19
sql/dolibarr_allversions.sql
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
--
|
||||
-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version.
|
||||
--
|
||||
|
||||
-- Add copper surcharge fields to import_line table (v2.8)
|
||||
ALTER TABLE llx_importzugferd_import_line ADD COLUMN copper_surcharge double(24,8) DEFAULT NULL AFTER ean;
|
||||
ALTER TABLE llx_importzugferd_import_line ADD COLUMN copper_surcharge_basis_qty double(24,8) DEFAULT NULL AFTER copper_surcharge;
|
||||
|
||||
-- Add fk_datanorm field to import_line table (v2.9)
|
||||
ALTER TABLE llx_importzugferd_import_line ADD COLUMN fk_datanorm integer DEFAULT NULL AFTER fk_product;
|
||||
|
||||
-- Add missing datanorm fields (v3.0)
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD COLUMN price_unit_code tinyint DEFAULT 0 AFTER price_unit;
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD COLUMN price_type tinyint DEFAULT 1 AFTER price_unit_code;
|
||||
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()
|
||||
13
sql/llx_importzugferd_datanorm.key.sql
Executable file
13
sql/llx_importzugferd_datanorm.key.sql
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
-- ============================================================================
|
||||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD INDEX idx_datanorm_fk_soc (fk_soc);
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD INDEX idx_datanorm_article_number (article_number);
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD INDEX idx_datanorm_ean (ean);
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD INDEX idx_datanorm_manufacturer_ref (manufacturer_ref);
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD INDEX idx_datanorm_matchcode (matchcode);
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD UNIQUE INDEX uk_datanorm_soc_article (fk_soc, article_number, entity);
|
||||
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD CONSTRAINT fk_datanorm_fk_soc FOREIGN KEY (fk_soc) REFERENCES llx_societe(rowid);
|
||||
ALTER TABLE llx_importzugferd_datanorm ADD CONSTRAINT fk_datanorm_fk_user_creat FOREIGN KEY (fk_user_creat) REFERENCES llx_user(rowid);
|
||||
39
sql/llx_importzugferd_datanorm.sql
Executable file
39
sql/llx_importzugferd_datanorm.sql
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
-- ============================================================================
|
||||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
--
|
||||
-- Datanorm-Artikeltabelle: Importierte Artikeldaten aus Datanorm-Dateien
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE llx_importzugferd_datanorm (
|
||||
rowid integer AUTO_INCREMENT PRIMARY KEY NOT NULL,
|
||||
fk_soc integer NOT NULL, -- Lieferant
|
||||
article_number varchar(128) NOT NULL, -- Artikelnummer (Typ A Feld 2)
|
||||
short_text1 varchar(255), -- Kurztext 1 (Typ A Feld 4)
|
||||
short_text2 varchar(255), -- Kurztext 2 (Typ A Feld 5)
|
||||
long_text text, -- Langtext (Typ B)
|
||||
ean varchar(32), -- EAN/GTIN (Typ A Feld 17)
|
||||
manufacturer_ref varchar(128), -- Hersteller-Artikelnummer (Typ A Feld 15)
|
||||
manufacturer_name varchar(128), -- Herstellername (Typ A Feld 16)
|
||||
unit_code varchar(8), -- Mengeneinheit (Typ A Feld 6)
|
||||
price double(24,8) DEFAULT 0, -- Listenpreis/Materialpreis (Typ P)
|
||||
price_unit integer DEFAULT 1, -- Preiseinheit (Stück pro Preis) - konvertiert aus PE-Code
|
||||
price_unit_code tinyint DEFAULT 0, -- Original PE-Code (0=1, 1=10, 2=100, 3=1000)
|
||||
price_type tinyint DEFAULT 1, -- Preiskennzeichen (1=Brutto, 2=Netto)
|
||||
metal_surcharge double(24,8) DEFAULT 0, -- Metallzuschlag/Kupferzuschlag (Typ P)
|
||||
vpe integer DEFAULT NULL, -- VPE aus B-Satz (Verpackungseinheit)
|
||||
discount_group varchar(32), -- Rabattgruppe (Typ A Feld 8)
|
||||
product_group varchar(64), -- Warengruppe (Typ A Feld 9)
|
||||
alt_unit varchar(8), -- Alternative Mengeneinheit
|
||||
alt_unit_factor double(10,4) DEFAULT 1, -- Umrechnungsfaktor
|
||||
weight double(10,4), -- Gewicht in kg
|
||||
matchcode varchar(128), -- Matchcode für Suche (Typ A Feld 3)
|
||||
datanorm_version varchar(8), -- Datanorm Version (4.0, 5.0)
|
||||
action_code char(1) DEFAULT 'N', -- Aktionscode (N=Neu, A=Ändern, L=Löschen)
|
||||
import_date datetime NOT NULL, -- Importzeitpunkt
|
||||
active tinyint DEFAULT 1, -- Aktiv/Inaktiv (0 bei action_code='L')
|
||||
date_creation datetime NOT NULL,
|
||||
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
fk_user_creat integer,
|
||||
fk_user_modif integer,
|
||||
entity integer DEFAULT 1 NOT NULL
|
||||
) ENGINE=innodb;
|
||||
8
sql/llx_importzugferd_datanorm_log.key.sql
Executable file
8
sql/llx_importzugferd_datanorm_log.key.sql
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
|
||||
-- No additional keys needed, all defined in main SQL file
|
||||
25
sql/llx_importzugferd_datanorm_log.sql
Executable file
25
sql/llx_importzugferd_datanorm_log.sql
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
|
||||
CREATE TABLE llx_importzugferd_datanorm_log (
|
||||
rowid integer AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_product integer NOT NULL,
|
||||
fk_soc integer NOT NULL,
|
||||
fk_user integer NOT NULL,
|
||||
datanorm_ref varchar(100),
|
||||
field_changed varchar(50) NOT NULL,
|
||||
old_value text,
|
||||
new_value text,
|
||||
date_change datetime NOT NULL,
|
||||
batch_id varchar(50),
|
||||
entity integer DEFAULT 1 NOT NULL
|
||||
) ENGINE=innodb;
|
||||
|
||||
ALTER TABLE llx_importzugferd_datanorm_log ADD INDEX idx_datanorm_log_product (fk_product);
|
||||
ALTER TABLE llx_importzugferd_datanorm_log ADD INDEX idx_datanorm_log_soc (fk_soc);
|
||||
ALTER TABLE llx_importzugferd_datanorm_log ADD INDEX idx_datanorm_log_date (date_change);
|
||||
ALTER TABLE llx_importzugferd_datanorm_log ADD INDEX idx_datanorm_log_batch (batch_id);
|
||||
14
sql/llx_importzugferd_import.key.sql
Executable file
14
sql/llx_importzugferd_import.key.sql
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
-- ============================================================================
|
||||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE llx_importzugferd_import ADD INDEX idx_importzugferd_import_ref (ref);
|
||||
ALTER TABLE llx_importzugferd_import ADD INDEX idx_importzugferd_import_invoice (invoice_number);
|
||||
ALTER TABLE llx_importzugferd_import ADD UNIQUE INDEX uk_importzugferd_import_hash (file_hash, entity);
|
||||
ALTER TABLE llx_importzugferd_import ADD INDEX idx_importzugferd_import_fk_soc (fk_soc);
|
||||
ALTER TABLE llx_importzugferd_import ADD INDEX idx_importzugferd_import_fk_facture (fk_facture_fourn);
|
||||
ALTER TABLE llx_importzugferd_import ADD INDEX idx_importzugferd_import_status (status);
|
||||
ALTER TABLE llx_importzugferd_import ADD INDEX idx_importzugferd_import_buyer_ref (buyer_reference);
|
||||
|
||||
ALTER TABLE llx_importzugferd_import ADD CONSTRAINT fk_importzugferd_import_fk_soc FOREIGN KEY (fk_soc) REFERENCES llx_societe(rowid);
|
||||
ALTER TABLE llx_importzugferd_import ADD CONSTRAINT fk_importzugferd_import_fk_user_creat FOREIGN KEY (fk_user_creat) REFERENCES llx_user(rowid);
|
||||
40
sql/llx_importzugferd_import.sql
Executable file
40
sql/llx_importzugferd_import.sql
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
-- ============================================================================
|
||||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
--
|
||||
-- This program is free software; you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation; either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE llx_importzugferd_import (
|
||||
rowid integer AUTO_INCREMENT PRIMARY KEY NOT NULL,
|
||||
ref varchar(128) NOT NULL, -- Interne Referenz
|
||||
invoice_number varchar(128) NOT NULL, -- Rechnungsnummer aus ZUGFeRD
|
||||
invoice_date date NOT NULL, -- Rechnungsdatum
|
||||
seller_name varchar(255), -- Lieferantenname aus Rechnung
|
||||
seller_vat varchar(50), -- USt-ID Lieferant
|
||||
buyer_reference varchar(128), -- Kundennummer beim Lieferanten
|
||||
total_ht double(24,8) DEFAULT 0, -- Nettobetrag
|
||||
total_ttc double(24,8) DEFAULT 0, -- Bruttobetrag
|
||||
currency varchar(3) DEFAULT 'EUR', -- Währung
|
||||
fk_soc integer, -- Zugeordneter Lieferant
|
||||
fk_facture_fourn integer, -- Erstellte Lieferantenrechnung
|
||||
xml_content mediumtext, -- Original XML-Inhalt
|
||||
pdf_filename varchar(255), -- Original PDF-Dateiname
|
||||
file_hash varchar(64), -- SHA256 Hash für Duplikatserkennung
|
||||
status integer DEFAULT 0, -- 0=importiert, 1=verarbeitet, 2=fehler
|
||||
error_message text, -- Fehlermeldung falls status=2
|
||||
date_creation datetime NOT NULL, -- Erstellungsdatum
|
||||
date_import datetime, -- Importdatum der Rechnung
|
||||
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
fk_user_creat integer, -- Ersteller
|
||||
fk_user_modif integer, -- Letzter Bearbeiter
|
||||
import_key varchar(14), -- Import-Batch-Key
|
||||
entity integer DEFAULT 1 NOT NULL -- Multi-company
|
||||
) ENGINE=innodb;
|
||||
7
sql/llx_importzugferd_import_line.key.sql
Executable file
7
sql/llx_importzugferd_import_line.key.sql
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
-- ============================================================================
|
||||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE llx_importzugferd_import_line ADD INDEX idx_importzugferd_import_line_fk_import (fk_import);
|
||||
ALTER TABLE llx_importzugferd_import_line ADD INDEX idx_importzugferd_import_line_fk_product (fk_product);
|
||||
ALTER TABLE llx_importzugferd_import_line ADD CONSTRAINT fk_importzugferd_import_line_import FOREIGN KEY (fk_import) REFERENCES llx_importzugferd_import (rowid);
|
||||
38
sql/llx_importzugferd_import_line.sql
Executable file
38
sql/llx_importzugferd_import_line.sql
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
-- ============================================================================
|
||||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
--
|
||||
-- This program is free software; you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation; either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE llx_importzugferd_import_line (
|
||||
rowid integer AUTO_INCREMENT PRIMARY KEY NOT NULL,
|
||||
fk_import integer NOT NULL, -- Referenz zum Import
|
||||
line_id varchar(50), -- Position/Zeilen-ID aus ZUGFeRD
|
||||
supplier_ref varchar(128), -- Lieferanten-Artikelnummer
|
||||
product_name varchar(255), -- Artikelbezeichnung aus ZUGFeRD
|
||||
description text, -- Zusätzliche Beschreibung
|
||||
quantity double(24,8) DEFAULT 1, -- Menge
|
||||
unit_code varchar(10), -- UN/ECE Einheitencode (C62, MTR, etc.)
|
||||
unit_price double(24,8) DEFAULT 0, -- Einzelpreis (berechnet)
|
||||
unit_price_raw double(24,8) DEFAULT 0, -- Original-Einzelpreis
|
||||
basis_quantity double(24,8) DEFAULT 1, -- Basismenge für Preis
|
||||
basis_quantity_unit varchar(10), -- Einheit der Basismenge
|
||||
line_total double(24,8) DEFAULT 0, -- Zeilensumme netto
|
||||
tax_percent double(24,8) DEFAULT 0, -- MwSt-Satz
|
||||
ean varchar(20), -- EAN/GTIN falls vorhanden
|
||||
copper_surcharge double(24,8) DEFAULT NULL, -- Kupferzuschlag pro Einheit
|
||||
copper_surcharge_basis_qty double(24,8) DEFAULT NULL, -- Basismenge für Kupferzuschlag
|
||||
fk_product integer, -- Zugeordnetes Dolibarr-Produkt
|
||||
fk_datanorm integer, -- Zugeordneter Datanorm-Artikel
|
||||
match_method varchar(50), -- Wie wurde Produkt gefunden
|
||||
date_creation datetime NOT NULL,
|
||||
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=innodb;
|
||||
13
sql/llx_importzugferd_productmapping.key.sql
Executable file
13
sql/llx_importzugferd_productmapping.key.sql
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
-- ============================================================================
|
||||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE llx_importzugferd_productmapping ADD INDEX idx_productmapping_fk_soc (fk_soc);
|
||||
ALTER TABLE llx_importzugferd_productmapping ADD INDEX idx_productmapping_fk_product (fk_product);
|
||||
ALTER TABLE llx_importzugferd_productmapping ADD INDEX idx_productmapping_supplier_ref (supplier_ref);
|
||||
ALTER TABLE llx_importzugferd_productmapping ADD INDEX idx_productmapping_ean (ean);
|
||||
ALTER TABLE llx_importzugferd_productmapping ADD UNIQUE INDEX uk_productmapping_soc_ref (fk_soc, supplier_ref, entity);
|
||||
|
||||
ALTER TABLE llx_importzugferd_productmapping ADD CONSTRAINT fk_productmapping_fk_soc FOREIGN KEY (fk_soc) REFERENCES llx_societe(rowid);
|
||||
ALTER TABLE llx_importzugferd_productmapping ADD CONSTRAINT fk_productmapping_fk_product FOREIGN KEY (fk_product) REFERENCES llx_product(rowid);
|
||||
ALTER TABLE llx_importzugferd_productmapping ADD CONSTRAINT fk_productmapping_fk_user_creat FOREIGN KEY (fk_user_creat) REFERENCES llx_user(rowid);
|
||||
22
sql/llx_importzugferd_productmapping.sql
Executable file
22
sql/llx_importzugferd_productmapping.sql
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
-- ============================================================================
|
||||
-- Copyright (C) 2026 ZUGFeRD Import Module
|
||||
--
|
||||
-- Artikelmapping-Tabelle: Zuordnung Lieferanten-Artikelnummern zu Produkten
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE llx_importzugferd_productmapping (
|
||||
rowid integer AUTO_INCREMENT PRIMARY KEY NOT NULL,
|
||||
fk_soc integer NOT NULL, -- Lieferant
|
||||
supplier_ref varchar(128) NOT NULL, -- Lieferanten-Artikelnummer (SellerAssignedID)
|
||||
fk_product integer NOT NULL, -- Dolibarr Produkt
|
||||
ean varchar(32), -- EAN/GTIN (GlobalID)
|
||||
manufacturer_ref varchar(128), -- Hersteller-Artikelnummer
|
||||
description varchar(255), -- Optionale Beschreibung
|
||||
priority integer DEFAULT 0, -- Priorität bei mehreren Mappings
|
||||
active tinyint DEFAULT 1, -- Aktiv/Inaktiv
|
||||
date_creation datetime NOT NULL,
|
||||
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
fk_user_creat integer,
|
||||
fk_user_modif integer,
|
||||
entity integer DEFAULT 1 NOT NULL
|
||||
) ENGINE=innodb;
|
||||
Loading…
Reference in a new issue