V 1.1 Import Zugferd erstellt
This commit is contained in:
commit
424b2379ef
36 changed files with 27109 additions and 0 deletions
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pdfdetach:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(xmllint:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
2026-01-19 - Auftragsbestätigung - Witte.pdf
Executable file
BIN
2026-01-19 - Auftragsbestätigung - Witte.pdf
Executable file
Binary file not shown.
18971
2026-02-04 - Zugferd Rechnung - Sonepar - 9010548449 - 3581,33 EUR.pdf
Executable file
18971
2026-02-04 - Zugferd Rechnung - Sonepar - 9010548449 - 3581,33 EUR.pdf
Executable file
File diff suppressed because one or more lines are too long
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
|
||||
5
ChangeLog.md
Executable file
5
ChangeLog.md
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
# CHANGELOG MODULE IMPORTZUGFERD FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
|
||||
|
||||
## 1.0
|
||||
|
||||
Initial version
|
||||
129
README.md
Executable file
129
README.md
Executable file
|
|
@ -0,0 +1,129 @@
|
|||
# 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
|
||||
|
||||
### 1.1
|
||||
- New persistent import workflow with database storage
|
||||
- Manual product assignment via dropdown
|
||||
- Product removal/reassignment
|
||||
- Status "Pending" for imports requiring manual intervention
|
||||
- Pending imports overview on upload page
|
||||
- UN/ECE unit code translation (C62 → Stk., MTR → m, etc.)
|
||||
- Batch import from folder or IMAP mailbox
|
||||
- IMAP connection test with folder selection
|
||||
- Product template feature (duplicate existing product)
|
||||
|
||||
### 1.0
|
||||
- Initial release
|
||||
- Basic ZUGFeRD/Factur-X import
|
||||
- Automatic product matching
|
||||
- Supplier detection
|
||||
- Duplicate detection
|
||||
|
||||
## 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();
|
||||
286
admin/setup.php
Executable file
286
admin/setup.php
Executable file
|
|
@ -0,0 +1,286 @@
|
|||
<?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();
|
||||
|
||||
// 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_IMAP_ARCHIVE_FOLDER');
|
||||
$item->defaultFieldValue = 'Archive';
|
||||
$item->cssClass = 'minwidth200';
|
||||
|
||||
/*
|
||||
* 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';
|
||||
|
||||
/*
|
||||
* 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);
|
||||
|
||||
// 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>';
|
||||
}
|
||||
|
||||
print '<br>';
|
||||
|
||||
// Page end
|
||||
print dol_get_fiche_end();
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
424
batch.php
Normal file
424
batch.php
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
<?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');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 . '/' . basename($pdf_path);
|
||||
if (rename($pdf_path, $archive_path)) {
|
||||
$result['archived'] = true;
|
||||
}
|
||||
}
|
||||
} elseif ($res == -3) {
|
||||
// Duplicate
|
||||
$result['status'] = 'skipped';
|
||||
$result['message'] = $langs->trans('ErrorDuplicateInvoice');
|
||||
} else {
|
||||
$result['message'] = $actions->error;
|
||||
}
|
||||
|
||||
$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();
|
||||
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/
|
||||
268
card.php
Normal file
268
card.php
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
<?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);
|
||||
|
||||
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: 500px; overflow: auto; background: #f5f5f5; padding: 10px; font-size: 11px; white-space: pre-wrap; word-wrap: break-word;">';
|
||||
print dol_escape_htmltag($formattedXml);
|
||||
print '</pre>';
|
||||
print '</div>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
723
class/actions_importzugferd.class.php
Normal file
723
class/actions_importzugferd.class.php
Normal file
|
|
@ -0,0 +1,723 @@
|
|||
<?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/actions_importzugferd.class.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Actions class for ZUGFeRD import operations
|
||||
*/
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
|
||||
dol_include_once('/importzugferd/class/zugferdparser.class.php');
|
||||
dol_include_once('/importzugferd/class/zugferdimport.class.php');
|
||||
dol_include_once('/importzugferd/class/productmapping.class.php');
|
||||
|
||||
/**
|
||||
* Class ActionsImportZugferd
|
||||
* Handles the import process of ZUGFeRD invoices
|
||||
*/
|
||||
class ActionsImportZugferd
|
||||
{
|
||||
/**
|
||||
* @var DoliDB Database handler
|
||||
*/
|
||||
public $db;
|
||||
|
||||
/**
|
||||
* @var string Error message
|
||||
*/
|
||||
public $error = '';
|
||||
|
||||
/**
|
||||
* @var array Error messages
|
||||
*/
|
||||
public $errors = array();
|
||||
|
||||
/**
|
||||
* @var array Warning messages
|
||||
*/
|
||||
public $warnings = array();
|
||||
|
||||
/**
|
||||
* @var ZugferdParser Parser instance
|
||||
*/
|
||||
public $parser;
|
||||
|
||||
/**
|
||||
* @var ZugferdImport Import record
|
||||
*/
|
||||
public $import;
|
||||
|
||||
/**
|
||||
* @var ProductMapping Mapping helper
|
||||
*/
|
||||
public $mapping;
|
||||
|
||||
/**
|
||||
* @var array Import result data
|
||||
*/
|
||||
public $result = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->parser = new ZugferdParser($db);
|
||||
$this->import = new ZugferdImport($db);
|
||||
$this->mapping = new ProductMapping($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a ZUGFeRD PDF file
|
||||
*
|
||||
* @param string $pdf_path Path to PDF file
|
||||
* @param User $user Current user
|
||||
* @param bool $create_invoice Whether to create supplier invoice
|
||||
* @param bool $force_reimport Whether to bypass duplicate check
|
||||
* @return int <0 if KO, >0 if OK (import record ID)
|
||||
*/
|
||||
public function processPdf($pdf_path, $user, $create_invoice = false, $force_reimport = false)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$this->result = array(
|
||||
'import_id' => 0,
|
||||
'invoice_id' => 0,
|
||||
'supplier_id' => 0,
|
||||
'supplier_found' => false,
|
||||
'is_duplicate' => false,
|
||||
'lines' => array(),
|
||||
'warnings' => array(),
|
||||
);
|
||||
|
||||
// Extract XML from PDF
|
||||
$res = $this->parser->extractFromPdf($pdf_path);
|
||||
if ($res < 0) {
|
||||
$this->error = $this->parser->error;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parse XML
|
||||
$res = $this->parser->parse();
|
||||
if ($res < 0) {
|
||||
$this->error = $this->parser->error;
|
||||
return -2;
|
||||
}
|
||||
|
||||
$invoice_data = $this->parser->getInvoiceData();
|
||||
|
||||
// Check for duplicates
|
||||
$file_hash = $this->parser->getFileHash($pdf_path);
|
||||
if ($this->import->isDuplicate($file_hash)) {
|
||||
if ($force_reimport) {
|
||||
// Delete existing import record to allow reimport
|
||||
$this->deleteExistingImport($file_hash, $user);
|
||||
} else {
|
||||
global $langs;
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
$this->result['is_duplicate'] = true;
|
||||
$this->error = $langs->trans('ErrorDuplicateInvoice');
|
||||
return -3;
|
||||
}
|
||||
}
|
||||
|
||||
// Find supplier
|
||||
$supplier_id = $this->findSupplier($invoice_data);
|
||||
$this->result['supplier_id'] = $supplier_id;
|
||||
$this->result['supplier_found'] = ($supplier_id > 0);
|
||||
|
||||
// Create import record
|
||||
$this->import->invoice_number = $invoice_data['invoice_number'];
|
||||
$this->import->invoice_date = $invoice_data['invoice_date'];
|
||||
$this->import->seller_name = $invoice_data['seller']['name'];
|
||||
$this->import->seller_vat = $invoice_data['seller']['vat_id'];
|
||||
$this->import->buyer_reference = $invoice_data['buyer']['reference'] ?: $invoice_data['buyer']['id'];
|
||||
$this->import->total_ht = $invoice_data['totals']['net'];
|
||||
$this->import->total_ttc = $invoice_data['totals']['gross'];
|
||||
$this->import->currency = $invoice_data['totals']['currency'] ?: 'EUR';
|
||||
$this->import->fk_soc = $supplier_id;
|
||||
$this->import->xml_content = $this->parser->getXmlContent();
|
||||
$this->import->pdf_filename = basename($pdf_path);
|
||||
$this->import->file_hash = $file_hash;
|
||||
$this->import->status = ZugferdImport::STATUS_IMPORTED;
|
||||
$this->import->date_import = dol_now();
|
||||
|
||||
$import_id = $this->import->create($user);
|
||||
if ($import_id < 0) {
|
||||
$this->error = $this->import->error;
|
||||
return -4;
|
||||
}
|
||||
|
||||
$this->result['import_id'] = $import_id;
|
||||
|
||||
// Process line items
|
||||
$this->result['lines'] = $this->processLineItems($invoice_data['lines'], $supplier_id);
|
||||
|
||||
// Copy PDF to documents folder
|
||||
$this->copyToDocuments($pdf_path, $import_id);
|
||||
|
||||
// Create supplier invoice if requested
|
||||
if ($create_invoice && $supplier_id > 0) {
|
||||
$invoice_id = $this->createSupplierInvoice($invoice_data, $supplier_id, $user, $pdf_path);
|
||||
if ($invoice_id > 0) {
|
||||
$this->result['invoice_id'] = $invoice_id;
|
||||
$this->import->fk_facture_fourn = $invoice_id;
|
||||
|
||||
// Check validation result - status may have been set to ERROR in validateTotals()
|
||||
if ($this->import->status != ZugferdImport::STATUS_ERROR) {
|
||||
$this->import->status = ZugferdImport::STATUS_PROCESSED;
|
||||
}
|
||||
$this->import->update($user);
|
||||
|
||||
// Add validation warning if there was a sum mismatch
|
||||
if (!empty($this->result['validation']) && !$this->result['validation']['valid']) {
|
||||
$this->result['warnings'][] = $this->result['validation']['message'];
|
||||
}
|
||||
} else {
|
||||
$this->result['warnings'][] = 'Could not create supplier invoice: ' . $this->error;
|
||||
}
|
||||
}
|
||||
|
||||
return $import_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find supplier by buyer reference (customer number)
|
||||
*
|
||||
* @param array $invoice_data Parsed invoice data
|
||||
* @return int Supplier ID or 0
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process line items and find matching products
|
||||
*
|
||||
* @param array $lines Line items from invoice
|
||||
* @param int $supplier_id Supplier ID
|
||||
* @return array Processed lines with product info
|
||||
*/
|
||||
public function processLineItems($lines, $supplier_id)
|
||||
{
|
||||
$processed = array();
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$processed_line = array(
|
||||
'line_id' => $line['line_id'],
|
||||
'supplier_ref' => $line['product']['seller_id'],
|
||||
'ean' => $line['product']['global_id'],
|
||||
'name' => $line['product']['name'],
|
||||
'description' => $line['product']['description'],
|
||||
'quantity' => $line['quantity'],
|
||||
'unit_code' => $line['unit_code'],
|
||||
'unit_price' => $line['unit_price'],
|
||||
'unit_price_raw' => isset($line['unit_price_raw']) ? $line['unit_price_raw'] : $line['unit_price'],
|
||||
'basis_quantity' => isset($line['basis_quantity']) ? $line['basis_quantity'] : 1,
|
||||
'basis_quantity_unit' => isset($line['basis_quantity_unit']) ? $line['basis_quantity_unit'] : '',
|
||||
'line_total' => $line['line_total'],
|
||||
'tax_percent' => $line['tax_percent'],
|
||||
'fk_product' => 0,
|
||||
'product_ref' => '',
|
||||
'product_label' => '',
|
||||
'match_method' => '',
|
||||
'needs_creation' => false,
|
||||
);
|
||||
|
||||
// Try to find product
|
||||
if ($supplier_id > 0) {
|
||||
$match = $this->mapping->findProduct($supplier_id, $line['product']);
|
||||
if ($match['fk_product'] > 0) {
|
||||
$processed_line['fk_product'] = $match['fk_product'];
|
||||
$processed_line['match_method'] = $match['method'];
|
||||
|
||||
// Get product info
|
||||
$product = new Product($this->db);
|
||||
if ($product->fetch($match['fk_product']) > 0) {
|
||||
$processed_line['product_ref'] = $product->ref;
|
||||
$processed_line['product_label'] = $product->label;
|
||||
}
|
||||
} else {
|
||||
$processed_line['needs_creation'] = true;
|
||||
}
|
||||
} else {
|
||||
$processed_line['needs_creation'] = true;
|
||||
}
|
||||
|
||||
$processed[] = $processed_line;
|
||||
}
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create supplier invoice from parsed data
|
||||
*
|
||||
* @param array $invoice_data Parsed invoice data
|
||||
* @param int $supplier_id Supplier ID
|
||||
* @param User $user Current user
|
||||
* @param string $pdf_path Path to source PDF file (optional)
|
||||
* @return int Invoice ID or <0 if error
|
||||
*/
|
||||
public function createSupplierInvoice($invoice_data, $supplier_id, $user, $pdf_path = '')
|
||||
{
|
||||
global $conf, $langs;
|
||||
|
||||
$invoice = new FactureFournisseur($this->db);
|
||||
|
||||
$invoice->socid = $supplier_id;
|
||||
$invoice->ref_supplier = $invoice_data['invoice_number'];
|
||||
$invoice->date = strtotime($invoice_data['invoice_date']);
|
||||
$invoice->date_echeance = !empty($invoice_data['due_date']) ? strtotime($invoice_data['due_date']) : null;
|
||||
$invoice->note_private = $langs->trans('ImportedFromZugferd') . ' - ' . $this->import->ref;
|
||||
$invoice->multicurrency_code = $invoice_data['totals']['currency'] ?: 'EUR';
|
||||
|
||||
$this->db->begin();
|
||||
|
||||
$invoice_id = $invoice->create($user);
|
||||
if ($invoice_id < 0) {
|
||||
$this->error = $invoice->error;
|
||||
$this->db->rollback();
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Add lines
|
||||
foreach ($this->result['lines'] as $line) {
|
||||
$result = $this->addInvoiceLine($invoice, $line, $user);
|
||||
if ($result < 0) {
|
||||
$this->db->rollback();
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
// Validate totals - re-fetch invoice to get calculated totals
|
||||
$invoice->fetch($invoice_id);
|
||||
$validation_result = $this->validateTotals($invoice_data, $invoice);
|
||||
$this->result['validation'] = $validation_result;
|
||||
|
||||
// Attach PDF to supplier invoice
|
||||
if (!empty($pdf_path) && file_exists($pdf_path)) {
|
||||
$this->attachPdfToInvoice($invoice, $pdf_path);
|
||||
}
|
||||
|
||||
return $invoice_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach PDF file to supplier invoice
|
||||
*
|
||||
* @param FactureFournisseur $invoice Invoice object
|
||||
* @param string $pdf_path Source PDF path
|
||||
* @return bool Success
|
||||
*/
|
||||
public function attachPdfToInvoice($invoice, $pdf_path)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
// Get supplier for folder name
|
||||
$supplier = new Societe($this->db);
|
||||
$supplier->fetch($invoice->socid);
|
||||
|
||||
// Build destination directory path for supplier invoice
|
||||
// Format: DOL_DATA_ROOT/fournisseur/facture/[thirdparty_name]/[invoice_ref]/
|
||||
$destdir = $conf->fournisseur->facture->dir_output;
|
||||
$destdir .= '/' . dol_sanitizeFileName($supplier->nom);
|
||||
$destdir .= '/' . dol_sanitizeFileName($invoice->ref);
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!is_dir($destdir)) {
|
||||
dol_mkdir($destdir);
|
||||
}
|
||||
|
||||
// Build descriptive filename
|
||||
// Format: YYYY-MM-DD - Lieferant - Rechnungsnummer - Material - Preis EUR.pdf
|
||||
$newFilename = $this->buildInvoiceFilename($invoice, $supplier);
|
||||
$destfile = $destdir . '/' . $newFilename;
|
||||
|
||||
if (copy($pdf_path, $destfile)) {
|
||||
dol_syslog("Attached PDF as " . $newFilename . " to supplier invoice " . $invoice->ref, LOG_INFO);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build descriptive filename for invoice PDF
|
||||
* Format: YYYY-MM-DD - Lieferant - Rechnungsnummer - Material - Preis EUR.pdf
|
||||
*
|
||||
* @param FactureFournisseur $invoice Invoice object
|
||||
* @param Societe $supplier Supplier object
|
||||
* @return string Filename
|
||||
*/
|
||||
private function buildInvoiceFilename($invoice, $supplier)
|
||||
{
|
||||
// Date: YYYY-MM-DD
|
||||
$date = dol_print_date($invoice->date, '%Y-%m-%d');
|
||||
|
||||
// Supplier name (shortened if too long)
|
||||
$supplierName = dol_sanitizeFileName($supplier->nom);
|
||||
if (strlen($supplierName) > 30) {
|
||||
$supplierName = substr($supplierName, 0, 30);
|
||||
}
|
||||
|
||||
// Invoice number from supplier
|
||||
$invoiceNumber = dol_sanitizeFileName($invoice->ref_supplier);
|
||||
if (empty($invoiceNumber)) {
|
||||
$invoiceNumber = $invoice->ref;
|
||||
}
|
||||
|
||||
// Get material description from first line item or use generic term
|
||||
$material = 'Material';
|
||||
if (!empty($this->result['lines'])) {
|
||||
// Try to get a meaningful description from line items
|
||||
$firstLine = reset($this->result['lines']);
|
||||
if (!empty($firstLine['name'])) {
|
||||
// Use first product name, shortened
|
||||
$material = dol_sanitizeFileName($firstLine['name']);
|
||||
if (strlen($material) > 25) {
|
||||
$material = substr($material, 0, 25);
|
||||
}
|
||||
}
|
||||
// If multiple lines, indicate it
|
||||
if (count($this->result['lines']) > 1) {
|
||||
$material .= ' ua'; // "und andere" / "and others"
|
||||
}
|
||||
}
|
||||
|
||||
// Price rounded
|
||||
$price = round($invoice->total_ttc);
|
||||
|
||||
// Build filename
|
||||
$filename = sprintf(
|
||||
'%s - %s - %s - %s - %d EUR.pdf',
|
||||
$date,
|
||||
$supplierName,
|
||||
$invoiceNumber,
|
||||
$material,
|
||||
$price
|
||||
);
|
||||
|
||||
// Clean up any double spaces or invalid characters
|
||||
$filename = preg_replace('/\s+/', ' ', $filename);
|
||||
$filename = str_replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], '-', $filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that ZUGFeRD totals match Dolibarr calculated totals
|
||||
*
|
||||
* @param array $invoice_data Parsed ZUGFeRD invoice data
|
||||
* @param FactureFournisseur $invoice Created Dolibarr invoice
|
||||
* @return array Validation result with status and message
|
||||
*/
|
||||
public function validateTotals($invoice_data, $invoice)
|
||||
{
|
||||
global $langs;
|
||||
$langs->load('importzugferd@importzugferd');
|
||||
|
||||
$result = array(
|
||||
'valid' => true,
|
||||
'zugferd_ht' => (float) $invoice_data['totals']['net'],
|
||||
'zugferd_ttc' => (float) $invoice_data['totals']['gross'],
|
||||
'dolibarr_ht' => (float) $invoice->total_ht,
|
||||
'dolibarr_ttc' => (float) $invoice->total_ttc,
|
||||
'diff_ht' => 0,
|
||||
'diff_ttc' => 0,
|
||||
'message' => '',
|
||||
);
|
||||
|
||||
$result['diff_ht'] = abs($result['zugferd_ht'] - $result['dolibarr_ht']);
|
||||
$result['diff_ttc'] = abs($result['zugferd_ttc'] - $result['dolibarr_ttc']);
|
||||
|
||||
// Allow small deviations (max 0.05€ per total)
|
||||
$tolerance = 0.05;
|
||||
|
||||
if ($result['diff_ht'] > $tolerance || $result['diff_ttc'] > $tolerance) {
|
||||
$result['valid'] = false;
|
||||
$result['message'] = $langs->trans(
|
||||
'SumValidationError',
|
||||
price($result['zugferd_ttc']),
|
||||
price($result['dolibarr_ttc']),
|
||||
price($result['diff_ttc'])
|
||||
);
|
||||
|
||||
// Update import record with error
|
||||
$this->import->status = ZugferdImport::STATUS_ERROR;
|
||||
$this->import->error_message = $result['message'];
|
||||
} else {
|
||||
$result['message'] = $langs->trans('SumValidationOk');
|
||||
// Keep status as PROCESSED (already set)
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a line to supplier invoice
|
||||
*
|
||||
* @param FactureFournisseur $invoice Invoice object
|
||||
* @param array $line Line data
|
||||
* @param User $user Current user
|
||||
* @return int >0 if OK, <0 if error
|
||||
*/
|
||||
private function addInvoiceLine($invoice, $line, $user)
|
||||
{
|
||||
$desc = $line['name'];
|
||||
if (!empty($line['description']) && $line['description'] != $line['name']) {
|
||||
$desc .= "\n" . $line['description'];
|
||||
}
|
||||
|
||||
// Add supplier reference to description if no product found
|
||||
if ($line['fk_product'] == 0 && !empty($line['supplier_ref'])) {
|
||||
$desc .= "\n[" . $line['supplier_ref'] . "]";
|
||||
}
|
||||
|
||||
// Determine VAT rate
|
||||
$tva_tx = $line['tax_percent'] ?: 19;
|
||||
|
||||
// Add line
|
||||
$result = $invoice->addline(
|
||||
$desc, // description
|
||||
$line['unit_price'], // pu_ht
|
||||
$tva_tx, // tva_tx
|
||||
0, // localtax1_tx
|
||||
0, // localtax2_tx
|
||||
$line['quantity'], // qty
|
||||
$line['fk_product'] ?: 0, // fk_product
|
||||
0, // remise_percent
|
||||
'', // date_start
|
||||
'', // date_end
|
||||
0, // ventil
|
||||
0, // info_bits
|
||||
'HT', // price_base_type
|
||||
0, // type (0=product, 1=service)
|
||||
-1, // rang
|
||||
0, // notrigger
|
||||
array(), // array_options
|
||||
'', // fk_unit
|
||||
0, // origin_id
|
||||
0, // pu_ht_devise
|
||||
$line['supplier_ref'] ?: '' // ref_supplier
|
||||
);
|
||||
|
||||
if ($result < 0) {
|
||||
$this->error = $invoice->error;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Update supplier price with EAN if product was matched and EAN is available
|
||||
if ($line['fk_product'] > 0 && !empty($line['ean'])) {
|
||||
$this->updateSupplierPriceBarcode($invoice->socid, $line['fk_product'], $line['ean'], $line['supplier_ref']);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update barcode in supplier price record
|
||||
*
|
||||
* @param int $supplier_id Supplier ID
|
||||
* @param int $product_id Product ID
|
||||
* @param string $barcode EAN/GTIN barcode
|
||||
* @param string $ref_fourn Supplier reference (optional, to identify correct price record)
|
||||
* @return int >0 if updated, 0 if no update needed, <0 if error
|
||||
*/
|
||||
public function updateSupplierPriceBarcode($supplier_id, $product_id, $barcode, $ref_fourn = '')
|
||||
{
|
||||
global $conf;
|
||||
|
||||
// Check if barcode column exists in product_fournisseur_price table
|
||||
if (!$this->checkSupplierPriceBarcodeColumn()) {
|
||||
return 0; // Column doesn't exist, skip update
|
||||
}
|
||||
|
||||
// Find supplier price record
|
||||
$sql = "SELECT rowid, barcode FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sql .= " WHERE fk_soc = " . (int) $supplier_id;
|
||||
$sql .= " AND fk_product = " . (int) $product_id;
|
||||
$sql .= " AND entity IN (" . getEntity('product') . ")";
|
||||
if (!empty($ref_fourn)) {
|
||||
$sql .= " AND ref_fourn = '" . $this->db->escape($ref_fourn) . "'";
|
||||
}
|
||||
$sql .= " ORDER BY rowid DESC LIMIT 1";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
|
||||
// Only update if barcode is empty or different
|
||||
if (empty($obj->barcode) || $obj->barcode != $barcode) {
|
||||
$sql_update = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sql_update .= " SET barcode = '" . $this->db->escape($barcode) . "'";
|
||||
$sql_update .= " WHERE rowid = " . (int) $obj->rowid;
|
||||
|
||||
$res = $this->db->query($sql_update);
|
||||
if ($res) {
|
||||
dol_syslog("Updated supplier price barcode for product " . $product_id . " supplier " . $supplier_id . " to " . $barcode, LOG_DEBUG);
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0; // No update needed
|
||||
}
|
||||
|
||||
return 0; // No supplier price record found
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if barcode column exists in product_fournisseur_price table
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function checkSupplierPriceBarcodeColumn()
|
||||
{
|
||||
static $has_barcode_column = null;
|
||||
|
||||
if ($has_barcode_column === null) {
|
||||
$sql = "SHOW COLUMNS FROM " . MAIN_DB_PREFIX . "product_fournisseur_price LIKE 'barcode'";
|
||||
$resql = $this->db->query($sql);
|
||||
$has_barcode_column = ($resql && $this->db->num_rows($resql) > 0);
|
||||
}
|
||||
|
||||
return $has_barcode_column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete existing import record by file hash (for reimport)
|
||||
*
|
||||
* @param string $file_hash File hash
|
||||
* @param User $user Current user
|
||||
* @return int >0 if deleted, 0 if not found, <0 if error
|
||||
*/
|
||||
public function deleteExistingImport($file_hash, $user)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
// Find existing import by hash
|
||||
$existingImport = new ZugferdImport($this->db);
|
||||
$result = $existingImport->fetch(0, null, $file_hash);
|
||||
|
||||
if ($result > 0) {
|
||||
// Delete the existing import record
|
||||
$deleteResult = $existingImport->delete($user);
|
||||
if ($deleteResult > 0) {
|
||||
dol_syslog("Deleted existing import record " . $existingImport->ref . " for reimport", LOG_INFO);
|
||||
return 1;
|
||||
} else {
|
||||
$this->error = $existingImport->error;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Not found
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy PDF to documents folder
|
||||
*
|
||||
* @param string $pdf_path Source PDF path
|
||||
* @param int $import_id Import record ID
|
||||
* @return bool
|
||||
*/
|
||||
public function copyToDocuments($pdf_path, $import_id)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$destdir = $conf->importzugferd->dir_output . '/imports';
|
||||
if (!is_dir($destdir)) {
|
||||
dol_mkdir($destdir);
|
||||
}
|
||||
|
||||
$destfile = $destdir . '/' . $this->import->ref . '_' . basename($pdf_path);
|
||||
|
||||
return copy($pdf_path, $destfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get import result
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getResult()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed invoice data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getInvoiceData()
|
||||
{
|
||||
return $this->parser->getInvoiceData();
|
||||
}
|
||||
}
|
||||
299
class/cron_importzugferd.class.php
Normal file
299
class/cron_importzugferd.class.php
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
<?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';
|
||||
|
||||
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
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
}
|
||||
377
class/importline.class.php
Normal file
377
class/importline.class.php
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
<?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 int Assigned Dolibarr product ID
|
||||
*/
|
||||
public $fk_product;
|
||||
|
||||
/**
|
||||
* @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, 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->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)
|
||||
{
|
||||
$sql = "SELECT rowid, 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, fk_product, match_method, date_creation";
|
||||
$sql .= " 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->fk_product = $obj->fk_product;
|
||||
$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 .= " 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);
|
||||
}
|
||||
}
|
||||
478
class/productmapping.class.php
Normal file
478
class/productmapping.class.php
Normal 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;
|
||||
}
|
||||
}
|
||||
553
class/zugferdimport.class.php
Normal file
553
class/zugferdimport.class.php
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
<?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->db->escape($this->invoice_date) . "',";
|
||||
$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 ? $this->fk_soc : "null") . ",";
|
||||
$sql .= " fk_facture_fourn = " . ($this->fk_facture_fourn > 0 ? $this->fk_facture_fourn : "null") . ",";
|
||||
$sql .= " status = " . (int) $this->status . ",";
|
||||
$sql .= " error_message = '" . $this->db->escape($this->error_message) . "',";
|
||||
$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
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
}
|
||||
560
class/zugferdparser.class.php
Normal file
560
class/zugferdparser.class.php
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
$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')),
|
||||
);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
$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')),
|
||||
);
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
576
core/modules/modImportZugferd.class.php
Executable file
576
core/modules/modImportZugferd.class.php
Executable file
|
|
@ -0,0 +1,576 @@
|
|||
<?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 = '1.1';
|
||||
// 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(
|
||||
// 'hookcontext1',
|
||||
// 'hookcontext2',
|
||||
// ),
|
||||
// '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");
|
||||
|
||||
// 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' => 'importzugferdwidget1.php@importzugferd',
|
||||
// 'note' => 'Widget provided by ImportZugferd',
|
||||
// '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' => 'ImportZugferdFromMailbox',
|
||||
'jobtype' => 'method',
|
||||
'class' => '/importzugferd/class/cron_importzugferd.class.php',
|
||||
'objectname' => 'CronImportZugferd',
|
||||
'method' => 'fetchFromMailbox',
|
||||
'parameters' => '',
|
||||
'comment' => 'Fetch ZUGFeRD invoices from configured mailbox',
|
||||
'frequency' => 15,
|
||||
'unitfrequency' => 60,
|
||||
'status' => 0,
|
||||
'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++;
|
||||
|
||||
|
||||
// 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,
|
||||
);
|
||||
|
||||
|
||||
// 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)
|
||||
$extrafields->addExtraField(
|
||||
'supplier_customer_number', // attribute code
|
||||
'SupplierCustomerNumber', // label (translation key)
|
||||
'varchar', // type
|
||||
100, // position
|
||||
64, // size
|
||||
'thirdparty', // element type
|
||||
0, // unique
|
||||
0, // required
|
||||
'', // default value
|
||||
'', // param
|
||||
1, // always editable
|
||||
'', // permission
|
||||
1, // list (show in list)
|
||||
0, // printable
|
||||
'', // totalizable
|
||||
'', // langfile
|
||||
'importzugferd@importzugferd', // module
|
||||
'isModEnabled("importzugferd")' // enabled condition
|
||||
);
|
||||
|
||||
// Permissions
|
||||
$this->remove($options);
|
||||
|
||||
$sql = array();
|
||||
|
||||
// Document templates
|
||||
$moduledir = dol_sanitizeFileName('importzugferd');
|
||||
$myTmpObjects = array();
|
||||
$myTmpObjects['MyObject'] = array('includerefgeneration' => 0, 'includedocgeneration' => 0);
|
||||
|
||||
foreach ($myTmpObjects as $myTmpObjectKey => $myTmpObjectArray) {
|
||||
if ($myTmpObjectArray['includerefgeneration']) {
|
||||
$src = DOL_DOCUMENT_ROOT.'/install/doctemplates/'.$moduledir.'/template_myobjects.odt';
|
||||
$dirodt = DOL_DATA_ROOT.($conf->entity > 1 ? '/'.$conf->entity : '').'/doctemplates/'.$moduledir;
|
||||
$dest = $dirodt.'/template_myobjects.odt';
|
||||
|
||||
if (file_exists($src) && !file_exists($dest)) {
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
||||
dol_mkdir($dirodt);
|
||||
$result = dol_copy($src, $dest, '0', 0);
|
||||
if ($result < 0) {
|
||||
$langs->load("errors");
|
||||
$this->error = $langs->trans('ErrorFailToCopyFile', $src, $dest);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
$sql = array_merge($sql, array(
|
||||
"DELETE FROM ".$this->db->prefix()."document_model WHERE nom = 'standard_".strtolower($myTmpObjectKey)."' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity),
|
||||
"INSERT INTO ".$this->db->prefix()."document_model (nom, type, entity) VALUES('standard_".strtolower($myTmpObjectKey)."', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")",
|
||||
"DELETE FROM ".$this->db->prefix()."document_model WHERE nom = 'generic_".strtolower($myTmpObjectKey)."_odt' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity),
|
||||
"INSERT INTO ".$this->db->prefix()."document_model (nom, type, entity) VALUES('generic_".strtolower($myTmpObjectKey)."_odt', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_init($sql, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
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
Normal file
14
img/object_importzugferd.svg
Normal 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 |
841
import.php
Normal file
841
import.php
Normal file
|
|
@ -0,0 +1,841 @@
|
|||
<?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 import.php
|
||||
* \ingroup importzugferd
|
||||
* \brief Manual ZUGFeRD import page with persistent workflow
|
||||
*/
|
||||
|
||||
// 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';
|
||||
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.'/product/class/product.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
||||
|
||||
dol_include_once('/importzugferd/class/zugferdparser.class.php');
|
||||
dol_include_once('/importzugferd/class/zugferdimport.class.php');
|
||||
dol_include_once('/importzugferd/class/importline.class.php');
|
||||
dol_include_once('/importzugferd/class/productmapping.class.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", "companies"));
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('importzugferd', 'import', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
$id = GETPOST('id', 'int'); // Import ID for editing existing imports
|
||||
$supplier_id = GETPOST('supplier_id', 'int');
|
||||
$line_id = GETPOST('line_id', 'int');
|
||||
$product_id = GETPOST('product_id', 'int');
|
||||
$template_product_id = GETPOST('template_product_id', 'int');
|
||||
|
||||
// Initialize objects
|
||||
$form = new Form($db);
|
||||
$formfile = new FormFile($db);
|
||||
$actions = new ActionsImportZugferd($db);
|
||||
$import = new ZugferdImport($db);
|
||||
$importLine = new ImportLine($db);
|
||||
|
||||
$error = 0;
|
||||
$message = '';
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// Upload and parse PDF - creates import record immediately
|
||||
if ($action == 'upload') {
|
||||
if (!empty($_FILES['zugferd_file']['tmp_name'])) {
|
||||
$upload_dir = $conf->importzugferd->dir_output.'/temp';
|
||||
if (!is_dir($upload_dir)) {
|
||||
dol_mkdir($upload_dir);
|
||||
}
|
||||
|
||||
$filename = dol_sanitizeFileName($_FILES['zugferd_file']['name']);
|
||||
$destfile = $upload_dir.'/'.$filename;
|
||||
|
||||
if (move_uploaded_file($_FILES['zugferd_file']['tmp_name'], $destfile)) {
|
||||
$force_reimport = GETPOST('force_reimport', 'int');
|
||||
|
||||
// Check for duplicate
|
||||
$file_hash = hash_file('sha256', $destfile);
|
||||
$isDuplicate = $import->isDuplicate($file_hash);
|
||||
|
||||
if ($isDuplicate && !$force_reimport) {
|
||||
$error++;
|
||||
$message = $langs->trans('ErrorDuplicateInvoice');
|
||||
@unlink($destfile);
|
||||
} else {
|
||||
// If force reimport, delete the old record first
|
||||
if ($isDuplicate && $force_reimport) {
|
||||
$oldImport = new ZugferdImport($db);
|
||||
$oldImport->fetch(0, null, $file_hash);
|
||||
if ($oldImport->id > 0) {
|
||||
// Delete old lines
|
||||
$oldLines = new ImportLine($db);
|
||||
$oldLines->deleteAllByImport($oldImport->id);
|
||||
// Delete old files
|
||||
$old_dir = $conf->importzugferd->dir_output.'/imports/'.$oldImport->id;
|
||||
if (is_dir($old_dir)) {
|
||||
dol_delete_dir_recursive($old_dir);
|
||||
}
|
||||
// Delete old import record
|
||||
$oldImport->delete($user);
|
||||
}
|
||||
}
|
||||
// Parse the file
|
||||
$parser = new ZugferdParser($db);
|
||||
$res = $parser->extractFromPdf($destfile);
|
||||
|
||||
if ($res > 0) {
|
||||
$res = $parser->parse();
|
||||
if ($res > 0) {
|
||||
$parsed_data = $parser->getInvoiceData();
|
||||
|
||||
// Create import record immediately
|
||||
$import->invoice_number = $parsed_data['invoice_number'];
|
||||
$import->invoice_date = $parsed_data['invoice_date'];
|
||||
$import->seller_name = $parsed_data['seller']['name'];
|
||||
$import->seller_vat = $parsed_data['seller']['vat_id'];
|
||||
$import->buyer_reference = $parsed_data['buyer']['reference'] ?: $parsed_data['buyer']['id'];
|
||||
$import->total_ht = $parsed_data['totals']['net'];
|
||||
$import->total_ttc = $parsed_data['totals']['gross'];
|
||||
$import->currency = $parsed_data['totals']['currency'];
|
||||
$import->xml_content = $parser->getXmlContent();
|
||||
$import->pdf_filename = $filename;
|
||||
$import->file_hash = $file_hash;
|
||||
|
||||
// Find supplier
|
||||
$supplier_id = $actions->findSupplier($parsed_data);
|
||||
$import->fk_soc = $supplier_id;
|
||||
|
||||
// Process line items to find products
|
||||
$processed_lines = $actions->processLineItems($parsed_data['lines'], $supplier_id);
|
||||
|
||||
// Check if all lines have products
|
||||
$all_have_products = true;
|
||||
foreach ($processed_lines as $line) {
|
||||
if ($line['fk_product'] <= 0) {
|
||||
$all_have_products = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Set status based on product matching
|
||||
if ($all_have_products && $supplier_id > 0) {
|
||||
$import->status = ZugferdImport::STATUS_IMPORTED;
|
||||
} else {
|
||||
$import->status = ZugferdImport::STATUS_PENDING;
|
||||
}
|
||||
|
||||
$import->date_creation = dol_now();
|
||||
$result = $import->create($user);
|
||||
|
||||
if ($result > 0) {
|
||||
// Store line items in database
|
||||
foreach ($processed_lines as $line) {
|
||||
$importLineObj = new ImportLine($db);
|
||||
$importLineObj->fk_import = $import->id;
|
||||
$importLineObj->line_id = $line['line_id'];
|
||||
$importLineObj->supplier_ref = $line['supplier_ref'];
|
||||
$importLineObj->product_name = $line['name'];
|
||||
$importLineObj->description = $line['description'];
|
||||
$importLineObj->quantity = $line['quantity'];
|
||||
$importLineObj->unit_code = $line['unit_code'];
|
||||
$importLineObj->unit_price = $line['unit_price'];
|
||||
$importLineObj->unit_price_raw = $line['unit_price_raw'];
|
||||
$importLineObj->basis_quantity = $line['basis_quantity'];
|
||||
$importLineObj->basis_quantity_unit = $line['basis_quantity_unit'];
|
||||
$importLineObj->line_total = $line['line_total'];
|
||||
$importLineObj->tax_percent = $line['tax_percent'];
|
||||
$importLineObj->ean = $line['ean'];
|
||||
$importLineObj->fk_product = $line['fk_product'];
|
||||
$importLineObj->match_method = $line['match_method'];
|
||||
$importLineObj->create($user);
|
||||
}
|
||||
|
||||
// Move PDF to permanent storage
|
||||
$final_dir = $conf->importzugferd->dir_output.'/imports/'.$import->id;
|
||||
if (!is_dir($final_dir)) {
|
||||
dol_mkdir($final_dir);
|
||||
}
|
||||
rename($destfile, $final_dir.'/'.$filename);
|
||||
|
||||
// Redirect to edit page
|
||||
$id = $import->id;
|
||||
$action = 'edit';
|
||||
setEventMessages($langs->trans('ImportRecordCreated'), null, 'mesgs');
|
||||
} else {
|
||||
$error++;
|
||||
$message = $import->error;
|
||||
@unlink($destfile);
|
||||
}
|
||||
} else {
|
||||
$error++;
|
||||
$message = $parser->error;
|
||||
@unlink($destfile);
|
||||
}
|
||||
} else {
|
||||
$error++;
|
||||
$message = $parser->error;
|
||||
@unlink($destfile);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$error++;
|
||||
$message = $langs->trans('ErrorFileUploadFailed');
|
||||
}
|
||||
} else {
|
||||
$error++;
|
||||
$message = $langs->trans('ErrorNoFileUploaded');
|
||||
}
|
||||
}
|
||||
|
||||
// Load existing import for editing
|
||||
if ($id > 0 && empty($action)) {
|
||||
$action = 'edit';
|
||||
}
|
||||
|
||||
if ($action == 'edit' && $id > 0) {
|
||||
$result = $import->fetch($id);
|
||||
if ($result <= 0) {
|
||||
$error++;
|
||||
$message = $langs->trans('ErrorRecordNotFound');
|
||||
$action = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Assign product to line
|
||||
if ($action == 'assignproduct' && $line_id > 0 && $product_id > 0) {
|
||||
$lineObj = new ImportLine($db);
|
||||
$result = $lineObj->fetch($line_id);
|
||||
if ($result > 0) {
|
||||
$lineObj->setProduct($product_id, $langs->trans('ManualAssignment'), $user);
|
||||
setEventMessages($langs->trans('ProductAssigned'), null, 'mesgs');
|
||||
|
||||
// Get import ID to reload
|
||||
$id = $lineObj->fk_import;
|
||||
|
||||
// Check if all lines now have products
|
||||
$allHaveProducts = $importLine->allLinesHaveProducts($id);
|
||||
if ($allHaveProducts) {
|
||||
// Update import status
|
||||
$import->fetch($id);
|
||||
if ($import->status == ZugferdImport::STATUS_PENDING) {
|
||||
$import->status = ZugferdImport::STATUS_IMPORTED;
|
||||
$import->update($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
$action = 'edit';
|
||||
$import->fetch($id);
|
||||
}
|
||||
|
||||
// Remove product assignment from line
|
||||
if ($action == 'removeproduct' && $line_id > 0) {
|
||||
$lineObj = new ImportLine($db);
|
||||
$result = $lineObj->fetch($line_id);
|
||||
if ($result > 0) {
|
||||
$id = $lineObj->fk_import;
|
||||
$lineObj->setProduct(0, '', $user);
|
||||
setEventMessages($langs->trans('ProductRemoved'), null, 'mesgs');
|
||||
|
||||
// Update import status to pending
|
||||
$import->fetch($id);
|
||||
if ($import->status == ZugferdImport::STATUS_IMPORTED) {
|
||||
$import->status = ZugferdImport::STATUS_PENDING;
|
||||
$import->update($user);
|
||||
}
|
||||
}
|
||||
$action = 'edit';
|
||||
$import->fetch($id);
|
||||
}
|
||||
|
||||
// Update supplier
|
||||
if ($action == 'setsupplier' && $id > 0) {
|
||||
$import->fetch($id);
|
||||
$import->fk_soc = $supplier_id;
|
||||
$import->update($user);
|
||||
setEventMessages($langs->trans('SupplierUpdated'), null, 'mesgs');
|
||||
$action = 'edit';
|
||||
}
|
||||
|
||||
// Duplicate product from template
|
||||
if ($action == 'duplicateproduct' && $template_product_id > 0 && $line_id > 0) {
|
||||
$lineObj = new ImportLine($db);
|
||||
$result = $lineObj->fetch($line_id);
|
||||
|
||||
if ($result > 0) {
|
||||
// Load template product
|
||||
$template = new Product($db);
|
||||
if ($template->fetch($template_product_id) > 0) {
|
||||
// Create new product as copy
|
||||
$newproduct = new Product($db);
|
||||
|
||||
// Copy basic properties from template
|
||||
$newproduct->type = $template->type;
|
||||
$newproduct->status = $template->status;
|
||||
$newproduct->status_buy = $template->status_buy;
|
||||
$newproduct->status_batch = $template->status_batch;
|
||||
$newproduct->fk_product_type = $template->fk_product_type;
|
||||
$newproduct->price = $lineObj->unit_price;
|
||||
$newproduct->price_base_type = 'HT';
|
||||
$newproduct->tva_tx = $lineObj->tax_percent ?: $template->tva_tx;
|
||||
$newproduct->weight = $template->weight;
|
||||
$newproduct->weight_units = $template->weight_units;
|
||||
$newproduct->fk_unit = $template->fk_unit;
|
||||
|
||||
// Set label from ZUGFeRD
|
||||
$newproduct->label = $lineObj->product_name;
|
||||
|
||||
// Generate unique ref
|
||||
$newproduct->ref = 'NEW-'.dol_print_date(dol_now(), '%Y%m%d%H%M%S');
|
||||
|
||||
// Build description with ZUGFeRD data
|
||||
$zugferd_info = '';
|
||||
if (!empty($lineObj->supplier_ref)) {
|
||||
$zugferd_info .= $langs->trans('SupplierRef').': '.$lineObj->supplier_ref."\n";
|
||||
}
|
||||
if (!empty($lineObj->unit_code)) {
|
||||
$zugferd_info .= $langs->trans('Unit').': '.zugferdGetUnitLabel($lineObj->unit_code)."\n";
|
||||
}
|
||||
if (!empty($lineObj->ean)) {
|
||||
$zugferd_info .= 'EAN: '.$lineObj->ean."\n";
|
||||
}
|
||||
$zugferd_info .= "---\n";
|
||||
$newproduct->description = $zugferd_info . ($template->description ?: '');
|
||||
|
||||
// Create the product
|
||||
$result = $newproduct->create($user);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('ProductCreated'), null, 'mesgs');
|
||||
// Redirect to product card for editing
|
||||
header('Location: '.DOL_URL_ROOT.'/product/card.php?id='.$result);
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($newproduct->error, $newproduct->errors, 'errors');
|
||||
}
|
||||
}
|
||||
$id = $lineObj->fk_import;
|
||||
}
|
||||
$action = 'edit';
|
||||
$import->fetch($id);
|
||||
}
|
||||
|
||||
// Create supplier invoice
|
||||
if ($action == 'createinvoice' && $id > 0) {
|
||||
$import->fetch($id);
|
||||
|
||||
// Check prerequisites
|
||||
if ($import->fk_soc <= 0) {
|
||||
$error++;
|
||||
setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors');
|
||||
} else {
|
||||
// Check all lines have products
|
||||
$lines = $importLine->fetchAllByImport($id);
|
||||
$allHaveProducts = true;
|
||||
foreach ($lines as $line) {
|
||||
if ($line->fk_product <= 0) {
|
||||
$allHaveProducts = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$allHaveProducts) {
|
||||
$error++;
|
||||
setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors');
|
||||
} else {
|
||||
// Create invoice
|
||||
$invoice = new FactureFournisseur($db);
|
||||
$invoice->socid = $import->fk_soc;
|
||||
$invoice->ref_supplier = $import->invoice_number;
|
||||
$invoice->date = $import->invoice_date;
|
||||
$invoice->note_private = $langs->trans('ImportedFromZugferd').' ('.$import->ref.')';
|
||||
$invoice->cond_reglement_id = 1;
|
||||
|
||||
$db->begin();
|
||||
$result = $invoice->create($user);
|
||||
|
||||
if ($result > 0) {
|
||||
// Add lines
|
||||
foreach ($lines as $line) {
|
||||
$res = $invoice->addline(
|
||||
$line->product_name,
|
||||
$line->unit_price,
|
||||
$line->tax_percent,
|
||||
0, 0,
|
||||
$line->quantity,
|
||||
$line->fk_product,
|
||||
0, '', '',
|
||||
0, 0, '',
|
||||
'HT'
|
||||
);
|
||||
if ($res < 0) {
|
||||
$error++;
|
||||
setEventMessages($invoice->error, $invoice->errors, 'errors');
|
||||
break;
|
||||
}
|
||||
|
||||
// Update EAN on product if not set
|
||||
if (!empty($line->ean) && $line->fk_product > 0) {
|
||||
$product = new Product($db);
|
||||
$product->fetch($line->fk_product);
|
||||
if (empty($product->barcode)) {
|
||||
$product->barcode = $line->ean;
|
||||
$product->barcode_type = 2; // EAN13
|
||||
$product->update($product->id, $user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
// Validate invoice
|
||||
$invoice->validate($user);
|
||||
|
||||
// Copy PDF to invoice
|
||||
$source_pdf = $conf->importzugferd->dir_output.'/imports/'.$import->id.'/'.$import->pdf_filename;
|
||||
if (file_exists($source_pdf)) {
|
||||
$dest_dir = $conf->fournisseur->facture->dir_output.'/'.get_exdir($invoice->id, 2, 0, 0, $invoice, 'invoice_supplier').$invoice->ref;
|
||||
if (!is_dir($dest_dir)) {
|
||||
dol_mkdir($dest_dir);
|
||||
}
|
||||
copy($source_pdf, $dest_dir.'/'.$import->pdf_filename);
|
||||
}
|
||||
|
||||
// Update import record
|
||||
$import->fk_facture_fourn = $invoice->id;
|
||||
$import->status = ZugferdImport::STATUS_PROCESSED;
|
||||
$import->date_import = dol_now();
|
||||
$import->update($user);
|
||||
|
||||
$db->commit();
|
||||
setEventMessages($langs->trans('InvoiceCreatedSuccessfully'), null, 'mesgs');
|
||||
|
||||
// Redirect to invoice
|
||||
header('Location: '.DOL_URL_ROOT.'/fourn/facture/card.php?facid='.$invoice->id);
|
||||
exit;
|
||||
} else {
|
||||
$db->rollback();
|
||||
}
|
||||
} else {
|
||||
$error++;
|
||||
setEventMessages($invoice->error, $invoice->errors, 'errors');
|
||||
$db->rollback();
|
||||
}
|
||||
}
|
||||
}
|
||||
$action = 'edit';
|
||||
}
|
||||
|
||||
// Delete import record
|
||||
if ($action == 'confirm_delete' && $confirm == 'yes' && $id > 0) {
|
||||
$import->fetch($id);
|
||||
|
||||
// Delete lines first
|
||||
$importLine->deleteAllByImport($id);
|
||||
|
||||
// Delete files
|
||||
$import_dir = $conf->importzugferd->dir_output.'/imports/'.$import->id;
|
||||
if (is_dir($import_dir)) {
|
||||
dol_delete_dir_recursive($import_dir);
|
||||
}
|
||||
|
||||
// Delete import record
|
||||
$import->delete($user);
|
||||
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$title = $langs->trans('ZugferdImport');
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-import');
|
||||
|
||||
print load_fiche_titre($title, '', 'fa-file-import');
|
||||
|
||||
// Error message
|
||||
if ($error && !empty($message)) {
|
||||
setEventMessages($message, null, 'errors');
|
||||
}
|
||||
|
||||
/*
|
||||
* Upload form (shown when no import is being edited)
|
||||
*/
|
||||
if (empty($action) || ($action == 'upload' && $error)) {
|
||||
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 '<div class="fichecenter">';
|
||||
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('UploadZugferdInvoice').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans('File').' (PDF)</td>';
|
||||
print '<td>';
|
||||
print '<input type="file" name="zugferd_file" accept=".pdf" class="flat minwidth300" required>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans('ForceReimport').'</td>';
|
||||
print '<td>';
|
||||
print '<input type="checkbox" name="force_reimport" value="1"> ';
|
||||
print '<span class="opacitymedium">'.$langs->trans('ForceReimportHelp').'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '<div class="center" style="margin-top: 20px;">';
|
||||
print '<input type="submit" class="button button-primary" value="'.$langs->trans('Upload').'">';
|
||||
print '</div>';
|
||||
|
||||
print '</div>';
|
||||
|
||||
// Show pending 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 colspan="5">'.$langs->trans('PendingImports').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
$sql = "SELECT i.rowid, i.ref, i.invoice_number, i.seller_name, i.total_ttc, i.status, i.date_creation";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."importzugferd_import as i";
|
||||
$sql .= " WHERE i.entity = ".$conf->entity;
|
||||
$sql .= " AND i.status IN (".ZugferdImport::STATUS_IMPORTED.", ".ZugferdImport::STATUS_PENDING.")";
|
||||
$sql .= " ORDER BY i.date_creation DESC LIMIT 10";
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
$num = $db->num_rows($resql);
|
||||
if ($num > 0) {
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>'.$langs->trans('Ref').'</td>';
|
||||
print '<td>'.$langs->trans('InvoiceNumber').'</td>';
|
||||
print '<td>'.$langs->trans('Supplier').'</td>';
|
||||
print '<td class="right">'.$langs->trans('TotalTTC').'</td>';
|
||||
print '<td>'.$langs->trans('Status').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td><a href="'.$_SERVER['PHP_SELF'].'?id='.$obj->rowid.'">'.$obj->ref.'</a></td>';
|
||||
print '<td>'.$obj->invoice_number.'</td>';
|
||||
print '<td>'.$obj->seller_name.'</td>';
|
||||
print '<td class="right">'.price($obj->total_ttc).'</td>';
|
||||
print '<td>';
|
||||
$tmpimport = new ZugferdImport($db);
|
||||
print $tmpimport->LibStatut($obj->status, 1);
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="5" class="opacitymedium">'.$langs->trans('NoPendingImports').'</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
}
|
||||
|
||||
/*
|
||||
* Edit/Review import
|
||||
*/
|
||||
if ($action == 'edit' && $import->id > 0) {
|
||||
// Delete confirmation
|
||||
if ($action == 'delete') {
|
||||
$formconfirm = $form->formconfirm(
|
||||
$_SERVER['PHP_SELF'].'?id='.$import->id,
|
||||
$langs->trans('DeleteImportRecord'),
|
||||
$langs->trans('ConfirmDeleteImportRecord', $import->ref),
|
||||
'confirm_delete'
|
||||
);
|
||||
print $formconfirm;
|
||||
}
|
||||
|
||||
// Fetch lines
|
||||
$lines = $importLine->fetchAllByImport($import->id);
|
||||
$missingProducts = $importLine->countLinesWithoutProduct($import->id);
|
||||
$allComplete = ($missingProducts == 0 && $import->fk_soc > 0);
|
||||
|
||||
// Header info
|
||||
print '<div class="fichecenter">';
|
||||
|
||||
// Status banner
|
||||
if ($import->status == ZugferdImport::STATUS_PENDING) {
|
||||
print '<div class="warning">';
|
||||
print '<i class="fas fa-exclamation-triangle paddingright"></i>';
|
||||
print $langs->trans('ManualInterventionRequired');
|
||||
if ($missingProducts > 0) {
|
||||
print ' - '.$missingProducts.' '.$langs->trans('ProductsNotAssigned');
|
||||
}
|
||||
if ($import->fk_soc <= 0) {
|
||||
print ' - '.$langs->trans('SupplierNotAssigned');
|
||||
}
|
||||
print '</div><br>';
|
||||
} elseif ($allComplete) {
|
||||
print '<div class="ok" style="padding: 10px; margin-bottom: 10px;">';
|
||||
print '<i class="fas fa-check-circle paddingright"></i>';
|
||||
print $langs->trans('ReadyToCreateInvoice');
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Invoice data
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="4">'.$langs->trans('InvoiceData').' - '.$import->ref.'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans('InvoiceNumber').'</td>';
|
||||
print '<td><strong>'.dol_escape_htmltag($import->invoice_number).'</strong></td>';
|
||||
print '<td>'.$langs->trans('InvoiceDate').'</td>';
|
||||
print '<td>'.dol_print_date($import->invoice_date, 'day').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans('Supplier').'</td>';
|
||||
print '<td>'.dol_escape_htmltag($import->seller_name).'</td>';
|
||||
print '<td>'.$langs->trans('VATIntra').'</td>';
|
||||
print '<td>'.dol_escape_htmltag($import->seller_vat).'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans('BuyerReference').'</td>';
|
||||
print '<td>'.dol_escape_htmltag($import->buyer_reference).'</td>';
|
||||
print '<td>'.$langs->trans('TotalHT').'</td>';
|
||||
print '<td>'.price($import->total_ht).' '.$import->currency.'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans('Status').'</td>';
|
||||
print '<td>'.$import->getLibStatut(1).'</td>';
|
||||
print '<td>'.$langs->trans('TotalTTC').'</td>';
|
||||
print '<td><strong>'.price($import->total_ttc).' '.$import->currency.'</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
// Supplier selection
|
||||
print '<br>';
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="setsupplier">';
|
||||
print '<input type="hidden" name="id" value="'.$import->id.'">';
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans('SupplierAssignment').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans('SelectSupplier').' <span class="fieldrequired">*</span></td>';
|
||||
print '<td>';
|
||||
print $form->select_company($import->fk_soc, 'supplier_id', 's.fournisseur = 1', 'SelectThirdParty', 0, 0, null, 0, 'minwidth300');
|
||||
print ' <input type="submit" class="button smallpaddingimp" value="'.$langs->trans('Save').'">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
// Line items
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>'.$langs->trans('Position').'</td>';
|
||||
print '<td>'.$langs->trans('SupplierRef').'</td>';
|
||||
print '<td>'.$langs->trans('ProductDescription').'</td>';
|
||||
print '<td class="right">'.$langs->trans('Qty').'</td>';
|
||||
print '<td class="right">'.$langs->trans('UnitPrice').'</td>';
|
||||
print '<td class="right">'.$langs->trans('TotalHT').'</td>';
|
||||
print '<td>'.$langs->trans('MatchedProduct').'</td>';
|
||||
print '<td>'.$langs->trans('Action').'</td>';
|
||||
print '</tr>';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$hasProduct = ($line->fk_product > 0);
|
||||
$rowClass = $hasProduct ? 'oddeven opacitymedium' : 'oddeven';
|
||||
|
||||
print '<tr class="'.$rowClass.'">';
|
||||
print '<td>'.$line->line_id.'</td>';
|
||||
print '<td>'.dol_escape_htmltag($line->supplier_ref).'</td>';
|
||||
print '<td>';
|
||||
print dol_escape_htmltag($line->product_name);
|
||||
if (!empty($line->ean) && !$hasProduct) {
|
||||
print '<br><span class="opacitymedium">EAN: '.$line->ean.'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '<td class="right">'.price2num($line->quantity, 'MS').' '.zugferdGetUnitLabel($line->unit_code).'</td>';
|
||||
print '<td class="right">';
|
||||
print price($line->unit_price);
|
||||
if (!empty($line->basis_quantity) && $line->basis_quantity != 1) {
|
||||
print '<br><span class="opacitymedium">('.price($line->unit_price_raw).'/'.price2num($line->basis_quantity, 'MS').zugferdGetUnitLabel($line->basis_quantity_unit).')</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '<td class="right">'.price($line->line_total).'</td>';
|
||||
print '<td>';
|
||||
|
||||
if ($hasProduct) {
|
||||
$product = new Product($db);
|
||||
$product->fetch($line->fk_product);
|
||||
print $product->getNomUrl(1);
|
||||
if (!empty($line->match_method)) {
|
||||
print '<br><span class="opacitymedium">'.$langs->trans('MatchMethod').': '.$line->match_method.'</span>';
|
||||
}
|
||||
if (!empty($line->ean)) {
|
||||
print '<br><span class="opacitymedium"><i class="fas fa-barcode"></i> '.$line->ean.'</span>';
|
||||
}
|
||||
print ' <i class="fas fa-check-circle" style="color: green;"></i>';
|
||||
} else {
|
||||
print '<span class="warning">'.$langs->trans('NoProductMatch').'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
print '<td class="nowraponall">';
|
||||
if ($hasProduct) {
|
||||
// Remove assignment button
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=removeproduct&line_id='.$line->id.'&id='.$import->id.'&token='.newToken().'" class="button buttongen">';
|
||||
print '<i class="fas fa-times"></i>';
|
||||
print '</a>';
|
||||
} else {
|
||||
// Product selection form
|
||||
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="assignproduct">';
|
||||
print '<input type="hidden" name="line_id" value="'.$line->id.'">';
|
||||
print '<input type="hidden" name="id" value="'.$import->id.'">';
|
||||
print $form->select_produits('', 'product_id', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth150 maxwidth200', 1, '', 0);
|
||||
print ' <button type="submit" class="button buttongen" title="'.$langs->trans('AssignProduct').'">';
|
||||
print '<i class="fas fa-link"></i>';
|
||||
print '</button>';
|
||||
print '</form>';
|
||||
|
||||
// Create new product link
|
||||
$create_url = DOL_URL_ROOT.'/product/card.php?action=create';
|
||||
$create_url .= '&label='.urlencode($line->product_name);
|
||||
$create_url .= '&price='.urlencode($line->unit_price);
|
||||
$create_desc = '';
|
||||
if (!empty($line->supplier_ref)) {
|
||||
$create_desc .= $langs->trans('SupplierRef').': '.$line->supplier_ref."\n";
|
||||
}
|
||||
if (!empty($line->unit_code)) {
|
||||
$create_desc .= $langs->trans('Unit').': '.zugferdGetUnitLabel($line->unit_code)."\n";
|
||||
}
|
||||
if (!empty($line->ean)) {
|
||||
$create_desc .= 'EAN: '.$line->ean."\n";
|
||||
}
|
||||
$create_url .= '&description='.urlencode(trim($create_desc));
|
||||
|
||||
print '<br><a href="'.$create_url.'" target="_blank" class="button buttongen margintoponlyshort">';
|
||||
print '<i class="fas fa-plus-circle"></i> '.$langs->trans('CreateProduct');
|
||||
print '</a>';
|
||||
|
||||
// Product template
|
||||
print '<br>';
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" class="inline-block margintoponlyshort">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="duplicateproduct">';
|
||||
print '<input type="hidden" name="line_id" value="'.$line->id.'">';
|
||||
print '<input type="hidden" name="id" value="'.$import->id.'">';
|
||||
print $form->select_produits('', 'template_product_id', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth100 maxwidth150', 1, '', 0);
|
||||
print ' <button type="submit" class="button buttongen" title="'.$langs->trans('ProductTemplateHelp').'">';
|
||||
print '<i class="fas fa-copy"></i>';
|
||||
print '</button>';
|
||||
print '</form>';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
// Action buttons
|
||||
print '<div class="center" style="margin-top: 20px;">';
|
||||
|
||||
if ($allComplete) {
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=createinvoice&id='.$import->id.'&token='.newToken().'" class="button button-primary">';
|
||||
print '<i class="fas fa-file-invoice paddingright"></i>'.$langs->trans('CreateSupplierInvoice');
|
||||
print '</a>';
|
||||
print ' ';
|
||||
}
|
||||
|
||||
print '<a href="'.dol_buildpath('/importzugferd/list.php', 1).'" class="button">'.$langs->trans('BackToList').'</a>';
|
||||
print ' ';
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&id='.$import->id.'&token='.newToken().'" class="button button-cancel">'.$langs->trans('Delete').'</a>';
|
||||
|
||||
print '</div>';
|
||||
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
191
importzugferdindex.php
Executable file
191
importzugferdindex.php
Executable file
|
|
@ -0,0 +1,191 @@
|
|||
<?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');
|
||||
}
|
||||
|
||||
/*
|
||||
* 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();
|
||||
215
langs/de_DE/importzugferd.lang
Normal file
215
langs/de_DE/importzugferd.lang
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Ü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_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
|
||||
|
||||
#
|
||||
# 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
|
||||
215
langs/en_US/importzugferd.lang
Executable file
215
langs/en_US/importzugferd.lang
Executable file
|
|
@ -0,0 +1,215 @@
|
|||
# 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_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
|
||||
|
||||
#
|
||||
# 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
|
||||
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
Normal file
311
list.php
Normal 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
Normal file
274
mapping.php
Normal 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.
|
||||
3
sql/dolibarr_allversions.sql
Executable file
3
sql/dolibarr_allversions.sql
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
--
|
||||
-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version.
|
||||
--
|
||||
14
sql/llx_importzugferd_import.key.sql
Normal file
14
sql/llx_importzugferd_import.key.sql
Normal 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
Normal file
40
sql/llx_importzugferd_import.sql
Normal 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
Normal file
7
sql/llx_importzugferd_import_line.key.sql
Normal 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);
|
||||
35
sql/llx_importzugferd_import_line.sql
Normal file
35
sql/llx_importzugferd_import_line.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- ============================================================================
|
||||
-- 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
|
||||
fk_product integer, -- Zugeordnetes Dolibarr-Produkt
|
||||
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
Normal file
13
sql/llx_importzugferd_productmapping.key.sql
Normal 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
Normal file
22
sql/llx_importzugferd_productmapping.sql
Normal 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