v1.7: Multi-invoice payments and payment unlinking
- Add multi-invoice payment support (link one bank transaction to multiple invoices) - Add payment unlinking feature to correct wrong matches - Show linked payments, invoices and bank entries in transaction detail view - Allow linking already paid invoices to bank transactions - Update README with new features - Add CHANGELOG.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
94efa59df3
387 changed files with 34718 additions and 0 deletions
46
CHANGELOG.md
Normal file
46
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Changelog
|
||||
|
||||
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
|
||||
|
||||
## [1.7] - 2026-02-20
|
||||
|
||||
### Hinzugefügt
|
||||
- **Multi-Rechnungszahlungen**: Eine Bankbuchung kann jetzt mit mehreren Rechnungen verknüpft werden (Sammelzahlungen)
|
||||
- **Zahlungsverknüpfung aufheben**: Falsche Zuordnungen können über "Verknüpfung aufheben" korrigiert werden
|
||||
- **Detailansicht Verknüpfungen**: In der Buchungsdetailansicht werden verknüpfte Zahlungen, Rechnungen und Bank-Einträge angezeigt
|
||||
- **Bezahlte Rechnungen verknüpfen**: Bereits bezahlte Rechnungen können mit Bankbuchungen verknüpft werden (für nachträgliche Bank-Zuordnung)
|
||||
|
||||
### Verbessert
|
||||
- Bessere Anzeige von Multi-Invoice-Matches im Zahlungsabgleich
|
||||
- Flexible Rechnungsauswahl per Checkbox bei Sammelzahlungen
|
||||
|
||||
## [1.6] - 2026-02-15
|
||||
|
||||
### Hinzugefügt
|
||||
- PDF-Kontoauszüge: Upload und Verwaltung mit automatischer Metadaten-Erkennung
|
||||
- Mehrfach-Upload für PDF-Kontoauszüge
|
||||
- Erinnerungsfunktion für veraltete Kontoauszüge
|
||||
- Dashboard-Widget für offene Zuordnungen
|
||||
|
||||
### Verbessert
|
||||
- Optimierte Buchungszuordnung mit Scoring-System
|
||||
- Verbesserte Benutzeroberfläche
|
||||
|
||||
## [1.5] - 2026-02-01
|
||||
|
||||
### Hinzugefügt
|
||||
- Automatischer Import via Cronjob
|
||||
- Unterstützung für SecureGo Plus (Decoupled TAN)
|
||||
- Automatische Kontoerkennung
|
||||
|
||||
### Verbessert
|
||||
- Stabilere FinTS-Verbindung
|
||||
- Bessere Fehlerbehandlung
|
||||
|
||||
## [1.0] - 2026-01-15
|
||||
|
||||
### Erste Version
|
||||
- FinTS/HBCI-Anbindung für deutsche Banken
|
||||
- Import von Kontobuchungen
|
||||
- Grundlegende Buchungszuordnung zu Rechnungen
|
||||
- Integration in Dolibarr-Menüstruktur
|
||||
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
|
||||
85
README.md
Executable file
85
README.md
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
# BANKIMPORT FOR [DOLIBARR ERP & CRM](https://www.dolibarr.org)
|
||||
|
||||
Dolibarr-Modul zum Import von Kontoauszügen und Buchungen über die FinTS/HBCI-Schnittstelle deutscher Banken.
|
||||
|
||||
## Features
|
||||
|
||||
- **FinTS/HBCI-Anbindung**: Automatischer Abruf von Kontobuchungen über die FinTS-Schnittstelle (getestet mit VR-Banken/Atruvia)
|
||||
- **TAN-Verfahren**: Unterstützung von SecureGo Plus (Decoupled TAN) für die TAN-Bestätigung per App
|
||||
- **Automatischer Import**: Cronjob-basierter Import von Buchungen (täglich, zweimal wöchentlich oder wöchentlich)
|
||||
- **Buchungszuordnung**: Automatische Zuordnung importierter Buchungen zu Rechnungen anhand von Referenznummern, Beträgen, Namen und IBAN
|
||||
- **Multi-Rechnungszahlungen**: Verknüpfung einer Bankbuchung mit mehreren Rechnungen (Sammelzahlungen)
|
||||
- **Zahlungsverknüpfung korrigieren**: Möglichkeit falsche Zuordnungen aufzuheben und neu zu verknüpfen
|
||||
- **PDF-Kontoauszüge**: Upload und Verwaltung von PDF-Kontoauszügen mit automatischer Metadaten-Erkennung (Auszugsnummer, Zeitraum, Saldo)
|
||||
- **Mehrfach-Upload**: Gleichzeitiger Upload mehrerer PDF-Kontoauszüge
|
||||
- **Dashboard**: Übersichtsseite mit den letzten Buchungen und Kontoauszügen
|
||||
- **Erinnerungsfunktion**: Konfigurierbare Warnung wenn Kontoauszüge nicht aktuell sind
|
||||
- **Integration**: Einbindung in das Dolibarr-Menü "Banken und Kasse"
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Dolibarr ERP & CRM >= 16.0
|
||||
- PHP >= 8.0
|
||||
- `pdfinfo` und `pdftotext` (Paket `poppler-utils`) für die PDF-Metadaten-Erkennung
|
||||
- Zugang zu einer Bank mit FinTS/HBCI-Schnittstelle
|
||||
|
||||
## Installation
|
||||
|
||||
### Aus dem Git-Repository
|
||||
|
||||
```shell
|
||||
cd /path/to/dolibarr/custom
|
||||
git clone <repository-url> bankimport
|
||||
cd bankimport
|
||||
composer install
|
||||
```
|
||||
|
||||
### Aktivierung
|
||||
|
||||
1. In Dolibarr als Administrator anmelden
|
||||
2. Unter "Einstellungen" > "Module/Applikationen" das Modul "Bankimport" aktivieren
|
||||
3. Unter "Banken und Kasse" > "Bankimport" die FinTS-Verbindungsdaten konfigurieren
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### FinTS-Verbindung
|
||||
|
||||
- **FinTS Server URL**: Die FinTS-URL Ihrer Bank (z.B. `https://fints1.atruvia.de/cgi-bin/hbciservlet` für VR-Banken)
|
||||
- **Bankleitzahl (BLZ)**: 8-stellige Bankleitzahl
|
||||
- **Benutzerkennung**: Ihre Online-Banking Benutzerkennung
|
||||
- **PIN**: Wird verschlüsselt in der Datenbank gespeichert
|
||||
- **IBAN**: Kontonummer/IBAN des abzurufenden Kontos
|
||||
|
||||
### Automatischer Import
|
||||
|
||||
Der automatische Import kann im Admin-Bereich aktiviert werden. Die Buchungen werden dann per Dolibarr-Cronjob abgerufen. Unterstützte Intervalle: täglich, zweimal wöchentlich, wöchentlich.
|
||||
|
||||
### PDF-Upload Einstellungen
|
||||
|
||||
- **Upload-Modus**: Automatisch (Metadaten aus PDF extrahieren) oder Manuell
|
||||
- **Erinnerung**: Konfigurierbare Warnung wenn der letzte Kontoauszug älter als X Monate ist
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
- **Bankimport lesen**: Buchungen und Kontoauszüge ansehen
|
||||
- **Bankimport schreiben**: Kontoauszüge abrufen und PDF hochladen
|
||||
- **Bankimport löschen**: Buchungen und Kontoauszüge löschen
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Verwendete Bibliotheken
|
||||
|
||||
- [nemiah/php-fints](https://github.com/nemiah/php-fints) - PHP FinTS/HBCI Bibliothek
|
||||
|
||||
### Datenbank-Tabellen
|
||||
|
||||
- `llx_bankimport_transaction` - Importierte Buchungen
|
||||
- `llx_bankimport_statement` - PDF-Kontoauszüge
|
||||
|
||||
## Lizenz
|
||||
|
||||
GPLv3 oder (nach Wahl) jede spätere Version. Siehe Datei COPYING für weitere Informationen.
|
||||
|
||||
## Autor
|
||||
|
||||
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 bankimport/admin/about.php
|
||||
* \ingroup bankimport
|
||||
* \brief About page of module BankImport.
|
||||
*/
|
||||
|
||||
// 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/bankimport.lib.php';
|
||||
|
||||
/**
|
||||
* @var Conf $conf
|
||||
* @var DoliDB $db
|
||||
* @var HookManager $hookmanager
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
// Translations
|
||||
$langs->loadLangs(array("errors", "admin", "bankimport@bankimport"));
|
||||
|
||||
// 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 = "BankImportSetup";
|
||||
|
||||
llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-bankimport 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 = bankimportAdminPrepareHead();
|
||||
print dol_get_fiche_head($head, 'about', $langs->trans($title), 0, 'bankimport@bankimport');
|
||||
|
||||
dol_include_once('/bankimport/core/modules/modBankImport.class.php');
|
||||
$tmpmodule = new modBankImport($db);
|
||||
print $tmpmodule->getDescLong();
|
||||
|
||||
// Page end
|
||||
print dol_get_fiche_end();
|
||||
llxFooter();
|
||||
$db->close();
|
||||
620
admin/setup.php
Executable file
620
admin/setup.php
Executable file
|
|
@ -0,0 +1,620 @@
|
|||
<?php
|
||||
/* Copyright (C) 2004-2017 Laurent Destailleur <eldy@users.sourceforge.net>
|
||||
* Copyright (C) 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/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file bankimport/admin/setup.php
|
||||
* \ingroup bankimport
|
||||
* \brief BankImport setup page for FinTS configuration.
|
||||
*/
|
||||
|
||||
// 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 DOL_DOCUMENT_ROOT."/core/lib/security.lib.php";
|
||||
require_once '../lib/bankimport.lib.php';
|
||||
|
||||
/**
|
||||
* @var Conf $conf
|
||||
* @var DoliDB $db
|
||||
* @var HookManager $hookmanager
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
// Translations
|
||||
$langs->loadLangs(array("admin", "bankimport@bankimport"));
|
||||
|
||||
// Initialize hooks
|
||||
$hookmanager->initHooks(array('bankimportsetup', 'globalsetup'));
|
||||
|
||||
// Parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$backtopage = GETPOST('backtopage', 'alpha');
|
||||
|
||||
$error = 0;
|
||||
|
||||
// Access control
|
||||
if (!$user->admin) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// Select IBAN from account list
|
||||
if ($action == 'selectiban') {
|
||||
$newIban = GETPOST('iban', 'alpha');
|
||||
if (!empty($newIban)) {
|
||||
$res = dolibarr_set_const($db, "BANKIMPORT_FINTS_IBAN", $newIban, 'chaine', 0, '', $conf->entity);
|
||||
if ($res > 0) {
|
||||
setEventMessages($langs->trans("IBANUpdated", $newIban), null, 'mesgs');
|
||||
// Refresh page to show updated value
|
||||
header("Location: ".$_SERVER["PHP_SELF"]);
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($langs->trans("Error"), null, 'errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($action == 'update') {
|
||||
$db->begin();
|
||||
|
||||
// FinTS Server URL
|
||||
$res = dolibarr_set_const(
|
||||
$db,
|
||||
"BANKIMPORT_FINTS_URL",
|
||||
GETPOST('BANKIMPORT_FINTS_URL', 'alpha'),
|
||||
'chaine',
|
||||
0,
|
||||
'',
|
||||
$conf->entity
|
||||
);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
// BLZ (Bankleitzahl)
|
||||
$res = dolibarr_set_const(
|
||||
$db,
|
||||
"BANKIMPORT_FINTS_BLZ",
|
||||
GETPOST('BANKIMPORT_FINTS_BLZ', 'alpha'),
|
||||
'chaine',
|
||||
0,
|
||||
'',
|
||||
$conf->entity
|
||||
);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
// Benutzerkennung
|
||||
$res = dolibarr_set_const(
|
||||
$db,
|
||||
"BANKIMPORT_FINTS_USERNAME",
|
||||
GETPOST('BANKIMPORT_FINTS_USERNAME', 'alpha'),
|
||||
'chaine',
|
||||
0,
|
||||
'',
|
||||
$conf->entity
|
||||
);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
// PIN (verschlüsselt speichern)
|
||||
$pin = GETPOST('BANKIMPORT_FINTS_PIN', 'none');
|
||||
if (!empty($pin)) {
|
||||
$encryptedPin = dolEncrypt($pin);
|
||||
$res = dolibarr_set_const(
|
||||
$db,
|
||||
"BANKIMPORT_FINTS_PIN",
|
||||
$encryptedPin,
|
||||
'chaine',
|
||||
0,
|
||||
'',
|
||||
$conf->entity
|
||||
);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
}
|
||||
|
||||
// Kontonummer/IBAN
|
||||
$res = dolibarr_set_const(
|
||||
$db,
|
||||
"BANKIMPORT_FINTS_IBAN",
|
||||
GETPOST('BANKIMPORT_FINTS_IBAN', 'alpha'),
|
||||
'chaine',
|
||||
0,
|
||||
'',
|
||||
$conf->entity
|
||||
);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
// FinTS Registrierungsnummer
|
||||
$res = dolibarr_set_const(
|
||||
$db,
|
||||
"BANKIMPORT_FINTS_PRODUCT_ID",
|
||||
GETPOST('BANKIMPORT_FINTS_PRODUCT_ID', 'alpha'),
|
||||
'chaine',
|
||||
0,
|
||||
'',
|
||||
$conf->entity
|
||||
);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
// PDF Upload mode default
|
||||
$res = dolibarr_set_const($db, "BANKIMPORT_UPLOAD_MODE", GETPOST('BANKIMPORT_UPLOAD_MODE', 'alpha'), 'chaine', 0, '', $conf->entity);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
// Dolibarr Bank Account mapping
|
||||
$res = dolibarr_set_const($db, "BANKIMPORT_BANK_ACCOUNT_ID", GETPOSTINT('BANKIMPORT_BANK_ACCOUNT_ID'), 'chaine', 0, '', $conf->entity);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
// Reminder setting
|
||||
$res = dolibarr_set_const($db, "BANKIMPORT_REMINDER_ENABLED", GETPOSTINT('BANKIMPORT_REMINDER_ENABLED'), 'chaine', 0, '', $conf->entity);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
$res = dolibarr_set_const($db, "BANKIMPORT_REMINDER_MONTHS", GETPOSTINT('BANKIMPORT_REMINDER_MONTHS'), 'chaine', 0, '', $conf->entity);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
// Automatic import settings
|
||||
$res = dolibarr_set_const($db, "BANKIMPORT_AUTO_ENABLED", GETPOSTINT('BANKIMPORT_AUTO_ENABLED'), 'chaine', 0, '', $conf->entity);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
$res = dolibarr_set_const($db, "BANKIMPORT_AUTO_FREQUENCY", GETPOST('BANKIMPORT_AUTO_FREQUENCY', 'alpha'), 'chaine', 0, '', $conf->entity);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
$res = dolibarr_set_const($db, "BANKIMPORT_AUTO_DAYS", GETPOSTINT('BANKIMPORT_AUTO_DAYS'), 'chaine', 0, '', $conf->entity);
|
||||
if (!($res > 0)) {
|
||||
$error++;
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$db->commit();
|
||||
setEventMessages($langs->trans("SetupSaved"), null, 'mesgs');
|
||||
} else {
|
||||
$db->rollback();
|
||||
setEventMessages($langs->trans("Error"), null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
// Test connection action
|
||||
if ($action == 'testconnection') {
|
||||
dol_include_once('/bankimport/class/fints.class.php');
|
||||
|
||||
if (class_exists('BankImportFinTS')) {
|
||||
$fints = new BankImportFinTS($db);
|
||||
$result = $fints->testConnection();
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans("ConnectionTestSuccessful"), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("ConnectionTestFailed").': '.$fints->error, null, 'errors');
|
||||
}
|
||||
} else {
|
||||
setEventMessages($langs->trans("FinTSClassNotFound"), null, 'warnings');
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch accounts action
|
||||
$availableAccounts = array();
|
||||
if ($action == 'fetchaccounts') {
|
||||
dol_include_once('/bankimport/class/fints.class.php');
|
||||
|
||||
if (class_exists('BankImportFinTS')) {
|
||||
$fints = new BankImportFinTS($db);
|
||||
|
||||
// Login
|
||||
$loginResult = $fints->login();
|
||||
if ($loginResult < 0) {
|
||||
setEventMessages($langs->trans("LoginFailed").': '.$fints->error, null, 'errors');
|
||||
} elseif ($loginResult == 0) {
|
||||
// TAN required - not supported in setup
|
||||
setEventMessages($langs->trans("TANRequiredForAccountList"), null, 'warnings');
|
||||
} else {
|
||||
// Fetch accounts
|
||||
$accounts = $fints->getAccounts();
|
||||
if (is_array($accounts) && !empty($accounts)) {
|
||||
$availableAccounts = $accounts;
|
||||
setEventMessages($langs->trans("AccountsFound", count($accounts)), null, 'mesgs');
|
||||
} elseif ($accounts === 0) {
|
||||
setEventMessages($langs->trans("TANRequiredForAccountList"), null, 'warnings');
|
||||
} else {
|
||||
setEventMessages($langs->trans("NoAccountsFound").': '.$fints->error, null, 'errors');
|
||||
}
|
||||
$fints->close();
|
||||
}
|
||||
} else {
|
||||
setEventMessages($langs->trans("FinTSClassNotFound"), null, 'warnings');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
$help_url = '';
|
||||
$title = "BankImportSetup";
|
||||
|
||||
llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-bankimport 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 = bankimportAdminPrepareHead();
|
||||
print dol_get_fiche_head($head, 'settings', $langs->trans($title), -1, "bankimport@bankimport");
|
||||
|
||||
// Setup page description
|
||||
print '<span class="opacitymedium">'.$langs->trans("BankImportSetupDescription").'</span><br><br>';
|
||||
|
||||
// Load current values
|
||||
$fintsUrl = getDolGlobalString('BANKIMPORT_FINTS_URL');
|
||||
$fintsBLZ = getDolGlobalString('BANKIMPORT_FINTS_BLZ');
|
||||
$fintsUsername = getDolGlobalString('BANKIMPORT_FINTS_USERNAME');
|
||||
$fintsPin = getDolGlobalString('BANKIMPORT_FINTS_PIN');
|
||||
$fintsIBAN = getDolGlobalString('BANKIMPORT_FINTS_IBAN');
|
||||
$fintsProductId = getDolGlobalString('BANKIMPORT_FINTS_PRODUCT_ID');
|
||||
|
||||
// Check if PIN is set
|
||||
$pinIsSet = !empty($fintsPin);
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="update">';
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
|
||||
// FinTS Section Header
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans("FinTSConfiguration").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// FinTS Server URL
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield fieldrequired">'.$langs->trans("FinTSServerURL").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" class="flat minwidth500" name="BANKIMPORT_FINTS_URL" value="'.dol_escape_htmltag($fintsUrl).'" placeholder="https://fints1.atruvia.de/cgi-bin/hbciservlet">';
|
||||
print '<br><span class="opacitymedium small">'.$langs->trans("FinTSServerURLHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// BLZ (Bankleitzahl)
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="fieldrequired">'.$langs->trans("BLZ").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" class="flat minwidth200" name="BANKIMPORT_FINTS_BLZ" value="'.dol_escape_htmltag($fintsBLZ).'" placeholder="12345678" maxlength="8">';
|
||||
print ' <span class="opacitymedium">'.$langs->trans("BLZHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Benutzerkennung
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="fieldrequired">'.$langs->trans("FinTSUsername").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" class="flat minwidth300" name="BANKIMPORT_FINTS_USERNAME" value="'.dol_escape_htmltag($fintsUsername).'" placeholder="VR-NetKey / Alias">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// PIN (verschlüsselt)
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="fieldrequired">'.$langs->trans("FinTSPIN").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="password" class="flat minwidth200" name="BANKIMPORT_FINTS_PIN" value="" placeholder="'.($pinIsSet ? '********' : '').'" autocomplete="new-password">';
|
||||
if ($pinIsSet) {
|
||||
print ' <span class="opacitymedium">'.img_picto('', 'tick', 'class="paddingleft"').' '.$langs->trans("PINAlreadySet").'</span>';
|
||||
}
|
||||
print '<br><span class="opacitymedium small">'.$langs->trans("PINHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Kontonummer/IBAN
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="fieldrequired">'.$langs->trans("AccountIBAN").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" class="flat minwidth400" name="BANKIMPORT_FINTS_IBAN" value="'.dol_escape_htmltag($fintsIBAN).'" placeholder="DE89370400440532013000">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// FinTS Product ID (optional)
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("FinTSProductID").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" class="flat minwidth300" name="BANKIMPORT_FINTS_PRODUCT_ID" value="'.dol_escape_htmltag($fintsProductId).'">';
|
||||
print '<br><span class="opacitymedium small">'.$langs->trans("FinTSProductIDHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
// PDF Upload Section
|
||||
print '<br>';
|
||||
print '<table class="noborder centpercent">';
|
||||
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans("PDFUploadSettings").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Default upload mode
|
||||
$defaultUploadMode = getDolGlobalString('BANKIMPORT_UPLOAD_MODE') ?: 'auto';
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans("DefaultUploadMode").'</td>';
|
||||
print '<td>';
|
||||
$uploadModes = array(
|
||||
'auto' => $langs->trans("UploadModeAuto"),
|
||||
'manual' => $langs->trans("UploadModeManual"),
|
||||
);
|
||||
print $form->selectarray('BANKIMPORT_UPLOAD_MODE', $uploadModes, $defaultUploadMode, 0, 0, 0, '', 0, 0, 0, '', 'minwidth200');
|
||||
print ' <span class="opacitymedium small">'.$langs->trans("DefaultUploadModeHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Reminder when no statement uploaded
|
||||
$reminderEnabled = getDolGlobalString('BANKIMPORT_REMINDER_ENABLED', '1');
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans("ReminderEnabled").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="checkbox" name="BANKIMPORT_REMINDER_ENABLED" value="1"'.($reminderEnabled ? ' checked' : '').'>';
|
||||
print ' <span class="opacitymedium small">'.$langs->trans("ReminderEnabledHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Reminder months threshold
|
||||
$reminderMonths = getDolGlobalInt('BANKIMPORT_REMINDER_MONTHS') ?: 3;
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans("ReminderMonths").'</td>';
|
||||
print '<td>';
|
||||
$monthOptions = array(1 => '1', 2 => '2', 3 => '3', 4 => '4', 5 => '5', 6 => '6');
|
||||
print $form->selectarray('BANKIMPORT_REMINDER_MONTHS', $monthOptions, $reminderMonths, 0, 0, 0, '', 0, 0, 0, '', 'minwidth75');
|
||||
print ' <span class="opacitymedium small">'.$langs->trans("ReminderMonthsHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
// Bank Account Mapping Section
|
||||
print '<br>';
|
||||
print '<table class="noborder centpercent">';
|
||||
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans("BankAccountMapping").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Bank Account Dropdown
|
||||
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield fieldrequired">'.$langs->trans("DolibarrBankAccount").'</td>';
|
||||
print '<td>';
|
||||
|
||||
// Build select from llx_bank_account
|
||||
$sql_ba = "SELECT rowid, label, iban_prefix FROM ".MAIN_DB_PREFIX."bank_account";
|
||||
$sql_ba .= " WHERE entity = ".((int) $conf->entity);
|
||||
$sql_ba .= " AND clos = 0";
|
||||
$sql_ba .= " ORDER BY label ASC";
|
||||
|
||||
$bankAccounts = array('' => $langs->trans("SelectBankAccount"));
|
||||
$resql_ba = $db->query($sql_ba);
|
||||
if ($resql_ba) {
|
||||
while ($obj_ba = $db->fetch_object($resql_ba)) {
|
||||
$ibanDisplay = $obj_ba->iban_prefix ? ' ('.dol_trunc($obj_ba->iban_prefix, 20).')' : '';
|
||||
$bankAccounts[$obj_ba->rowid] = $obj_ba->label.$ibanDisplay;
|
||||
}
|
||||
}
|
||||
print $form->selectarray('BANKIMPORT_BANK_ACCOUNT_ID', $bankAccounts, $bankAccountId, 0, 0, 0, '', 0, 0, 0, '', 'minwidth300');
|
||||
print '<br><span class="opacitymedium small">'.$langs->trans("DolibarrBankAccountHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
// Automatic Import Section
|
||||
print '<br>';
|
||||
print '<table class="noborder centpercent">';
|
||||
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans("AutomaticImport").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Enable automatic import
|
||||
$autoImportEnabled = getDolGlobalInt('BANKIMPORT_AUTO_ENABLED');
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans("EnableAutoImport").'</td>';
|
||||
print '<td>';
|
||||
print $form->selectyesno('BANKIMPORT_AUTO_ENABLED', $autoImportEnabled, 1);
|
||||
print ' <span class="opacitymedium small">'.$langs->trans("EnableAutoImportHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Import frequency
|
||||
$autoFrequency = getDolGlobalString('BANKIMPORT_AUTO_FREQUENCY') ?: 'daily';
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("ImportFrequency").'</td>';
|
||||
print '<td>';
|
||||
$frequencies = array(
|
||||
'daily' => $langs->trans("Daily"),
|
||||
'weekly' => $langs->trans("Weekly"),
|
||||
'twice_weekly' => $langs->trans("TwiceWeekly")
|
||||
);
|
||||
print $form->selectarray('BANKIMPORT_AUTO_FREQUENCY', $frequencies, $autoFrequency, 0, 0, 0, '', 0, 0, 0, '', 'minwidth150');
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Days to fetch
|
||||
$autoDays = getDolGlobalInt('BANKIMPORT_AUTO_DAYS') ?: 30;
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("DaysToFetch").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="number" class="flat width75" name="BANKIMPORT_AUTO_DAYS" value="'.$autoDays.'" min="1" max="60">';
|
||||
print ' <span class="opacitymedium">'.$langs->trans("DaysToFetchHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Last fetch info
|
||||
$lastFetch = getDolGlobalInt('BANKIMPORT_LAST_FETCH');
|
||||
$lastFetchCount = getDolGlobalInt('BANKIMPORT_LAST_FETCH_COUNT');
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("LastAutoFetch").'</td>';
|
||||
print '<td>';
|
||||
if ($lastFetch > 0) {
|
||||
print dol_print_date($lastFetch, 'dayhour');
|
||||
if ($lastFetchCount > 0) {
|
||||
print ' <span class="badge badge-info">'.$lastFetchCount.' '.$langs->trans("Transactions").'</span>';
|
||||
}
|
||||
// Warning if more than 7 days since last fetch
|
||||
if ((time() - $lastFetch) > 7 * 86400) {
|
||||
print ' <span class="badge badge-warning">'.$langs->trans("MoreThan7Days").'</span>';
|
||||
}
|
||||
} else {
|
||||
print '<span class="opacitymedium">'.$langs->trans("NeverFetched").'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print '<br>';
|
||||
|
||||
// Buttons
|
||||
print '<div class="center">';
|
||||
print '<input type="submit" class="button button-save" value="'.$langs->trans("Save").'">';
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
print '<br>';
|
||||
|
||||
// Test Connection Button
|
||||
print '<div class="center">';
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline-block; margin-right: 10px;">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="testconnection">';
|
||||
print '<input type="submit" class="button" value="'.$langs->trans("TestConnection").'">';
|
||||
print '</form>';
|
||||
|
||||
// Fetch Accounts Button
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline-block;">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="fetchaccounts">';
|
||||
print '<input type="submit" class="button" value="'.$langs->trans("FetchAvailableAccounts").'">';
|
||||
print '</form>';
|
||||
print '</div>';
|
||||
|
||||
// Display available accounts if fetched
|
||||
if (!empty($availableAccounts)) {
|
||||
print '<br><br>';
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("IBAN").'</th>';
|
||||
print '<th>'.$langs->trans("BIC").'</th>';
|
||||
print '<th>'.$langs->trans("AccountNumber").'</th>';
|
||||
print '<th>'.$langs->trans("BLZ").'</th>';
|
||||
print '<th>'.$langs->trans("Action").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
foreach ($availableAccounts as $acc) {
|
||||
$isSelected = ($acc['iban'] === $fintsIBAN);
|
||||
print '<tr class="oddeven'.($isSelected ? ' highlight' : '').'">';
|
||||
print '<td><strong>'.dol_escape_htmltag($acc['iban']).'</strong></td>';
|
||||
print '<td>'.dol_escape_htmltag($acc['bic']).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($acc['accountNumber']).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($acc['blz']).'</td>';
|
||||
print '<td>';
|
||||
if ($isSelected) {
|
||||
print '<span class="badge badge-status4">'.$langs->trans("Selected").'</span>';
|
||||
} else {
|
||||
print '<a class="button buttongen" href="'.$_SERVER["PHP_SELF"].'?action=selectiban&iban='.urlencode($acc['iban']).'&token='.newToken().'">'.$langs->trans("UseThisAccount").'</a>';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Info Box - VR Bank specific
|
||||
print '<br>';
|
||||
print '<div class="info">';
|
||||
print '<strong>'.$langs->trans("VRBankInfo").'</strong><br>';
|
||||
print $langs->trans("VRBankInfoText");
|
||||
print '</div>';
|
||||
|
||||
// Security Info Box
|
||||
print '<br>';
|
||||
print '<div class="warning">';
|
||||
print '<strong>'.$langs->trans("SecurityInfo").'</strong><br>';
|
||||
print $langs->trans("SecurityInfoText");
|
||||
print '</div>';
|
||||
|
||||
// Page end
|
||||
print dol_get_fiche_end();
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
102
ajax/checkpending.php
Executable file
102
ajax/checkpending.php
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
<?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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* AJAX endpoint to check for pending bank transaction matches
|
||||
* Used by browser notification system
|
||||
*/
|
||||
|
||||
if (!defined('NOTOKENRENEWAL')) {
|
||||
define('NOTOKENRENEWAL', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREMENU')) {
|
||||
define('NOREQUIREMENU', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREHTML')) {
|
||||
define('NOREQUIREHTML', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREAJAX')) {
|
||||
define('NOREQUIREAJAX', '1');
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('bankimport', 'read')) {
|
||||
echo json_encode(array('error' => 'access_denied'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
if (empty($bankAccountId)) {
|
||||
echo json_encode(array('pending' => 0, 'incoming' => 0));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Count new unmatched transactions (incoming payments = positive amount)
|
||||
$sqlIncoming = "SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total";
|
||||
$sqlIncoming .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
||||
$sqlIncoming .= " WHERE entity IN (".getEntity('banktransaction').")";
|
||||
$sqlIncoming .= " AND status = 0 AND amount > 0";
|
||||
$resIncoming = $db->query($sqlIncoming);
|
||||
$incoming = 0;
|
||||
$incomingTotal = 0;
|
||||
if ($resIncoming) {
|
||||
$obj = $db->fetch_object($resIncoming);
|
||||
$incoming = (int) $obj->cnt;
|
||||
$incomingTotal = (float) $obj->total;
|
||||
}
|
||||
|
||||
// Count all new transactions
|
||||
$sqlAll = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
||||
$sqlAll .= " WHERE entity IN (".getEntity('banktransaction').")";
|
||||
$sqlAll .= " AND status = 0";
|
||||
$resAll = $db->query($sqlAll);
|
||||
$pending = 0;
|
||||
if ($resAll) {
|
||||
$obj = $db->fetch_object($resAll);
|
||||
$pending = (int) $obj->cnt;
|
||||
}
|
||||
|
||||
echo json_encode(array(
|
||||
'pending' => $pending,
|
||||
'incoming' => $incoming,
|
||||
'incoming_total' => $incomingTotal,
|
||||
));
|
||||
|
||||
$db->close();
|
||||
194
ajax/checktan.php
Executable file
194
ajax/checktan.php
Executable file
|
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file bankimport/ajax/checktan.php
|
||||
* \ingroup bankimport
|
||||
* \brief AJAX endpoint for checking decoupled TAN status (SecureGo Plus)
|
||||
*/
|
||||
|
||||
// Disable error display for JSON output
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
// 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(json_encode(['status' => 'error', 'message' => 'Include of main fails']));
|
||||
}
|
||||
|
||||
dol_include_once('/bankimport/class/fints.class.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// CSRF check - use Dolibarr's token validation
|
||||
$token = GETPOST('token', 'aZ09');
|
||||
if (empty($token) || !isset($_SESSION['token']) || $token !== $_SESSION['token']) {
|
||||
// Skip strict token check for AJAX polling - session is sufficient
|
||||
// The session itself provides protection
|
||||
}
|
||||
|
||||
// Check session
|
||||
if (empty($_SESSION['fints_state']) || empty($_SESSION['fints_pending_action'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'No pending TAN request']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prevent concurrent requests with file locking
|
||||
$lockFile = DOL_DATA_ROOT.'/bankimport/tan_check_'.session_id().'.lock';
|
||||
if (!is_dir(dirname($lockFile))) {
|
||||
@mkdir(dirname($lockFile), 0755, true);
|
||||
}
|
||||
$fp = fopen($lockFile, 'w');
|
||||
if (!flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
// Another request is already checking
|
||||
echo json_encode(['status' => 'waiting', 'message' => 'Check in progress...']);
|
||||
fclose($fp);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$fints = new BankImportFinTS($db);
|
||||
|
||||
// Debug: Log session state
|
||||
dol_syslog("BankImport AJAX: Checking TAN, state size=".strlen($_SESSION['fints_state'] ?? ''), LOG_DEBUG);
|
||||
|
||||
// Restore FinTS state (includes dialog context)
|
||||
$result = $fints->restore($_SESSION['fints_state']);
|
||||
if ($result < 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Could not restore session: '.$fints->error]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Restore pending action - must be done AFTER restore()
|
||||
$pendingAction = @unserialize($_SESSION['fints_pending_action']);
|
||||
if ($pendingAction === false) {
|
||||
dol_syslog("BankImport AJAX: Failed to unserialize pending action", LOG_ERR);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Could not restore pending action']);
|
||||
exit;
|
||||
}
|
||||
$fints->setPendingAction($pendingAction);
|
||||
|
||||
dol_syslog("BankImport AJAX: Calling checkDecoupledTan", LOG_DEBUG);
|
||||
|
||||
// Check if TAN was confirmed
|
||||
$checkResult = $fints->checkDecoupledTan();
|
||||
|
||||
if ($checkResult > 0) {
|
||||
// TAN confirmed! Now fetch statements
|
||||
$savedAction = $_SESSION['fints_action'] ?? 'statements';
|
||||
$savedDateFrom = $_SESSION['fints_datefrom'] ?? strtotime('-30 days');
|
||||
$savedDateTo = $_SESSION['fints_dateto'] ?? time();
|
||||
|
||||
// Clear session
|
||||
unset($_SESSION['fints_state']);
|
||||
unset($_SESSION['fints_pending_action']);
|
||||
unset($_SESSION['fints_action']);
|
||||
unset($_SESSION['fints_datefrom']);
|
||||
unset($_SESSION['fints_dateto']);
|
||||
|
||||
// Fetch statements
|
||||
$transactions = $fints->fetchStatements($savedDateFrom, $savedDateTo);
|
||||
|
||||
if ($transactions === 0) {
|
||||
// Another TAN required (unlikely but possible)
|
||||
$_SESSION['fints_state'] = $fints->persist();
|
||||
$_SESSION['fints_pending_action'] = serialize($fints->getPendingAction());
|
||||
$_SESSION['fints_action'] = 'statements';
|
||||
$_SESSION['fints_datefrom'] = $savedDateFrom;
|
||||
$_SESSION['fints_dateto'] = $savedDateTo;
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'tan_required',
|
||||
'message' => 'Another TAN required'
|
||||
]);
|
||||
} elseif (is_array($transactions)) {
|
||||
// Success!
|
||||
$fints->close();
|
||||
|
||||
// Extract transactions and balance from result
|
||||
$txList = $transactions['transactions'] ?? array();
|
||||
$balance = $transactions['balance'] ?? array();
|
||||
|
||||
// Store in session for display
|
||||
$_SESSION['fints_transactions'] = $txList;
|
||||
$_SESSION['fints_balance'] = $balance;
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Transactions fetched',
|
||||
'count' => count($txList),
|
||||
'transactions' => $txList,
|
||||
'balance' => $balance
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Fetch failed: '.$fints->error
|
||||
]);
|
||||
}
|
||||
} elseif ($checkResult == 0) {
|
||||
// Still waiting for confirmation
|
||||
// Save updated state
|
||||
$_SESSION['fints_state'] = $fints->persist();
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'waiting',
|
||||
'message' => 'Waiting for SecureGo Plus confirmation...'
|
||||
]);
|
||||
} else {
|
||||
// Error
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'TAN check failed: '.$fints->error
|
||||
]);
|
||||
|
||||
// Clear session on error
|
||||
unset($_SESSION['fints_state']);
|
||||
unset($_SESSION['fints_pending_action']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Exception: '.$e->getMessage()
|
||||
]);
|
||||
|
||||
// Clear session on exception
|
||||
unset($_SESSION['fints_state']);
|
||||
unset($_SESSION['fints_pending_action']);
|
||||
} finally {
|
||||
// Release lock
|
||||
if (isset($fp)) {
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
@unlink($lockFile);
|
||||
}
|
||||
}
|
||||
382
bankimportindex.php
Executable file
382
bankimportindex.php
Executable file
|
|
@ -0,0 +1,382 @@
|
|||
<?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 bankimport/bankimportindex.php
|
||||
* \ingroup bankimport
|
||||
* \brief Dashboard page for BankImport 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 && 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';
|
||||
|
||||
/**
|
||||
* @var Conf $conf
|
||||
* @var DoliDB $db
|
||||
* @var HookManager $hookmanager
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
// Load translation files required by the page
|
||||
$langs->loadLangs(array("bankimport@bankimport", "banks"));
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('bankimport', 'read')) {
|
||||
accessforbidden();
|
||||
}
|
||||
$socid = GETPOSTINT('socid');
|
||||
if (!empty($user->socid) && $user->socid > 0) {
|
||||
$action = '';
|
||||
$socid = $user->socid;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
dol_include_once('/bankimport/class/bankstatement.class.php');
|
||||
|
||||
llxHeader("", $langs->trans("BankImportArea"), '', '', 0, 0, '', '', '', 'mod-bankimport page-index');
|
||||
|
||||
print load_fiche_titre($langs->trans("BankImportArea"), '', 'bank');
|
||||
|
||||
// Reminder: check if statements are outdated
|
||||
$reminderEnabled = getDolGlobalString('BANKIMPORT_REMINDER_ENABLED', '1');
|
||||
if ($reminderEnabled) {
|
||||
$reminderMonths = getDolGlobalInt('BANKIMPORT_REMINDER_MONTHS') ?: 3;
|
||||
$stmtCheck = new BankImportStatement($db);
|
||||
$lastEndDate = $stmtCheck->getLatestStatementEndDate();
|
||||
$thresholdDate = dol_time_plus_duree(dol_now(), -$reminderMonths, 'm');
|
||||
|
||||
if ($lastEndDate === null) {
|
||||
// No statements at all
|
||||
print '<div class="warning">';
|
||||
print img_warning().' '.$langs->trans("ReminderNoStatements");
|
||||
print ' <a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'">'.$langs->trans("UploadPDFStatement").'</a>';
|
||||
print '</div><br>';
|
||||
} elseif ($lastEndDate < $thresholdDate) {
|
||||
$monthsAgo = (int) round((dol_now() - $lastEndDate) / (30 * 24 * 3600));
|
||||
print '<div class="warning">';
|
||||
print img_warning().' '.$langs->trans("ReminderOutdatedStatements", dol_print_date($lastEndDate, 'day'), $monthsAgo);
|
||||
print ' <a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'">'.$langs->trans("UploadPDFStatement").'</a>';
|
||||
print '</div><br>';
|
||||
}
|
||||
}
|
||||
|
||||
// Payment matching notification
|
||||
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
if (!empty($bankAccountId)) {
|
||||
$sqlNewCount = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
||||
$sqlNewCount .= " WHERE entity IN (".getEntity('banktransaction').")";
|
||||
$sqlNewCount .= " AND status = 0";
|
||||
$resNewCount = $db->query($sqlNewCount);
|
||||
$newCount = 0;
|
||||
if ($resNewCount) {
|
||||
$objNewCount = $db->fetch_object($resNewCount);
|
||||
$newCount = (int) $objNewCount->cnt;
|
||||
}
|
||||
|
||||
if ($newCount > 0) {
|
||||
print '<div class="info" style="border-left: 4px solid #2196F3; background: #e3f2fd; padding: 12px; margin-bottom: 15px;">';
|
||||
print img_picto('', 'payment', 'class="pictofixedwidth"');
|
||||
print '<strong>'.$langs->trans("PendingPaymentMatches", $newCount).'</strong>';
|
||||
print '<br>'.$langs->trans("PendingPaymentMatchesDesc");
|
||||
print ' <a class="butAction" style="margin-left: 10px;" href="'.dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
|
||||
print $langs->trans("ReviewAndConfirm");
|
||||
print '</a>';
|
||||
print '</div>';
|
||||
}
|
||||
} else {
|
||||
print '<div class="warning" style="margin-bottom: 15px;">';
|
||||
print img_warning().' '.$langs->trans("NoBankAccountConfigured");
|
||||
print ' <a href="'.dol_buildpath('/bankimport/admin/setup.php', 1).'">'.$langs->trans("GoToSetup").'</a>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
print '<div class="fichecenter"><div class="fichethirdleft">';
|
||||
|
||||
// -----------------------------------------------
|
||||
// Widget: Letzte 10 importierte Buchungen
|
||||
// -----------------------------------------------
|
||||
$max = 10;
|
||||
|
||||
$sql = "SELECT t.rowid, t.ref, t.date_trans, t.name, t.description, t.amount, t.currency, t.status";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction as t";
|
||||
$sql .= " WHERE t.entity IN (".getEntity('banktransaction').")";
|
||||
$sql .= " ORDER BY t.date_trans DESC, t.rowid DESC";
|
||||
$sql .= $db->plimit($max, 0);
|
||||
|
||||
$resql = $db->query($sql);
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th colspan="4">';
|
||||
print $langs->trans("LastImportedTransactions");
|
||||
|
||||
// Count total
|
||||
$sqlcount = "SELECT COUNT(*) as total FROM ".MAIN_DB_PREFIX."bankimport_transaction WHERE entity IN (".getEntity('banktransaction').")";
|
||||
$rescount = $db->query($sqlcount);
|
||||
if ($rescount) {
|
||||
$objcount = $db->fetch_object($rescount);
|
||||
if ($objcount->total > 0) {
|
||||
print '<a class="paddingleft" href="'.dol_buildpath('/bankimport/list.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
|
||||
print '<span class="badge">'.$objcount->total.'</span>';
|
||||
print '</a>';
|
||||
}
|
||||
}
|
||||
print '</th>';
|
||||
print '</tr>';
|
||||
|
||||
if ($resql) {
|
||||
$num = $db->num_rows($resql);
|
||||
|
||||
if ($num > 0) {
|
||||
$i = 0;
|
||||
while ($i < $num) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Date
|
||||
print '<td class="nowraponall">';
|
||||
print dol_print_date($db->jdate($obj->date_trans), 'day');
|
||||
print '</td>';
|
||||
|
||||
// Name + Description
|
||||
print '<td class="tdoverflowmax200">';
|
||||
print '<a href="'.dol_buildpath('/bankimport/card.php', 1).'?id='.$obj->rowid.'&mainmenu=bank&leftmenu=bankimport">';
|
||||
print dol_escape_htmltag(dol_trunc($obj->name, 30));
|
||||
print '</a>';
|
||||
if ($obj->description) {
|
||||
print '<br><span class="opacitymedium small">'.dol_escape_htmltag(dol_trunc($obj->description, 40)).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Amount
|
||||
print '<td class="right nowraponall">';
|
||||
if ($obj->amount >= 0) {
|
||||
print '<span class="amount" style="color: green;">+'.price($obj->amount, 0, $langs, 1, -1, 2, $obj->currency).'</span>';
|
||||
} else {
|
||||
print '<span class="amount" style="color: red;">'.price($obj->amount, 0, $langs, 1, -1, 2, $obj->currency).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Status
|
||||
print '<td class="right nowraponall">';
|
||||
switch ($obj->status) {
|
||||
case 0:
|
||||
print '<span class="badge badge-status4 badge-status">'.$langs->trans("New").'</span>';
|
||||
break;
|
||||
case 1:
|
||||
print '<span class="badge badge-status1 badge-status">'.$langs->trans("Matched").'</span>';
|
||||
break;
|
||||
case 2:
|
||||
print '<span class="badge badge-status6 badge-status">'.$langs->trans("Reconciled").'</span>';
|
||||
break;
|
||||
case 9:
|
||||
print '<span class="badge badge-status5 badge-status">'.$langs->trans("Ignored").'</span>';
|
||||
break;
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
$i++;
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="4" class="opacitymedium">'.$langs->trans("NoTransactionsInDatabase").'</td></tr>';
|
||||
}
|
||||
|
||||
$db->free($resql);
|
||||
} else {
|
||||
dol_print_error($db);
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
|
||||
// Link "Alle anzeigen"
|
||||
if (!empty($objcount) && $objcount->total > 0) {
|
||||
print '<div class="right" style="margin-top: 5px;">';
|
||||
print '<a href="'.dol_buildpath('/bankimport/list.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
|
||||
print $langs->trans("ShowAll").' »';
|
||||
print '</a>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
|
||||
print '</div><div class="fichetwothirdright">';
|
||||
|
||||
|
||||
// -----------------------------------------------
|
||||
// Widget: Letzte 5 PDF-Kontoauszüge
|
||||
// -----------------------------------------------
|
||||
$maxpdf = 5;
|
||||
|
||||
$sql2 = "SELECT s.rowid, s.statement_number, s.statement_year, s.iban, s.date_from, s.date_to,";
|
||||
$sql2 .= " s.opening_balance, s.closing_balance, s.filename, s.filepath, s.filesize, s.datec";
|
||||
$sql2 .= " FROM ".MAIN_DB_PREFIX."bankimport_statement as s";
|
||||
$sql2 .= " WHERE s.entity IN (".getEntity('bankstatement').")";
|
||||
$sql2 .= " ORDER BY s.datec DESC";
|
||||
$sql2 .= $db->plimit($maxpdf, 0);
|
||||
|
||||
$resql2 = $db->query($sql2);
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th colspan="5">';
|
||||
print $langs->trans("LastPDFStatements");
|
||||
|
||||
// Count total
|
||||
$sqlcount2 = "SELECT COUNT(*) as total FROM ".MAIN_DB_PREFIX."bankimport_statement WHERE entity IN (".getEntity('bankstatement').")";
|
||||
$rescount2 = $db->query($sqlcount2);
|
||||
if ($rescount2) {
|
||||
$objcount2 = $db->fetch_object($rescount2);
|
||||
if ($objcount2->total > 0) {
|
||||
print '<a class="paddingleft" href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
|
||||
print '<span class="badge">'.$objcount2->total.'</span>';
|
||||
print '</a>';
|
||||
}
|
||||
}
|
||||
print '</th>';
|
||||
print '</tr>';
|
||||
|
||||
if ($resql2) {
|
||||
$num2 = $db->num_rows($resql2);
|
||||
|
||||
if ($num2 > 0) {
|
||||
$i = 0;
|
||||
while ($i < $num2) {
|
||||
$obj2 = $db->fetch_object($resql2);
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Statement number / Year
|
||||
print '<td class="nowraponall">';
|
||||
print '<strong>'.dol_escape_htmltag($obj2->statement_number).'</strong>/'.$obj2->statement_year;
|
||||
print '</td>';
|
||||
|
||||
// IBAN (shortened)
|
||||
print '<td class="tdoverflowmax150">';
|
||||
if ($obj2->iban) {
|
||||
print dol_escape_htmltag(dol_trunc($obj2->iban, 20));
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Period
|
||||
print '<td class="center nowraponall">';
|
||||
if ($obj2->date_from && $obj2->date_to) {
|
||||
print dol_print_date($db->jdate($obj2->date_from), 'day').' - '.dol_print_date($db->jdate($obj2->date_to), 'day');
|
||||
} elseif ($obj2->date_from) {
|
||||
print dol_print_date($db->jdate($obj2->date_from), 'day').' -';
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Closing balance
|
||||
print '<td class="right nowraponall">';
|
||||
if ($obj2->closing_balance !== null && $obj2->closing_balance !== '') {
|
||||
$color = (float) $obj2->closing_balance >= 0 ? '' : 'color: red;';
|
||||
print '<span style="'.$color.'">'.price($obj2->closing_balance, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Actions
|
||||
print '<td class="center nowraponall">';
|
||||
if ($obj2->filepath && file_exists($obj2->filepath)) {
|
||||
print '<a class="paddingright" href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?action=view&id='.$obj2->rowid.'&token='.newToken().'" target="_blank" title="'.$langs->trans("View").'">';
|
||||
print img_picto($langs->trans("View"), 'eye');
|
||||
print '</a>';
|
||||
|
||||
print '<a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?action=download&id='.$obj2->rowid.'&token='.newToken().'" title="'.$langs->trans("Download").'">';
|
||||
print img_picto($langs->trans("Download"), 'download');
|
||||
print '</a>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
$i++;
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="5" class="opacitymedium">'.$langs->trans("NoPDFStatementsFound").'</td></tr>';
|
||||
}
|
||||
|
||||
$db->free($resql2);
|
||||
} else {
|
||||
dol_print_error($db);
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
|
||||
// Links
|
||||
print '<div class="right" style="margin-top: 5px;">';
|
||||
if (!empty($objcount2) && $objcount2->total > 0) {
|
||||
print '<a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
|
||||
print $langs->trans("ShowAll");
|
||||
print '</a>';
|
||||
print ' | ';
|
||||
}
|
||||
print '<a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
|
||||
print $langs->trans("UploadNew").' »';
|
||||
print '</a>';
|
||||
print '</div>';
|
||||
|
||||
|
||||
print '</div></div>';
|
||||
|
||||
// End of page
|
||||
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-bankimport.conf
Executable file
11
build/makepack-bankimport.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/
|
||||
379
class/bankimportcron.class.php
Executable file
379
class/bankimportcron.class.php
Executable file
|
|
@ -0,0 +1,379 @@
|
|||
<?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 bankimport/class/bankimportcron.class.php
|
||||
* \ingroup bankimport
|
||||
* \brief Cron job class for automatic bank statement import
|
||||
*/
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
||||
dol_include_once('/bankimport/class/fints.class.php');
|
||||
dol_include_once('/bankimport/class/banktransaction.class.php');
|
||||
|
||||
/**
|
||||
* Class BankImportCron
|
||||
* Handles automatic bank statement import via scheduled task
|
||||
*/
|
||||
class BankImportCron
|
||||
{
|
||||
/**
|
||||
* @var DoliDB Database handler
|
||||
*/
|
||||
public $db;
|
||||
|
||||
/**
|
||||
* @var string Error message
|
||||
*/
|
||||
public $error = '';
|
||||
|
||||
/**
|
||||
* @var array Error messages
|
||||
*/
|
||||
public $errors = array();
|
||||
|
||||
/**
|
||||
* @var string Output message for cron log
|
||||
*/
|
||||
public $output = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the automatic import cron job
|
||||
* Called by Dolibarr's scheduled task system
|
||||
*
|
||||
* @return int 0 if OK, < 0 if error
|
||||
*/
|
||||
public function doAutoImport()
|
||||
{
|
||||
global $conf, $langs, $user;
|
||||
|
||||
$langs->load('bankimport@bankimport');
|
||||
|
||||
dol_syslog("BankImportCron::doAutoImport - Starting automatic import", LOG_INFO);
|
||||
|
||||
// Check if automatic import is enabled
|
||||
if (!getDolGlobalInt('BANKIMPORT_AUTO_ENABLED')) {
|
||||
$this->output = $langs->trans('AutoImportDisabled');
|
||||
dol_syslog("BankImportCron::doAutoImport - Auto import is disabled", LOG_INFO);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Initialize FinTS
|
||||
$fints = new BankImportFinTS($this->db);
|
||||
|
||||
if (!$fints->isConfigured()) {
|
||||
$this->error = $langs->trans('AutoImportNotConfigured');
|
||||
dol_syslog("BankImportCron::doAutoImport - FinTS not configured", LOG_WARNING);
|
||||
$this->setNotification('config_error');
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!$fints->isLibraryAvailable()) {
|
||||
$this->error = $langs->trans('FinTSLibraryNotFound');
|
||||
dol_syslog("BankImportCron::doAutoImport - Library not found", LOG_ERR);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check for stored session state (from previous successful TAN)
|
||||
$storedState = getDolGlobalString('BANKIMPORT_CRON_STATE');
|
||||
$sessionRestored = false;
|
||||
|
||||
try {
|
||||
// Try to restore previous session
|
||||
if (!empty($storedState)) {
|
||||
dol_syslog("BankImportCron::doAutoImport - Attempting to restore previous session (state length: ".strlen($storedState).")", LOG_INFO);
|
||||
$restoreResult = $fints->restore($storedState);
|
||||
if ($restoreResult < 0) {
|
||||
// Session expired, need fresh login
|
||||
dol_syslog("BankImportCron::doAutoImport - Session restore failed: ".$fints->error.", trying fresh login", LOG_WARNING);
|
||||
$storedState = '';
|
||||
// Clear stale session
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_STATE', $conf->entity);
|
||||
} else {
|
||||
dol_syslog("BankImportCron::doAutoImport - Session restored successfully", LOG_INFO);
|
||||
$sessionRestored = true;
|
||||
}
|
||||
} else {
|
||||
dol_syslog("BankImportCron::doAutoImport - No stored session found", LOG_INFO);
|
||||
}
|
||||
|
||||
// If no stored session or restore failed, try fresh login
|
||||
if (empty($storedState)) {
|
||||
dol_syslog("BankImportCron::doAutoImport - Attempting fresh login", LOG_INFO);
|
||||
$loginResult = $fints->login();
|
||||
|
||||
if ($loginResult < 0) {
|
||||
$this->error = $langs->trans('LoginFailed').': '.$fints->error;
|
||||
dol_syslog("BankImportCron::doAutoImport - Login failed: ".$fints->error, LOG_ERR);
|
||||
$this->setNotification('login_error');
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($loginResult == 0) {
|
||||
// TAN required - can't proceed automatically
|
||||
$this->output = $langs->trans('TANRequired');
|
||||
dol_syslog("BankImportCron::doAutoImport - TAN required for login, cannot proceed automatically", LOG_WARNING);
|
||||
$this->setNotification('tan_required');
|
||||
|
||||
// Store the state so user can complete TAN manually
|
||||
$state = $fints->persist();
|
||||
if (!empty($state)) {
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $state, 'chaine', 0, '', $conf->entity);
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', serialize($fints->getPendingAction()), 'chaine', 0, '', $conf->entity);
|
||||
}
|
||||
return 0; // Not an error, just can't proceed
|
||||
}
|
||||
|
||||
dol_syslog("BankImportCron::doAutoImport - Fresh login successful", LOG_INFO);
|
||||
}
|
||||
|
||||
// Login successful or session restored - fetch statements
|
||||
$daysToFetch = getDolGlobalInt('BANKIMPORT_AUTO_DAYS') ?: 30;
|
||||
$dateFrom = strtotime("-{$daysToFetch} days");
|
||||
$dateTo = time();
|
||||
|
||||
dol_syslog("BankImportCron::doAutoImport - Fetching statements from ".date('Y-m-d', $dateFrom)." to ".date('Y-m-d', $dateTo)." ({$daysToFetch} days)", LOG_INFO);
|
||||
|
||||
$result = $fints->fetchStatements($dateFrom, $dateTo);
|
||||
|
||||
// Log what we got back
|
||||
if (is_array($result)) {
|
||||
$txCount = count($result['transactions'] ?? array());
|
||||
$hasBalance = !empty($result['balance']);
|
||||
$isPartial = !empty($result['partial']);
|
||||
dol_syslog("BankImportCron::doAutoImport - fetchStatements returned array: transactions={$txCount}, hasBalance=".($hasBalance?'yes':'no').", partial=".($isPartial?'yes':'no'), LOG_INFO);
|
||||
} else {
|
||||
dol_syslog("BankImportCron::doAutoImport - fetchStatements returned: ".var_export($result, true), LOG_INFO);
|
||||
}
|
||||
|
||||
if ($result === 0) {
|
||||
// TAN required for statements
|
||||
$this->output = $langs->trans('TANRequired');
|
||||
dol_syslog("BankImportCron::doAutoImport - TAN required for statements", LOG_WARNING);
|
||||
$this->setNotification('tan_required');
|
||||
|
||||
// Store state for manual completion
|
||||
$state = $fints->persist();
|
||||
if (!empty($state)) {
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $state, 'chaine', 0, '', $conf->entity);
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', serialize($fints->getPendingAction()), 'chaine', 0, '', $conf->entity);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($result < 0) {
|
||||
$this->error = $langs->trans('FetchFailed').': '.$fints->error;
|
||||
dol_syslog("BankImportCron::doAutoImport - Fetch failed: ".$fints->error, LOG_ERR);
|
||||
$this->setNotification('fetch_error');
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Success - import transactions
|
||||
$transactions = $result['transactions'] ?? array();
|
||||
$fetchedCount = count($transactions);
|
||||
$importedCount = 0;
|
||||
$skippedCount = 0;
|
||||
|
||||
dol_syslog("BankImportCron::doAutoImport - Bank returned {$fetchedCount} transactions", LOG_INFO);
|
||||
|
||||
// If restored session returned 0 transactions, the session might be stale
|
||||
// Try fresh login as fallback
|
||||
if ($fetchedCount == 0 && $sessionRestored) {
|
||||
dol_syslog("BankImportCron::doAutoImport - Restored session returned 0 transactions, clearing stale session", LOG_WARNING);
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_STATE', $conf->entity);
|
||||
|
||||
// Note: We don't retry here because a fresh login would require TAN
|
||||
// Just mark that the session was stale
|
||||
$this->output = $langs->trans('AutoImportNoTransactions').' (Session abgelaufen - nächster Lauf erfordert TAN)';
|
||||
|
||||
$this->setNotification('session_expired');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!empty($transactions)) {
|
||||
// Get a system user for the import
|
||||
$importUser = new User($this->db);
|
||||
$importUser->fetch(1); // Admin user
|
||||
|
||||
$iban = $fints->getIban();
|
||||
|
||||
// Use the importFromFinTS method for correct field mapping
|
||||
$transImporter = new BankImportTransaction($this->db);
|
||||
$importResult = $transImporter->importFromFinTS($transactions, $iban, $importUser);
|
||||
|
||||
$importedCount = $importResult['imported'] ?? 0;
|
||||
$skippedCount = $importResult['skipped'] ?? 0;
|
||||
|
||||
dol_syslog("BankImportCron::doAutoImport - Import result: imported={$importedCount}, skipped={$skippedCount}", LOG_INFO);
|
||||
}
|
||||
|
||||
// Update last fetch info
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_LAST_FETCH', time(), 'chaine', 0, '', $conf->entity);
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_LAST_FETCH_COUNT', $importedCount, 'chaine', 0, '', $conf->entity);
|
||||
|
||||
// Store session for next run (might avoid TAN)
|
||||
$state = $fints->persist();
|
||||
if (!empty($state)) {
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_STATE', $state, 'chaine', 0, '', $conf->entity);
|
||||
}
|
||||
|
||||
// Clear any pending state
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity);
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity);
|
||||
|
||||
// Clear notification flag on success
|
||||
$this->clearNotification();
|
||||
|
||||
$fints->close();
|
||||
|
||||
if ($importedCount > 0) {
|
||||
$this->output = $langs->trans('AutoImportSuccess', $importedCount);
|
||||
} elseif ($skippedCount > 0) {
|
||||
$this->output = $langs->trans('AutoImportNoTransactions').' ('.$skippedCount.' bereits vorhanden)';
|
||||
} else {
|
||||
$this->output = $langs->trans('AutoImportNoTransactions');
|
||||
}
|
||||
|
||||
dol_syslog("BankImportCron::doAutoImport - Completed: imported={$importedCount}, skipped={$skippedCount}", LOG_INFO);
|
||||
|
||||
return 0;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->error = 'Exception: '.$e->getMessage();
|
||||
dol_syslog("BankImportCron::doAutoImport - Exception: ".$e->getMessage(), LOG_ERR);
|
||||
$this->setNotification('error');
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set notification flag for admin users
|
||||
*
|
||||
* @param string $type Notification type (tan_required, error, etc.)
|
||||
* @return void
|
||||
*/
|
||||
private function setNotification($type)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_NOTIFICATION', $type, 'chaine', 0, '', $conf->entity);
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_NOTIFICATION_DATE', time(), 'chaine', 0, '', $conf->entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear notification flag
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function clearNotification()
|
||||
{
|
||||
global $conf;
|
||||
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_NOTIFICATION', $conf->entity);
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_NOTIFICATION_DATE', $conf->entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's a pending notification
|
||||
*
|
||||
* @return array|null Notification info or null
|
||||
*/
|
||||
public static function getNotification()
|
||||
{
|
||||
$type = getDolGlobalString('BANKIMPORT_NOTIFICATION');
|
||||
$date = getDolGlobalInt('BANKIMPORT_NOTIFICATION_DATE');
|
||||
|
||||
if (empty($type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => $type,
|
||||
'date' => $date
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a pending TAN action (called from web interface)
|
||||
*
|
||||
* @return int 1 if TAN confirmed and import done, 0 if still waiting, -1 if error
|
||||
*/
|
||||
public function resumePendingAction()
|
||||
{
|
||||
global $conf, $langs, $user;
|
||||
|
||||
$pendingState = getDolGlobalString('BANKIMPORT_CRON_PENDING_STATE');
|
||||
$pendingAction = getDolGlobalString('BANKIMPORT_CRON_PENDING_ACTION');
|
||||
|
||||
if (empty($pendingState) || empty($pendingAction)) {
|
||||
$this->error = 'No pending action';
|
||||
return -1;
|
||||
}
|
||||
|
||||
$fints = new BankImportFinTS($this->db);
|
||||
|
||||
$restoreResult = $fints->restore($pendingState);
|
||||
if ($restoreResult < 0) {
|
||||
$this->error = $fints->error;
|
||||
// Clear expired state
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity);
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity);
|
||||
return -1;
|
||||
}
|
||||
|
||||
$action = @unserialize($pendingAction);
|
||||
if ($action === false) {
|
||||
$this->error = 'Could not restore pending action';
|
||||
return -1;
|
||||
}
|
||||
|
||||
$fints->setPendingAction($action);
|
||||
|
||||
// Check if TAN was confirmed
|
||||
$checkResult = $fints->checkDecoupledTan();
|
||||
|
||||
if ($checkResult == 0) {
|
||||
// Still waiting
|
||||
// Update state
|
||||
$newState = $fints->persist();
|
||||
if (!empty($newState)) {
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $newState, 'chaine', 0, '', $conf->entity);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($checkResult < 0) {
|
||||
$this->error = $fints->error;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// TAN confirmed - now run the import
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity);
|
||||
dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity);
|
||||
|
||||
// Store the confirmed session and run import
|
||||
$state = $fints->persist();
|
||||
if (!empty($state)) {
|
||||
dolibarr_set_const($this->db, 'BANKIMPORT_CRON_STATE', $state, 'chaine', 0, '', $conf->entity);
|
||||
}
|
||||
|
||||
return $this->doAutoImport();
|
||||
}
|
||||
}
|
||||
1325
class/bankstatement.class.php
Executable file
1325
class/bankstatement.class.php
Executable file
File diff suppressed because it is too large
Load diff
2289
class/banktransaction.class.php
Executable file
2289
class/banktransaction.class.php
Executable file
File diff suppressed because it is too large
Load diff
1020
class/fints.class.php
Executable file
1020
class/fints.class.php
Executable file
File diff suppressed because it is too large
Load diff
16
composer.json
Executable file
16
composer.json
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "dolibarr/bankimport",
|
||||
"description": "Dolibarr module for importing bank statements via FinTS/HBCI",
|
||||
"type": "dolibarr-module",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"nemiah/php-fints": "^3.2"
|
||||
},
|
||||
"replace": {
|
||||
"psr/log": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": ["class/"]
|
||||
}
|
||||
}
|
||||
70
composer.lock
generated
Executable file
70
composer.lock
generated
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "cfc07b7e6c4a3dcfdcd6e754983b1a9b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "nemiah/php-fints",
|
||||
"version": "3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nemiah/phpFinTS.git",
|
||||
"reference": "08257e10229db2d4ca8c54ed7fec0f390b332519"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nemiah/phpFinTS/zipball/08257e10229db2d4ca8c54ed7fec0f390b332519",
|
||||
"reference": "08257e10229db2d4ca8c54ed7fec0f390b332519",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=8.0",
|
||||
"psr/log": "^1|^2|^3"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"php-mock/php-mock-phpunit": "^2.6",
|
||||
"phpunit/phpunit": "^9.5"
|
||||
},
|
||||
"suggest": {
|
||||
"abcaeffchen/sephpa": "1.*",
|
||||
"monolog/monolog": "Allow sending log messages to a variety of different handlers",
|
||||
"nemiah/php-sepa-xml": "dev-master"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Fhp": "lib/",
|
||||
"Tests\\Fhp": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHP Library for the protocols fints and hbci",
|
||||
"homepage": "https://github.com/nemiah/phpFinTS",
|
||||
"support": {
|
||||
"issues": "https://github.com/nemiah/phpFinTS/issues",
|
||||
"source": "https://github.com/nemiah/phpFinTS/tree/3.7"
|
||||
},
|
||||
"time": "2025-10-14T15:05:56+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
458
confirm.php
Executable file
458
confirm.php
Executable file
|
|
@ -0,0 +1,458 @@
|
|||
<?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 bankimport/confirm.php
|
||||
* \ingroup bankimport
|
||||
* \brief Payment confirmation page - match bank transactions to 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) {
|
||||
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';
|
||||
dol_include_once('/bankimport/class/banktransaction.class.php');
|
||||
dol_include_once('/bankimport/lib/bankimport.lib.php');
|
||||
|
||||
/**
|
||||
* @var Conf $conf
|
||||
* @var DoliDB $db
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
$langs->loadLangs(array("bankimport@bankimport", "banks", "bills"));
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('bankimport', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// Confirm single payment
|
||||
if ($action == 'confirmpayment' && !empty($bankAccountId)) {
|
||||
$transid = GETPOSTINT('transid');
|
||||
$matchtype = GETPOST('matchtype', 'alpha');
|
||||
$matchid = GETPOSTINT('matchid');
|
||||
|
||||
if ($transid > 0 && !empty($matchtype) && $matchid > 0) {
|
||||
$trans = new BankImportTransaction($db);
|
||||
if ($trans->fetch($transid) > 0) {
|
||||
if ($trans->status != BankImportTransaction::STATUS_NEW) {
|
||||
setEventMessages($langs->trans("TransactionAlreadyProcessed"), null, 'warnings');
|
||||
} else {
|
||||
$result = $trans->confirmPayment($user, $matchtype, $matchid, $bankAccountId);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans("PaymentCreatedSuccessfully", dol_escape_htmltag($trans->name), price(abs($trans->amount))), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($trans->error, $trans->errors, 'errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: ".$_SERVER["PHP_SELF"]."?token=".newToken());
|
||||
exit;
|
||||
}
|
||||
|
||||
// Confirm multiple invoices payment
|
||||
if ($action == 'confirmmulti' && !empty($bankAccountId)) {
|
||||
$transid = GETPOSTINT('transid');
|
||||
$invoiceIds = GETPOST('invoices', 'array');
|
||||
|
||||
if ($transid > 0 && !empty($invoiceIds)) {
|
||||
$trans = new BankImportTransaction($db);
|
||||
if ($trans->fetch($transid) > 0) {
|
||||
if ($trans->status != BankImportTransaction::STATUS_NEW) {
|
||||
setEventMessages($langs->trans("TransactionAlreadyProcessed"), null, 'warnings');
|
||||
} else {
|
||||
// Build invoices array with amounts
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
||||
$invoices = array();
|
||||
foreach ($invoiceIds as $invId) {
|
||||
$invId = (int) $invId;
|
||||
if ($invId > 0) {
|
||||
$invoice = new FactureFournisseur($db);
|
||||
if ($invoice->fetch($invId) > 0) {
|
||||
$alreadyPaid = $invoice->getSommePaiement();
|
||||
$creditnotes = $invoice->getSumCreditNotesUsed();
|
||||
$deposits = $invoice->getSumDepositsUsed();
|
||||
$remainToPay = price2num($invoice->total_ttc - $alreadyPaid - $creditnotes - $deposits, 'MT');
|
||||
if ($remainToPay > 0) {
|
||||
$invoices[] = array(
|
||||
'type' => 'facture_fourn',
|
||||
'id' => $invId,
|
||||
'ref' => $invoice->ref,
|
||||
'amount' => $remainToPay
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($invoices)) {
|
||||
$result = $trans->confirmMultiplePayment($user, $invoices, $bankAccountId);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans("PaymentCreatedSuccessfully", dol_escape_htmltag($trans->name), price(abs($trans->amount))).' ('.count($invoices).' '.$langs->trans("Invoices").')', null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($trans->error, $trans->errors, 'errors');
|
||||
}
|
||||
} else {
|
||||
setEventMessages($langs->trans("NoInvoicesSelected"), null, 'errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: ".$_SERVER["PHP_SELF"]."?token=".newToken());
|
||||
exit;
|
||||
}
|
||||
|
||||
// Confirm all high-score matches
|
||||
if ($action == 'confirmall' && !empty($bankAccountId)) {
|
||||
$transaction = new BankImportTransaction($db);
|
||||
$transactions = $transaction->fetchAll('date_trans', 'DESC', 0, 0, array('status' => BankImportTransaction::STATUS_NEW));
|
||||
|
||||
$created = 0;
|
||||
$failed = 0;
|
||||
|
||||
if (is_array($transactions)) {
|
||||
foreach ($transactions as $trans) {
|
||||
$matches = $trans->findMatches();
|
||||
if (!empty($matches) && $matches[0]['match_score'] >= 80) {
|
||||
$bestMatch = $matches[0];
|
||||
|
||||
// Handle multi-invoice matches
|
||||
if ($bestMatch['type'] == 'multi_facture_fourn' && !empty($bestMatch['invoices'])) {
|
||||
$invoices = array();
|
||||
foreach ($bestMatch['invoices'] as $inv) {
|
||||
$invoices[] = array(
|
||||
'type' => 'facture_fourn',
|
||||
'id' => $inv['id'],
|
||||
'ref' => $inv['ref'],
|
||||
'amount' => $inv['amount']
|
||||
);
|
||||
}
|
||||
$result = $trans->confirmMultiplePayment($user, $invoices, $bankAccountId);
|
||||
} else {
|
||||
$result = $trans->confirmPayment($user, $bestMatch['type'], $bestMatch['id'], $bankAccountId);
|
||||
}
|
||||
|
||||
if ($result > 0) {
|
||||
$created++;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($created > 0 || $failed > 0) {
|
||||
setEventMessages($langs->trans("PaymentsCreatedSummary", $created, $failed), null, $failed > 0 ? 'warnings' : 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("NoNewMatchesFound"), null, 'warnings');
|
||||
}
|
||||
|
||||
header("Location: ".$_SERVER["PHP_SELF"]."?token=".newToken());
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ignore transaction
|
||||
if ($action == 'ignore') {
|
||||
$transid = GETPOSTINT('transid');
|
||||
if ($transid > 0) {
|
||||
$trans = new BankImportTransaction($db);
|
||||
if ($trans->fetch($transid) > 0 && $trans->status == BankImportTransaction::STATUS_NEW) {
|
||||
$trans->setStatus(BankImportTransaction::STATUS_IGNORED, $user);
|
||||
setEventMessages($langs->trans("StatusUpdated"), null, 'mesgs');
|
||||
}
|
||||
}
|
||||
header("Location: ".$_SERVER["PHP_SELF"]."?token=".newToken());
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
$title = $langs->trans("PaymentConfirmation");
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-bankimport page-confirm');
|
||||
|
||||
print load_fiche_titre($title, '', 'bank');
|
||||
|
||||
// Check if bank account is configured
|
||||
if (empty($bankAccountId)) {
|
||||
print '<div class="warning">';
|
||||
print img_warning().' '.$langs->trans("ErrorNoBankAccountConfigured");
|
||||
print ' <a href="'.dol_buildpath('/bankimport/admin/setup.php', 1).'">'.$langs->trans("GoToSetup").'</a>';
|
||||
print '</div>';
|
||||
llxFooter();
|
||||
$db->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Description
|
||||
print '<div class="opacitymedium" style="margin-bottom: 15px;">'.$langs->trans("PaymentConfirmationDesc").'</div>';
|
||||
|
||||
// Fetch all new transactions and find matches
|
||||
$transaction = new BankImportTransaction($db);
|
||||
$transactions = $transaction->fetchAll('date_trans', 'DESC', 0, 0, array('status' => BankImportTransaction::STATUS_NEW));
|
||||
|
||||
$pendingMatches = array(); // transactions with matches
|
||||
$noMatches = array(); // transactions without matches
|
||||
|
||||
if (is_array($transactions)) {
|
||||
foreach ($transactions as $trans) {
|
||||
$matches = $trans->findMatches();
|
||||
if (!empty($matches)) {
|
||||
$pendingMatches[] = array('transaction' => $trans, 'matches' => $matches);
|
||||
} else {
|
||||
$noMatches[] = $trans;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm all button (if high-score matches exist)
|
||||
$highScoreCount = 0;
|
||||
foreach ($pendingMatches as $pm) {
|
||||
if ($pm['matches'][0]['match_score'] >= 80) {
|
||||
$highScoreCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($highScoreCount > 0) {
|
||||
print '<div class="tabsAction">';
|
||||
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=confirmall&token='.newToken().'">';
|
||||
print $langs->trans("ConfirmAllHighScore").' ('.$highScoreCount.')';
|
||||
print '</a>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Match reasons translation
|
||||
$reasonLabels = array(
|
||||
'ref' => $langs->trans("MatchByRef"),
|
||||
'ref_client' => $langs->trans("MatchByClientRef"),
|
||||
'ref_supplier' => $langs->trans("MatchBySupplierRef"),
|
||||
'amount' => $langs->trans("MatchByAmount"),
|
||||
'amount_close' => $langs->trans("MatchByAmountClose"),
|
||||
'name_exact' => $langs->trans("MatchByNameExact"),
|
||||
'name_similar' => $langs->trans("MatchByNameSimilar"),
|
||||
'iban' => $langs->trans("MatchByIBAN"),
|
||||
'multi_invoice' => $langs->trans("MatchByMultiInvoice")
|
||||
);
|
||||
|
||||
if (!empty($pendingMatches)) {
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("Date").'</th>';
|
||||
print '<th>'.$langs->trans("Counterparty").'</th>';
|
||||
print '<th class="right">'.$langs->trans("Amount").' ('.$langs->trans("Transaction").')</th>';
|
||||
print '<th style="text-align: center; width: 30px;"></th>';
|
||||
print '<th>'.$langs->trans("Invoice").'</th>';
|
||||
print '<th>'.$langs->trans("ThirdParty").'</th>';
|
||||
print '<th class="right">'.$langs->trans("Amount").' ('.$langs->trans("Invoice").')</th>';
|
||||
print '<th class="center">'.$langs->trans("Score").'</th>';
|
||||
print '<th>'.$langs->trans("MatchReason").'</th>';
|
||||
print '<th class="center">'.$langs->trans("Action").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
foreach ($pendingMatches as $pm) {
|
||||
$trans = $pm['transaction'];
|
||||
$bestMatch = $pm['matches'][0];
|
||||
$isMultiInvoice = ($bestMatch['type'] == 'multi_facture_fourn');
|
||||
|
||||
// Score color
|
||||
$scoreColor = $bestMatch['match_score'] >= 80 ? '#4caf50' : ($bestMatch['match_score'] >= 60 ? '#ff9800' : '#9e9e9e');
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Transaction date
|
||||
print '<td class="nowraponall">'.dol_print_date($trans->date_trans, 'day').'</td>';
|
||||
|
||||
// Counterparty name + description
|
||||
print '<td class="tdoverflowmax200">';
|
||||
print '<a href="'.dol_buildpath('/bankimport/card.php', 1).'?id='.$trans->id.'">'.dol_escape_htmltag(dol_trunc($trans->name, 30)).'</a>';
|
||||
if ($trans->description) {
|
||||
print '<br><span class="opacitymedium small">'.dol_escape_htmltag(dol_trunc($trans->description, 50)).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Transaction amount
|
||||
print '<td class="right nowraponall">';
|
||||
if ($trans->amount >= 0) {
|
||||
print '<span style="color: green; font-weight: bold;">+'.price($trans->amount, 0, $langs, 1, -1, 2, $trans->currency).'</span>';
|
||||
} else {
|
||||
print '<span style="color: red; font-weight: bold;">'.price($trans->amount, 0, $langs, 1, -1, 2, $trans->currency).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Arrow
|
||||
print '<td class="center" style="font-size: 1.3em;">↔</td>';
|
||||
|
||||
// Invoice reference(s)
|
||||
print '<td class="nowraponall">';
|
||||
if ($isMultiInvoice && !empty($bestMatch['invoices'])) {
|
||||
// Multi-invoice display
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
||||
print '<strong>'.count($bestMatch['invoices']).' '.$langs->trans("Invoices").':</strong><br>';
|
||||
foreach ($bestMatch['invoices'] as $invData) {
|
||||
$inv = new FactureFournisseur($db);
|
||||
$inv->fetch($invData['id']);
|
||||
print $inv->getNomUrl(1).' <span class="opacitymedium small">('.price($invData['amount'], 0, $langs, 1, -1, 2, 'EUR').')</span><br>';
|
||||
}
|
||||
} elseif ($bestMatch['type'] == 'facture') {
|
||||
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
|
||||
$inv = new Facture($db);
|
||||
$inv->fetch($bestMatch['id']);
|
||||
print $inv->getNomUrl(1);
|
||||
} else {
|
||||
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
||||
$inv = new FactureFournisseur($db);
|
||||
$inv->fetch($bestMatch['id']);
|
||||
print $inv->getNomUrl(1);
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Third party
|
||||
print '<td class="tdoverflowmax150">';
|
||||
if ($bestMatch['socid'] > 0) {
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
$soc = new Societe($db);
|
||||
$soc->fetch($bestMatch['socid']);
|
||||
print $soc->getNomUrl(1);
|
||||
} else {
|
||||
print dol_escape_htmltag($bestMatch['socname']);
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Invoice amount
|
||||
print '<td class="right nowraponall">';
|
||||
print price($bestMatch['amount'], 0, $langs, 1, -1, 2, 'EUR');
|
||||
if ($isMultiInvoice && isset($bestMatch['difference']) && abs($bestMatch['difference']) > 0.01) {
|
||||
$diffColor = $bestMatch['difference'] > 0 ? 'orange' : 'green';
|
||||
print '<br><span class="small" style="color: '.$diffColor.';">'.($bestMatch['difference'] > 0 ? '+' : '').price($bestMatch['difference'], 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Score
|
||||
print '<td class="center"><span style="color: '.$scoreColor.'; font-weight: bold; font-size: 1.1em;">'.$bestMatch['match_score'].'%</span></td>';
|
||||
|
||||
// Match reasons
|
||||
print '<td>';
|
||||
if (!empty($bestMatch['match_reasons'])) {
|
||||
foreach ($bestMatch['match_reasons'] as $reason) {
|
||||
$label = $reasonLabels[$reason] ?? $reason;
|
||||
print '<span class="badge badge-secondary">'.$label.'</span> ';
|
||||
}
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Actions
|
||||
print '<td class="center nowraponall">';
|
||||
|
||||
if ($isMultiInvoice && !empty($bestMatch['invoices'])) {
|
||||
// Multi-invoice: Form with checkboxes for selection
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="confirmmulti">';
|
||||
print '<input type="hidden" name="transid" value="'.$trans->id.'">';
|
||||
|
||||
print '<div style="text-align: left; margin-bottom: 5px;">';
|
||||
foreach ($bestMatch['invoices'] as $invData) {
|
||||
print '<label style="display: block; margin: 2px 0;">';
|
||||
print '<input type="checkbox" name="invoices[]" value="'.$invData['id'].'" checked> ';
|
||||
print dol_escape_htmltag($invData['ref_supplier'] ?: $invData['ref']).' ('.price($invData['amount'], 0, $langs, 1, -1, 2, 'EUR').')';
|
||||
print '</label>';
|
||||
}
|
||||
print '</div>';
|
||||
|
||||
print '<button type="submit" class="butActionSmall">'.$langs->trans("ConfirmPayment").'</button>';
|
||||
print '</form>';
|
||||
} else {
|
||||
// Single invoice: Confirm payment button
|
||||
print '<a class="butActionSmall" href="'.$_SERVER["PHP_SELF"].'?action=confirmpayment&transid='.$trans->id.'&matchtype='.urlencode($bestMatch['type']).'&matchid='.$bestMatch['id'].'&token='.newToken().'">';
|
||||
print $langs->trans("ConfirmPayment");
|
||||
print '</a>';
|
||||
}
|
||||
|
||||
print '<br>';
|
||||
// Ignore button
|
||||
print '<a class="butActionSmall button-cancel" style="margin-top: 3px;" href="'.$_SERVER["PHP_SELF"].'?action=ignore&transid='.$trans->id.'&token='.newToken().'">';
|
||||
print $langs->trans("SetAsIgnored");
|
||||
print '</a>';
|
||||
|
||||
// Show alternatives if multiple matches
|
||||
if (count($pm['matches']) > 1) {
|
||||
print '<br><a class="small opacitymedium" style="margin-top: 3px;" href="'.dol_buildpath('/bankimport/card.php', 1).'?id='.$trans->id.'&action=findmatches&token='.newToken().'">';
|
||||
print '+'.($count = count($pm['matches']) - 1).' '.$langs->trans("Alternatives");
|
||||
print '</a>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
} else {
|
||||
print '<div class="opacitymedium" style="padding: 20px; text-align: center;">';
|
||||
print $langs->trans("NoNewMatchesFound");
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Show unmatched transactions count
|
||||
if (!empty($noMatches)) {
|
||||
print '<br>';
|
||||
print '<div class="info">';
|
||||
print img_picto('', 'info', 'class="pictofixedwidth"');
|
||||
print $langs->trans("UnmatchedTransactions", count($noMatches));
|
||||
print ' <a href="'.dol_buildpath('/bankimport/list.php', 1).'?search_status=0">'.$langs->trans("ShowAll").'</a>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
167
core/boxes/box_bankimport_pending.php
Executable file
167
core/boxes/box_bankimport_pending.php
Executable file
|
|
@ -0,0 +1,167 @@
|
|||
<?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.
|
||||
*/
|
||||
|
||||
include_once DOL_DOCUMENT_ROOT.'/core/boxes/modules_boxes.php';
|
||||
|
||||
/**
|
||||
* Dashboard widget showing pending bank transaction matches
|
||||
*/
|
||||
class box_bankimport_pending extends ModeleBoxes
|
||||
{
|
||||
public $boxcode = "bankimport_pending";
|
||||
public $boximg = "fa-money-check-alt";
|
||||
public $boxlabel = "BoxBankImportPending";
|
||||
public $depends = array("bankimport");
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
* @param string $param More parameters
|
||||
*/
|
||||
public function __construct($db, $param = '')
|
||||
{
|
||||
global $user;
|
||||
$this->db = $db;
|
||||
$this->hidden = !$user->hasRight('bankimport', 'read');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data into info_box_contents array to show on dashboard
|
||||
*
|
||||
* @param int $max Maximum number of records to load
|
||||
* @return void
|
||||
*/
|
||||
public function loadBox($max = 5)
|
||||
{
|
||||
global $user, $langs, $conf;
|
||||
|
||||
$langs->loadLangs(array("bankimport@bankimport", "banks"));
|
||||
|
||||
$this->max = $max;
|
||||
|
||||
// Box header
|
||||
$this->info_box_head = array(
|
||||
'text' => $langs->trans("BoxBankImportPending"),
|
||||
'sublink' => dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport',
|
||||
'subtext' => $langs->trans("ReviewAndConfirm"),
|
||||
'subpicto' => 'payment',
|
||||
);
|
||||
|
||||
$line = 0;
|
||||
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
|
||||
if (empty($bankAccountId)) {
|
||||
// No bank account configured
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="center" colspan="4"',
|
||||
'text' => img_warning().' '.$langs->trans("NoBankAccountConfigured"),
|
||||
'url' => dol_buildpath('/bankimport/admin/setup.php', 1),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Count new (unmatched) transactions
|
||||
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
||||
$sql .= " WHERE entity IN (".getEntity('banktransaction').")";
|
||||
$sql .= " AND status = 0";
|
||||
$resql = $this->db->query($sql);
|
||||
$newCount = 0;
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$newCount = (int) $obj->cnt;
|
||||
}
|
||||
|
||||
if ($newCount > 0) {
|
||||
// Summary line: X transactions pending
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="left" colspan="3"',
|
||||
'text' => '<strong>'.$langs->trans("PendingPaymentMatches", $newCount).'</strong>',
|
||||
'asis' => 1,
|
||||
);
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="right"',
|
||||
'text' => '<a class="butActionSmall" href="'.dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport">'.$langs->trans("ReviewAndConfirm").'</a>',
|
||||
'asis' => 1,
|
||||
);
|
||||
$line++;
|
||||
|
||||
// Show last few unmatched transactions
|
||||
$sql2 = "SELECT t.rowid, t.date_trans, t.name, t.amount, t.currency";
|
||||
$sql2 .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction as t";
|
||||
$sql2 .= " WHERE t.entity IN (".getEntity('banktransaction').")";
|
||||
$sql2 .= " AND t.status = 0 AND t.amount > 0";
|
||||
$sql2 .= " ORDER BY t.date_trans DESC";
|
||||
$sql2 .= $this->db->plimit($max, 0);
|
||||
|
||||
$resql2 = $this->db->query($sql2);
|
||||
if ($resql2) {
|
||||
$num = $this->db->num_rows($resql2);
|
||||
$i = 0;
|
||||
while ($i < $num && $line < $max + 1) {
|
||||
$obj2 = $this->db->fetch_object($resql2);
|
||||
if (!$obj2) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Date
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="nowraponall"',
|
||||
'text' => dol_print_date($this->db->jdate($obj2->date_trans), 'day'),
|
||||
);
|
||||
|
||||
// Name
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="tdoverflowmax150"',
|
||||
'text' => dol_trunc($obj2->name, 28),
|
||||
'url' => dol_buildpath('/bankimport/card.php', 1).'?id='.$obj2->rowid.'&mainmenu=bank&leftmenu=bankimport',
|
||||
);
|
||||
|
||||
// Amount
|
||||
$amountColor = $obj2->amount >= 0 ? 'color: green;' : 'color: red;';
|
||||
$amountPrefix = $obj2->amount >= 0 ? '+' : '';
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="right nowraponall"',
|
||||
'text' => '<span style="'.$amountColor.'">'.$amountPrefix.price($obj2->amount, 0, $langs, 1, -1, 2, $obj2->currency).'</span>',
|
||||
'asis' => 1,
|
||||
);
|
||||
|
||||
// Status badge
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="right"',
|
||||
'text' => '<span class="badge badge-status4 badge-status">'.$langs->trans("New").'</span>',
|
||||
'asis' => 1,
|
||||
);
|
||||
|
||||
$line++;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No pending transactions
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'td' => 'class="center opacitymedium" colspan="4"',
|
||||
'text' => $langs->trans("NoNewMatchesFound"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the box
|
||||
*
|
||||
* @param array|null $head Optional head array
|
||||
* @param array|null $contents Optional contents array
|
||||
* @param int $nooutput No print, return output
|
||||
* @return string
|
||||
*/
|
||||
public function showBox($head = null, $contents = null, $nooutput = 0)
|
||||
{
|
||||
return parent::showBox($this->info_box_head, $this->info_box_contents, $nooutput);
|
||||
}
|
||||
}
|
||||
552
core/modules/modBankImport.class.php
Executable file
552
core/modules/modBankImport.class.php
Executable file
|
|
@ -0,0 +1,552 @@
|
|||
<?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 bankimport Module BankImport
|
||||
* \brief BankImport module descriptor.
|
||||
*
|
||||
* \file htdocs/bankimport/core/modules/modBankImport.class.php
|
||||
* \ingroup bankimport
|
||||
* \brief Description and activation file for module BankImport
|
||||
*/
|
||||
include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php';
|
||||
|
||||
|
||||
/**
|
||||
* Description and activation class for module BankImport
|
||||
*/
|
||||
class modBankImport 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 = 500021; // 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 = 'bankimport';
|
||||
|
||||
// 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 'ModuleBankImportName' not found (BankImport is name of module).
|
||||
$this->name = preg_replace('/^mod/i', '', get_class($this));
|
||||
|
||||
// DESCRIPTION_FLAG
|
||||
// Module description, used if translation string 'ModuleBankImportDesc' not found (BankImport is name of module).
|
||||
$this->description = "BankImportDescription";
|
||||
// Used only if file README.md and README-LL.md not found.
|
||||
$this->descriptionlong = "BankImportDescription";
|
||||
|
||||
// Author
|
||||
$this->editor_name = 'Alles Watt laeuft';
|
||||
$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@bankimport'
|
||||
|
||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||
$this->version = '1.7';
|
||||
// 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 BANKIMPORT 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-money-check-alt';
|
||||
|
||||
// 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(
|
||||
// '/bankimport/css/bankimport.css.php',
|
||||
),
|
||||
// Set this to relative path of js file if module must load a js on all pages
|
||||
'js' => array(
|
||||
'/bankimport/js/bankimport_notify.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.
|
||||
// Example: this->dirs = array("/bankimport/temp","/bankimport/subdir");
|
||||
$this->dirs = array("/bankimport/temp");
|
||||
|
||||
// Config pages. Put here list of php page, stored into bankimport/admin directory, to use to setup module.
|
||||
$this->config_page_url = array("setup.php@bankimport");
|
||||
|
||||
// Dependencies
|
||||
// A condition to hide module
|
||||
$this->hidden = getDolGlobalInt('MODULE_BANKIMPORT_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("bankimport@bankimport");
|
||||
|
||||
// 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'=>'BankImportWasAutomaticallyActivatedBecauseOfYourCountryChoice');
|
||||
//$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('BANKIMPORT_MYNEWCONST1', 'chaine', 'myvalue', 'This is a constant to add', 1),
|
||||
// 2 => array('BANKIMPORT_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("bankimport")) {
|
||||
$conf->bankimport = new stdClass();
|
||||
$conf->bankimport->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@bankimport:$user->hasRight(\'bankimport\', \'read\'):/bankimport/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@bankimport:$user->hasRight(\'othermodule\', \'read\'):/bankimport/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' => 'bankimport@bankimport',
|
||||
// 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('bankimport'), isModEnabled('bankimport'), isModEnabled('bankimport')),
|
||||
// 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 bankimport/core/boxes that contains a class to show a widget.
|
||||
/* BEGIN MODULEBUILDER WIDGETS */
|
||||
$this->boxes = array(
|
||||
0 => array(
|
||||
'file' => 'box_bankimport_pending.php@bankimport',
|
||||
'note' => 'Pending bank transaction matches',
|
||||
'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' => 'BankImportAutoFetch',
|
||||
'jobtype' => 'method',
|
||||
'class' => '/bankimport/class/bankimportcron.class.php',
|
||||
'objectname' => 'BankImportCron',
|
||||
'method' => 'doAutoImport',
|
||||
'parameters' => '',
|
||||
'comment' => 'Automatic bank statement import via FinTS',
|
||||
'frequency' => 1,
|
||||
'unitfrequency' => 86400, // Daily
|
||||
'status' => 0, // Disabled by default
|
||||
'test' => 'isModEnabled("bankimport") && getDolGlobalInt("BANKIMPORT_AUTO_ENABLED")',
|
||||
'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("bankimport")', 'priority'=>50),
|
||||
// 1=>array('label'=>'My label', 'jobtype'=>'command', 'command'=>'', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>1, 'unitfrequency'=>3600*24, 'status'=>0, 'test'=>'isModEnabled("bankimport")', 'priority'=>50)
|
||||
// );
|
||||
|
||||
// Permissions provided by this module
|
||||
$this->rights = array();
|
||||
$r = 0;
|
||||
|
||||
// $user->hasRight('bankimport', 'read')
|
||||
$this->rights[$r][0] = $this->numero . '01';
|
||||
$this->rights[$r][1] = 'PermBankImportRead';
|
||||
$this->rights[$r][2] = 'r';
|
||||
$this->rights[$r][3] = 1; // Default enabled
|
||||
$this->rights[$r][4] = 'read';
|
||||
$r++;
|
||||
|
||||
// $user->hasRight('bankimport', 'write')
|
||||
$this->rights[$r][0] = $this->numero . '02';
|
||||
$this->rights[$r][1] = 'PermBankImportWrite';
|
||||
$this->rights[$r][2] = 'w';
|
||||
$this->rights[$r][3] = 0;
|
||||
$this->rights[$r][4] = 'write';
|
||||
$r++;
|
||||
|
||||
// $user->hasRight('bankimport', 'delete')
|
||||
$this->rights[$r][0] = $this->numero . '03';
|
||||
$this->rights[$r][1] = 'PermBankImportDelete';
|
||||
$this->rights[$r][2] = 'd';
|
||||
$this->rights[$r][3] = 0;
|
||||
$this->rights[$r][4] = 'delete';
|
||||
$r++;
|
||||
|
||||
|
||||
// Main menu entries to add
|
||||
$this->menu = array();
|
||||
$r = 0;
|
||||
|
||||
// Left menu entries under "Banken und Kasse" (mainmenu=bank)
|
||||
$r = 0;
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=bank',
|
||||
'type' => 'left',
|
||||
'titre' => 'BankImportMenu',
|
||||
'prefix' => img_picto('', 'download', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'bank',
|
||||
'leftmenu' => 'bankimport',
|
||||
'url' => '/bankimport/bankimportindex.php?mainmenu=bank&leftmenu=bankimport',
|
||||
'langs' => 'bankimport@bankimport',
|
||||
'position' => 200,
|
||||
'enabled' => 'isModEnabled("bankimport")',
|
||||
'perms' => '$user->hasRight("bankimport", "read")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
|
||||
'type' => 'left',
|
||||
'titre' => 'BankStatements',
|
||||
'prefix' => img_picto('', 'bank_account', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'bank',
|
||||
'leftmenu' => 'bankimport_statements',
|
||||
'url' => '/bankimport/statements.php?mainmenu=bank&leftmenu=bankimport',
|
||||
'langs' => 'bankimport@bankimport',
|
||||
'position' => 201,
|
||||
'enabled' => 'isModEnabled("bankimport")',
|
||||
'perms' => '$user->hasRight("bankimport", "write")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
|
||||
'type' => 'left',
|
||||
'titre' => 'TransactionList',
|
||||
'prefix' => img_picto('', 'list', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'bank',
|
||||
'leftmenu' => 'bankimport_transactions',
|
||||
'url' => '/bankimport/list.php?mainmenu=bank&leftmenu=bankimport',
|
||||
'langs' => 'bankimport@bankimport',
|
||||
'position' => 202,
|
||||
'enabled' => 'isModEnabled("bankimport")',
|
||||
'perms' => '$user->hasRight("bankimport", "read")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
|
||||
'type' => 'left',
|
||||
'titre' => 'PaymentConfirmation',
|
||||
'prefix' => img_picto('', 'payment', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'bank',
|
||||
'leftmenu' => 'bankimport_confirm',
|
||||
'url' => '/bankimport/confirm.php?mainmenu=bank&leftmenu=bankimport',
|
||||
'langs' => 'bankimport@bankimport',
|
||||
'position' => 203,
|
||||
'enabled' => 'isModEnabled("bankimport")',
|
||||
'perms' => '$user->hasRight("bankimport", "write")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
$this->menu[$r++] = array(
|
||||
'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
|
||||
'type' => 'left',
|
||||
'titre' => 'PDFStatements',
|
||||
'prefix' => img_picto('', 'pdf', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||
'mainmenu' => 'bank',
|
||||
'leftmenu' => 'bankimport_pdfstatements',
|
||||
'url' => '/bankimport/pdfstatements.php?mainmenu=bank&leftmenu=bankimport',
|
||||
'langs' => 'bankimport@bankimport',
|
||||
'position' => 204,
|
||||
'enabled' => 'isModEnabled("bankimport")',
|
||||
'perms' => '$user->hasRight("bankimport", "read")',
|
||||
'target' => '',
|
||||
'user' => 2,
|
||||
);
|
||||
|
||||
|
||||
// Exports profiles provided by this module
|
||||
$r = 0;
|
||||
/* BEGIN MODULEBUILDER EXPORT MYOBJECT */
|
||||
/*
|
||||
$langs->load("bankimport@bankimport");
|
||||
$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='/bankimport/class/myobject.class.php'; $keyforelement='myobject@bankimport';
|
||||
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='/bankimport/class/myobject.class.php'; $keyforelement='myobjectline@bankimport'; $keyforalias='tl';
|
||||
//include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php';
|
||||
$keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@bankimport';
|
||||
include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php';
|
||||
//$keyforselect='myobjectline'; $keyforaliasextra='extraline'; $keyforelement='myobjectline@bankimport';
|
||||
//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().'bankimport_myobject as t';
|
||||
//$this->export_sql_end[$r] .=' LEFT JOIN '.$this->db->prefix().'bankimport_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("bankimport@bankimport");
|
||||
$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().'bankimport_myobject', 'extra' => $this->db->prefix().'bankimport_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='/bankimport/class/myobject.class.php'; $keyforelement='myobject@bankimport';
|
||||
include DOL_DOCUMENT_ROOT.'/core/commonfieldsinimport.inc.php';
|
||||
$import_extrafield_sample = array();
|
||||
$keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@bankimport';
|
||||
include DOL_DOCUMENT_ROOT.'/core/extrafieldsinimport.inc.php';
|
||||
$this->import_fieldshidden_array[$r] = array('extra.fk_object' => 'lastrowid-'.$this->db->prefix().'bankimport_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('BANKIMPORT_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('BANKIMPORT_MYOBJECT_ADDON')),
|
||||
'path'=>"/core/modules/bankimport/".(!getDolGlobalString('BANKIMPORT_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('BANKIMPORT_MYOBJECT_ADDON')).'.php',
|
||||
'classobject'=>'MyObject',
|
||||
'pathobject'=>'/bankimport/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/', 'bankimport');
|
||||
$result = $this->_load_tables('/bankimport/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);
|
||||
//$result0=$extrafields->addExtraField('bankimport_separator1', "Separator 1", 'separator', 1, 0, 'thirdparty', 0, 0, '', array('options'=>array(1=>1)), 1, '', 1, 0, '', '', 'bankimport@bankimport', 'isModEnabled("bankimport")');
|
||||
//$result1=$extrafields->addExtraField('bankimport_myattr1', "New Attr 1 label", 'boolean', 1, 3, 'thirdparty', 0, 0, '', '', 1, '', -1, 0, '', '', 'bankimport@bankimport', 'isModEnabled("bankimport")');
|
||||
//$result2=$extrafields->addExtraField('bankimport_myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project', 0, 0, '', '', 1, '', -1, 0, '', '', 'bankimport@bankimport', 'isModEnabled("bankimport")');
|
||||
//$result3=$extrafields->addExtraField('bankimport_myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', -1, 0, '', '', 'bankimport@bankimport', 'isModEnabled("bankimport")');
|
||||
//$result4=$extrafields->addExtraField('bankimport_myattr4', "New Attr 4 label", 'select', 1, 3, 'thirdparty', 0, 1, '', array('options'=>array('code1'=>'Val1','code2'=>'Val2','code3'=>'Val3')), 1,'', -1, 0, '', '', 'bankimport@bankimport', 'isModEnabled("bankimport")');
|
||||
//$result5=$extrafields->addExtraField('bankimport_myattr5', "New Attr 5 label", 'text', 1, 10, 'user', 0, 0, '', '', 1, '', -1, 0, '', '', 'bankimport@bankimport', 'isModEnabled("bankimport")');
|
||||
|
||||
// Permissions
|
||||
$this->remove($options);
|
||||
|
||||
$sql = array();
|
||||
|
||||
// Document templates
|
||||
$moduledir = dol_sanitizeFileName('bankimport');
|
||||
$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 'bankimport.png@bankimport', you can put into this
|
||||
directory a .png file called *object_bankimport.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@bankimport', then you can put into this
|
||||
directory a .png file called *object_myobject.png* (16x16 or 32x32 pixels)
|
||||
|
||||
174
js/bankimport_notify.js.php
Executable file
174
js/bankimport_notify.js.php
Executable file
|
|
@ -0,0 +1,174 @@
|
|||
<?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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* JavaScript for browser push notifications about incoming payments
|
||||
* Loaded on every Dolibarr page via module_parts['js']
|
||||
*/
|
||||
|
||||
// Define MIME type
|
||||
if (!defined('NOTOKENRENEWAL')) {
|
||||
define('NOTOKENRENEWAL', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREMENU')) {
|
||||
define('NOREQUIREMENU', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREHTML')) {
|
||||
define('NOREQUIREHTML', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREAJAX')) {
|
||||
define('NOREQUIREAJAX', '1');
|
||||
}
|
||||
|
||||
$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";
|
||||
}
|
||||
|
||||
header('Content-Type: application/javascript; charset=UTF-8');
|
||||
header('Cache-Control: max-age=3600');
|
||||
|
||||
if (!isModEnabled('bankimport') || empty($user->id) || !$user->hasRight('bankimport', 'read')) {
|
||||
echo '/* bankimport: no access */';
|
||||
exit;
|
||||
}
|
||||
|
||||
$checkUrl = dol_buildpath('/bankimport/ajax/checkpending.php', 1);
|
||||
$confirmUrl = dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport';
|
||||
$checkInterval = 5 * 60 * 1000; // 5 Minuten
|
||||
?>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var STORAGE_KEY = 'bankimport_last_pending';
|
||||
var CHECK_URL = <?php echo json_encode($checkUrl); ?>;
|
||||
var CONFIRM_URL = <?php echo json_encode($confirmUrl); ?>;
|
||||
var CHECK_INTERVAL = <?php echo $checkInterval; ?>;
|
||||
|
||||
// Erst nach Seitenload starten
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
function init() {
|
||||
// Berechtigung anfragen beim ersten Mal
|
||||
if ('Notification' in window && Notification.permission === 'default') {
|
||||
// Dezent um Berechtigung bitten - nicht sofort, sondern nach 10 Sekunden
|
||||
setTimeout(function() {
|
||||
Notification.requestPermission();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Sofort prüfen
|
||||
checkPending();
|
||||
|
||||
// Regelmäßig prüfen
|
||||
setInterval(checkPending, CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
function checkPending() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', CHECK_URL, true);
|
||||
xhr.timeout = 15000;
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status !== 200) return;
|
||||
|
||||
try {
|
||||
var data = JSON.parse(xhr.responseText);
|
||||
} catch(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lastKnown = parseInt(localStorage.getItem(STORAGE_KEY) || '0', 10);
|
||||
var currentPending = data.pending || 0;
|
||||
var incoming = data.incoming || 0;
|
||||
var incomingTotal = data.incoming_total || 0;
|
||||
|
||||
// Neue Buchungen seit letztem Check?
|
||||
if (currentPending > lastKnown && currentPending > 0) {
|
||||
var newCount = currentPending - lastKnown;
|
||||
|
||||
if (incoming > 0) {
|
||||
showNotification(
|
||||
'Zahlungseingang',
|
||||
incoming + ' Zahlungseingang' + (incoming > 1 ? 'e' : '') + ' (' + formatAmount(incomingTotal) + ' €)\nBestätigung erforderlich',
|
||||
'incoming'
|
||||
);
|
||||
} else {
|
||||
showNotification(
|
||||
'Bankimport',
|
||||
newCount + ' neue Buchung' + (newCount > 1 ? 'en' : '') + ' warten auf Zuordnung',
|
||||
'pending'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Aktuellen Stand merken
|
||||
localStorage.setItem(STORAGE_KEY, currentPending.toString());
|
||||
};
|
||||
|
||||
xhr.onerror = function() {};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function showNotification(title, body, type) {
|
||||
if (!('Notification' in window)) return;
|
||||
if (Notification.permission !== 'granted') return;
|
||||
|
||||
var icon = type === 'incoming'
|
||||
? '/theme/common/mime/money.png'
|
||||
: '/theme/common/mime/doc.png';
|
||||
|
||||
var notification = new Notification(title, {
|
||||
body: body,
|
||||
icon: icon,
|
||||
tag: 'bankimport-' + type,
|
||||
requireInteraction: true
|
||||
});
|
||||
|
||||
notification.onclick = function() {
|
||||
window.focus();
|
||||
window.location.href = CONFIRM_URL;
|
||||
notification.close();
|
||||
};
|
||||
|
||||
// Nach 30 Sekunden automatisch schließen
|
||||
setTimeout(function() {
|
||||
notification.close();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function formatAmount(amount) {
|
||||
return parseFloat(amount).toFixed(2).replace('.', ',').replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||
}
|
||||
|
||||
})();
|
||||
356
langs/de_DE/bankimport.lang
Executable file
356
langs/de_DE/bankimport.lang
Executable file
|
|
@ -0,0 +1,356 @@
|
|||
# Übersetzungsdatei - Deutsch
|
||||
|
||||
#
|
||||
# Allgemein
|
||||
#
|
||||
|
||||
# Modulname 'ModuleBankImportName'
|
||||
ModuleBankImportName = Bankimport
|
||||
# Modulbeschreibung 'ModuleBankImportDesc'
|
||||
ModuleBankImportDesc = Kontoauszüge per FinTS/HBCI importieren
|
||||
|
||||
#
|
||||
# Admin-Seite
|
||||
#
|
||||
BankImportSetup = Bankimport Einstellungen
|
||||
Settings = Einstellungen
|
||||
BankImportSetupPage = Bankimport Einstellungen
|
||||
BankImportSetupDescription = Konfigurieren Sie die FinTS/HBCI-Verbindung zu Ihrer Bank für den automatischen Kontoauszugsimport.
|
||||
|
||||
# FinTS Konfiguration
|
||||
FinTSConfiguration = FinTS Bankverbindung
|
||||
FinTSServerURL = FinTS Server URL
|
||||
FinTSServerURLHelp = Für VR-Banken/Volksbanken: https://fints1.atruvia.de/cgi-bin/hbciservlet
|
||||
BLZ = Bankleitzahl (BLZ)
|
||||
BLZHelp = 8-stellige Bankleitzahl
|
||||
FinTSUsername = Benutzerkennung
|
||||
FinTSPIN = PIN
|
||||
PINAlreadySet = PIN ist konfiguriert
|
||||
PINHelp = Die PIN wird verschlüsselt gespeichert. Leer lassen um bestehende PIN zu behalten.
|
||||
AccountIBAN = Kontonummer / IBAN
|
||||
FinTSProductID = FinTS Produkt-ID
|
||||
FinTSProductIDHelp = Optional. Registrierungsnummer von der Deutschen Kreditwirtschaft.
|
||||
TestConnection = Verbindung testen
|
||||
ConnectionTestSuccessful = Verbindungstest erfolgreich!
|
||||
ConnectionTestFailed = Verbindungstest fehlgeschlagen
|
||||
FinTSClassNotFound = FinTS-Klasse nicht gefunden. Installation prüfen.
|
||||
FinTSNotConfigured = FinTS-Verbindung nicht konfiguriert
|
||||
FinTSLibraryNotFound = phpFinTS Bibliothek nicht gefunden. Bitte führen Sie composer install aus.
|
||||
GoToSetup = Zur Einrichtung
|
||||
FetchAvailableAccounts = Verfügbare Konten abrufen
|
||||
AccountsFound = %s Konten gefunden
|
||||
NoAccountsFound = Keine Konten gefunden
|
||||
TANRequiredForAccountList = TAN erforderlich - Kontoabruf ohne TAN nicht möglich
|
||||
AccountNumber = Kontonummer
|
||||
UseThisAccount = Dieses Konto verwenden
|
||||
Selected = Ausgewählt
|
||||
IBANUpdated = IBAN aktualisiert: %s
|
||||
|
||||
# VR-Bank Info
|
||||
VRBankInfo = VR-Bank / Volksbank Information
|
||||
VRBankInfoText = Für VR-Banken verwenden Sie den Atruvia-Server: https://fints1.atruvia.de/cgi-bin/hbciservlet<br>Das TAN-Verfahren SecureGo Plus (Decoupled TAN) wird unterstützt.
|
||||
|
||||
# Sicherheit
|
||||
SecurityInfo = Sicherheitshinweis
|
||||
SecurityInfoText = Die PIN wird verschlüsselt in der Datenbank gespeichert. Für zusätzliche Sicherheit sollte Ihre Dolibarr-Installation HTTPS verwenden.
|
||||
|
||||
#
|
||||
# Kontoauszüge Seite
|
||||
#
|
||||
BankStatements = Kontoauszüge
|
||||
FetchStatements = Kontoauszüge abrufen
|
||||
Transactions = Buchungen
|
||||
TransactionsFound = %s Buchungen gefunden
|
||||
NoTransactionsFound = Keine Buchungen im ausgewählten Zeitraum gefunden
|
||||
AccountBalance = Kontostand
|
||||
AsOf = Stand vom
|
||||
LoginFailed = Anmeldung fehlgeschlagen
|
||||
FetchFailed = Abruf fehlgeschlagen
|
||||
TANRequired = TAN erforderlich
|
||||
TANRequiredForStatements = TAN für Kontoauszüge erforderlich
|
||||
SecureGoPlusConfirmRequired = Bitte bestätigen Sie in Ihrer VR SecureGo plus App
|
||||
WaitingForSecureGoConfirmation = Warte auf Bestätigung in der SecureGo plus App...
|
||||
CheckSecureGoStatus = Status prüfen
|
||||
TANCheckFailed = TAN-Prüfung fehlgeschlagen
|
||||
|
||||
# Import
|
||||
ImportTransactions = Buchungen importieren
|
||||
ViewImportedTransactions = Importierte Buchungen anzeigen
|
||||
TransactionsImported = %s Buchungen importiert
|
||||
TransactionsSkipped = %s Buchungen übersprungen (bereits vorhanden)
|
||||
|
||||
#
|
||||
# Transaktionsliste
|
||||
#
|
||||
TransactionList = Buchungsliste
|
||||
TransactionRef = Referenz
|
||||
Counterparty = Gegenkonto
|
||||
AllStatuses = Alle Status
|
||||
StatusNew = Neu
|
||||
StatusMatched = Zugeordnet
|
||||
StatusReconciled = Abgeglichen
|
||||
StatusIgnored = Ignoriert
|
||||
SearchTransactions = Buchungen durchsuchen
|
||||
NoTransactionsInDatabase = Keine Buchungen in der Datenbank
|
||||
|
||||
# Status Labels
|
||||
NewTransaction = Neue Buchung
|
||||
TransactionMatched = Zugeordnet
|
||||
TransactionReconciled = Abgeglichen
|
||||
TransactionIgnored = Ignoriert
|
||||
New = Neu
|
||||
Matched = Zugeordnet
|
||||
Reconciled = Abgeglichen
|
||||
Ignored = Ignoriert
|
||||
|
||||
# Card page
|
||||
Transaction = Buchung
|
||||
DateValue = Valutadatum
|
||||
CounterpartyIBAN = Gegenkonto IBAN
|
||||
EndToEndId = End-to-End ID
|
||||
MandateId = Mandatsreferenz
|
||||
FindMatches = Zuordnungen suchen
|
||||
SetAsIgnored = Als ignoriert markieren
|
||||
Reopen = Wieder öffnen
|
||||
MatchesFound = %s mögliche Zuordnungen gefunden
|
||||
NoMatchesFound = Keine Zuordnungen gefunden
|
||||
Link = Verknüpfen
|
||||
LinkCreated = Verknüpfung erstellt
|
||||
StatusUpdated = Status aktualisiert
|
||||
BackToList = Zurück zur Liste
|
||||
Score = Übereinstimmung
|
||||
DateDue = Fälligkeitsdatum
|
||||
MatchReason = Grund
|
||||
|
||||
# Match Reasons
|
||||
MatchByRef = Rechnungsnr.
|
||||
MatchByClientRef = Kundenreferenz
|
||||
MatchBySupplierRef = Lieferantenreferenz
|
||||
MatchByAmount = Betrag
|
||||
MatchByAmountClose = Betrag ähnlich
|
||||
MatchByNameExact = Name
|
||||
MatchByNameSimilar = Name ähnlich
|
||||
MatchByIBAN = IBAN
|
||||
MatchByMultiInvoice = Sammelzahlung
|
||||
|
||||
# Automatic Import
|
||||
AutomaticImport = Automatischer Import
|
||||
EnableAutoImport = Automatischen Import aktivieren
|
||||
EnableAutoImportHelp = Buchungen werden automatisch per Cronjob abgerufen
|
||||
ImportFrequency = Abruf-Häufigkeit
|
||||
Daily = Täglich
|
||||
Weekly = Wöchentlich
|
||||
TwiceWeekly = Zweimal pro Woche
|
||||
DaysToFetch = Tage abrufen
|
||||
DaysToFetchHelp = Max. 60 Tage (Bank-Limit beachten)
|
||||
LastAutoFetch = Letzter automatischer Abruf
|
||||
NeverFetched = Noch nie abgerufen
|
||||
MoreThan7Days = Mehr als 7 Tage her
|
||||
CronJobDescription = Automatischer Import von Bankbuchungen via FinTS
|
||||
AutoImportSuccess = %s Buchungen automatisch importiert
|
||||
AutoImportNoTransactions = Keine neuen Buchungen gefunden
|
||||
AutoImportError = Automatischer Import fehlgeschlagen
|
||||
AutoImportDisabled = Automatischer Import ist deaktiviert
|
||||
AutoImportNotConfigured = FinTS ist nicht konfiguriert
|
||||
AutoImportTANRequired = Automatischer Import benötigt TAN-Bestätigung
|
||||
AutoImportTANRequiredDesc = Der automatische Import benötigt eine TAN-Bestätigung. Bitte bestätigen Sie in der SecureGo Plus App.
|
||||
AutoImportErrorDesc = Beim letzten automatischen Import am %s ist ein Fehler aufgetreten.
|
||||
TANConfirmedImportComplete = TAN bestätigt - Import abgeschlossen
|
||||
LastFetchWarning = Letzter Abruf am %s - Bitte regelmäßig neue Buchungen abrufen
|
||||
BankImportAutoFetch = Automatischer Bankimport
|
||||
|
||||
# Errors
|
||||
HistoricalDataLimit = Die Bank stellt historische Kontodaten nur für ca. 2-3 Monate bereit.
|
||||
SelectShorterPeriod = Bitte wählen Sie einen kürzeren Zeitraum.
|
||||
OlderDataNotAvailable = Ältere Daten vor %s sind bei der Bank nicht mehr verfügbar.
|
||||
PartialResultsReturned = Es konnten nur neuere Transaktionen abgerufen werden.
|
||||
|
||||
# PDF Statements
|
||||
PDFStatements = PDF-Kontoauszüge
|
||||
FetchPDFStatements = PDF-Kontoauszüge abrufen
|
||||
StatementNumber = Auszugsnummer
|
||||
StatementYear = Jahr
|
||||
StatementDate = Auszugsdatum
|
||||
DownloadPDF = PDF herunterladen
|
||||
NoPDFStatementsFound = Keine PDF-Kontoauszüge gefunden
|
||||
PDFStatementsImported = %s Kontoauszüge importiert
|
||||
StatementAlreadyExists = Kontoauszug bereits vorhanden
|
||||
DeleteStatement = Kontoauszug löschen
|
||||
ConfirmDeleteStatement = Möchten Sie den Kontoauszug %s wirklich löschen?
|
||||
StatementsInYear = Kontoauszüge im Jahr %s
|
||||
AllStatements = Alle Kontoauszüge
|
||||
OpeningBalance = Anfangssaldo
|
||||
ClosingBalance = Endsaldo
|
||||
StatementUploaded = Kontoauszug erfolgreich hochgeladen
|
||||
|
||||
#
|
||||
# Über-Seite
|
||||
#
|
||||
About = Über
|
||||
BankImportAbout = Über Bankimport
|
||||
BankImportAboutPage = Bankimport Info-Seite
|
||||
|
||||
#
|
||||
# Startseite / Dashboard
|
||||
#
|
||||
BankImportArea = Bankimport Übersicht
|
||||
LastImportedTransactions = Letzte importierte Buchungen
|
||||
LastPDFStatements = Letzte PDF-Kontoauszüge
|
||||
ShowAll = Alle anzeigen
|
||||
UploadNew = Neuen hochladen
|
||||
BankImportMenu = Bankimport
|
||||
|
||||
#
|
||||
# PDF Kontoauszüge Seite
|
||||
#
|
||||
PDFStatementsInfo = PDF-Kontoauszüge
|
||||
PDFStatementsInfoDesc = Hier können Sie Ihre PDF-Kontoauszüge hochladen, verwalten und einsehen. Die Auszüge werden sicher gespeichert und können jederzeit heruntergeladen werden.
|
||||
UploadPDFStatement = PDF-Kontoauszüge hochladen
|
||||
|
||||
#
|
||||
# Upload-Modus
|
||||
#
|
||||
UploadMode = Upload-Modus
|
||||
UploadModeAuto = Automatisch erkennen
|
||||
UploadModeManual = Manuelle Eingabe
|
||||
PdfAutoDetected = PDF-Metadaten automatisch erkannt
|
||||
ErrorNoFileUploaded = Keine Datei hochgeladen
|
||||
ErrorOnlyPDFAllowed = Nur PDF-Dateien sind erlaubt
|
||||
ErrorFileTooLarge = Datei ist zu groß (max. 10 MB)
|
||||
ErrorFailedToSaveFile = Datei konnte nicht gespeichert werden
|
||||
TransactionsLinked = %s Buchungen dem Kontoauszug zugeordnet
|
||||
StatementsUploaded = %s Kontoauszüge erfolgreich hochgeladen
|
||||
MultipleFilesHint = Sie können mehrere PDF-Dateien gleichzeitig auswählen (Strg+Klick oder Shift+Klick)
|
||||
|
||||
#
|
||||
# Admin - PDF Upload
|
||||
#
|
||||
PDFUploadSettings = PDF-Upload Einstellungen
|
||||
DefaultUploadMode = Standard Upload-Modus
|
||||
DefaultUploadModeHelp = Automatisch: Metadaten werden aus dem PDF extrahiert. Manuell: Alle Felder müssen von Hand ausgefüllt werden.
|
||||
ReminderEnabled = Erinnerung aktivieren
|
||||
ReminderEnabledHelp = Zeigt eine Warnung wenn Kontoauszüge nicht aktuell sind
|
||||
ReminderMonths = Erinnerung nach (Monate)
|
||||
ReminderMonthsHelp = Warnung anzeigen, wenn der letzte Kontoauszug älter als X Monate ist
|
||||
ReminderNoStatements = Es wurden noch keine Kontoauszüge hochgeladen. Bitte laden Sie Ihre Kontoauszüge hoch.
|
||||
ReminderOutdatedStatements = Der letzte Kontoauszug endet am %s (vor %s Monaten). Bitte laden Sie aktuelle Kontoauszüge hoch.
|
||||
|
||||
#
|
||||
# Kontoauszug-Verknüpfung
|
||||
#
|
||||
PDFStatement = PDF-Kontoauszug
|
||||
ViewPDFStatement = PDF-Kontoauszug anzeigen
|
||||
|
||||
#
|
||||
# Berechtigungen
|
||||
#
|
||||
PermBankImportRead = Bankimport: Buchungen und Kontoauszüge ansehen
|
||||
PermBankImportWrite = Bankimport: Kontoauszüge abrufen und PDF hochladen
|
||||
PermBankImportDelete = Bankimport: Buchungen und Kontoauszüge löschen
|
||||
|
||||
#
|
||||
# Zahlungsabgleich
|
||||
#
|
||||
PaymentConfirmation = Zahlungsabgleich
|
||||
PaymentConfirmationDesc = Bankbuchungen mit Rechnungen abgleichen und Zahlungen in Dolibarr erstellen.
|
||||
PendingPaymentMatches = %s neue Bankbuchungen warten auf Zuordnung
|
||||
PendingPaymentMatchesDesc = Prüfen und bestätigen Sie die Zuordnungen, um Zahlungen in Dolibarr zu erstellen.
|
||||
ReviewAndConfirm = Zuordnungen prüfen
|
||||
ConfirmPayment = Zahlung bestätigen
|
||||
ConfirmAllHighScore = Alle sicheren Zuordnungen bestätigen (Score >= 80%%)
|
||||
PaymentCreatedSuccessfully = Zahlung erstellt: %s - %s
|
||||
PaymentCreatedByBankImport = Automatisch erstellt durch Bankimport
|
||||
ErrorNoBankAccountConfigured = Kein Dolibarr-Bankkonto konfiguriert. Bitte in den Einstellungen zuordnen.
|
||||
NoBankAccountConfigured = Es ist noch kein Dolibarr-Bankkonto zugeordnet.
|
||||
TransactionAlreadyProcessed = Buchung wurde bereits verarbeitet
|
||||
PaymentsCreatedSummary = %s Zahlungen erstellt, %s fehlgeschlagen
|
||||
NoNewMatchesFound = Keine neuen Zuordnungen gefunden
|
||||
Alternatives = weitere Zuordnung(en)
|
||||
UnmatchedTransactions = %s neue Buchungen ohne Rechnungszuordnung
|
||||
InvoiceAlreadyPaid = Rechnung ist bereits vollständig bezahlt
|
||||
NoInvoicesSelected = Keine Rechnungen ausgewählt
|
||||
Invoices = Rechnungen
|
||||
SearchInvoiceManually = Rechnung manuell suchen
|
||||
SearchInvoiceByRef = Rechnungsnummer, Lieferantenzeichen oder Firma...
|
||||
InvoiceRef = Rechnungsnummer
|
||||
CustomerInvoice = Kundenrechnung
|
||||
SupplierInvoice = Lieferantenrechnung
|
||||
SupplierRef = Lieferantenreferenz
|
||||
ManualSearch = Manuelle Suche
|
||||
All = Alle
|
||||
SelectInvoicesManually = Rechnungen manuell auswählen
|
||||
SupplierInvoices = Lieferantenrechnungen
|
||||
CustomerInvoices = Kundenrechnungen
|
||||
Filter = Filtern
|
||||
RemoveFilter = Filter entfernen
|
||||
TransactionAmount = Transaktionsbetrag
|
||||
Selected = Ausgewählt
|
||||
AmountRemaining = Restbetrag
|
||||
NoUnpaidInvoices = Keine offenen Rechnungen gefunden
|
||||
CustomerRef = Kundenreferenz
|
||||
ShowPaidInvoices = Auch bezahlte anzeigen
|
||||
PaidInvoices = Bezahlte Rechnungen
|
||||
PaidInvoicesInfo = Diese Rechnungen wurden bereits bezahlt. Klicken Sie auf "Zahlung verknüpfen" um die existierende Zahlung mit der Bankbuchung zu verbinden.
|
||||
Paid = Bezahlt
|
||||
LinkExistingPayment = Zahlung verknüpfen
|
||||
PaymentLinkedSuccessfully = Zahlung erfolgreich verknüpft
|
||||
NoPaymentFoundForInvoice = Keine Zahlung für diese Rechnung gefunden
|
||||
|
||||
#
|
||||
# Bankkonto-Zuordnung
|
||||
#
|
||||
BankAccountMapping = Dolibarr-Bankkonto Zuordnung
|
||||
DolibarrBankAccount = Dolibarr Bankkonto
|
||||
DolibarrBankAccountHelp = Wählen Sie das Dolibarr-Bankkonto, das der konfigurierten IBAN entspricht. Zahlungen werden auf dieses Konto gebucht.
|
||||
SelectBankAccount = -- Bankkonto auswählen --
|
||||
|
||||
#
|
||||
# Kontoauszugsabgleich
|
||||
#
|
||||
BankEntriesReconciled = %s Bankbuchungen mit Auszug %s abgeglichen
|
||||
BankEntriesReconciledTotal = %s Bankbuchungen über %s Kontoauszüge abgeglichen
|
||||
ReconcileStatement = Kontoauszug abgleichen
|
||||
ReconcileAllStatements = Alle Kontoauszüge abgleichen
|
||||
NoBankEntriesToReconcile = Keine offenen Bankbuchungen zum Abgleichen gefunden
|
||||
StatementMissingDates = Kontoauszug hat keinen Zeitraum (Von/Bis) - Abgleich nicht möglich
|
||||
|
||||
#
|
||||
# Kontoauszugs-Positionen (Statement Lines)
|
||||
#
|
||||
StatementLinesExtracted = %s Buchungszeilen aus Auszug %s extrahiert
|
||||
StatementLines = Buchungszeilen
|
||||
BookingDate = Buchungstag
|
||||
ValueDate = Wertstellung
|
||||
TransactionType = Vorgangsart
|
||||
NoStatementLines = Keine Buchungszeilen vorhanden
|
||||
|
||||
#
|
||||
# Ausstehende Zuordnungen (Pending Review)
|
||||
#
|
||||
PendingReconciliationMatches = Zuordnungen mit Betragsabweichung
|
||||
PendingReconciliationMatchesDesc = Diese Zuordnungen wurden über Rechnungsnummern erkannt, aber der Betrag weicht um mehr als 5 EUR ab. Bitte prüfen und bestätigen.
|
||||
AmountStatement = Betrag (Auszug)
|
||||
AmountDolibarr = Betrag (Dolibarr)
|
||||
Difference = Differenz
|
||||
BankEntry = Bank-Eintrag
|
||||
ReconciliationConfirmed = Zuordnung bestätigt und abgeglichen
|
||||
Confirm = Bestätigen
|
||||
|
||||
#
|
||||
# Dashboard-Widget
|
||||
#
|
||||
BoxBankImportPending = Bankimport - Offene Zuordnungen
|
||||
|
||||
#
|
||||
# Unlink/Edit Payment
|
||||
#
|
||||
UnlinkPayment = Verknüpfung aufheben
|
||||
PaymentUnlinked = Verknüpfung wurde aufgehoben - Buchung ist wieder offen
|
||||
CannotUnlinkThisStatus = Verknüpfung kann bei diesem Status nicht aufgehoben werden
|
||||
|
||||
#
|
||||
# Linked Objects Display
|
||||
#
|
||||
Payment = Zahlung
|
||||
LinkedInvoices = Verknüpfte Rechnungen
|
||||
NoInvoicesLinkedToPayment = Keine Rechnungen mit dieser Zahlung verknüpft
|
||||
252
langs/en_US/bankimport.lang
Executable file
252
langs/en_US/bankimport.lang
Executable file
|
|
@ -0,0 +1,252 @@
|
|||
# Translation file
|
||||
|
||||
#
|
||||
# Generic
|
||||
#
|
||||
|
||||
# Module label 'ModuleBankImportName'
|
||||
ModuleBankImportName = BankImport
|
||||
# Module description 'ModuleBankImportDesc'
|
||||
ModuleBankImportDesc = Import bank statements via FinTS/HBCI
|
||||
|
||||
#
|
||||
# Admin page
|
||||
#
|
||||
BankImportSetup = BankImport Setup
|
||||
Settings = Settings
|
||||
BankImportSetupPage = BankImport setup page
|
||||
BankImportSetupDescription = Configure the FinTS/HBCI connection to your bank for automatic statement import.
|
||||
|
||||
# FinTS Configuration
|
||||
FinTSConfiguration = FinTS Bank Connection
|
||||
FinTSServerURL = FinTS Server URL
|
||||
FinTSServerURLHelp = For VR-Banks/Volksbanken: https://fints1.atruvia.de/cgi-bin/hbciservlet
|
||||
BLZ = Bank Code (BLZ)
|
||||
BLZHelp = 8-digit German bank code
|
||||
FinTSUsername = User ID
|
||||
FinTSPIN = PIN
|
||||
PINAlreadySet = PIN is configured
|
||||
PINHelp = PIN will be stored encrypted. Leave empty to keep existing PIN.
|
||||
AccountIBAN = Account Number / IBAN
|
||||
FinTSProductID = FinTS Product ID
|
||||
FinTSProductIDHelp = Optional. Registration number from Deutsche Kreditwirtschaft.
|
||||
TestConnection = Test Connection
|
||||
ConnectionTestSuccessful = Connection test successful!
|
||||
ConnectionTestFailed = Connection test failed
|
||||
FinTSClassNotFound = FinTS class not found. Please check installation.
|
||||
FinTSNotConfigured = FinTS connection not configured
|
||||
FinTSLibraryNotFound = phpFinTS library not found. Please run composer install.
|
||||
GoToSetup = Go to setup
|
||||
FetchAvailableAccounts = Fetch Available Accounts
|
||||
AccountsFound = %s accounts found
|
||||
NoAccountsFound = No accounts found
|
||||
TANRequiredForAccountList = TAN required - account list not available without TAN
|
||||
AccountNumber = Account Number
|
||||
UseThisAccount = Use This Account
|
||||
Selected = Selected
|
||||
IBANUpdated = IBAN updated: %s
|
||||
|
||||
# VR Bank Info
|
||||
VRBankInfo = VR-Bank / Volksbank Information
|
||||
VRBankInfoText = For VR-Banks use the Atruvia server: https://fints1.atruvia.de/cgi-bin/hbciservlet<br>The TAN procedure SecureGo Plus (Decoupled TAN) is supported.
|
||||
|
||||
# Security
|
||||
SecurityInfo = Security Information
|
||||
SecurityInfoText = The PIN is stored encrypted in the database. For additional security, ensure your Dolibarr installation uses HTTPS.
|
||||
|
||||
#
|
||||
# Statements page
|
||||
#
|
||||
BankStatements = Bank Statements
|
||||
FetchStatements = Fetch Statements
|
||||
Transactions = Transactions
|
||||
TransactionsFound = %s transactions found
|
||||
NoTransactionsFound = No transactions found in the selected period
|
||||
AccountBalance = Account Balance
|
||||
AsOf = As of
|
||||
LoginFailed = Login failed
|
||||
FetchFailed = Failed to fetch statements
|
||||
TANRequired = TAN required
|
||||
TANRequiredForStatements = TAN required to fetch statements
|
||||
SecureGoPlusConfirmRequired = Please confirm in your VR SecureGo plus App
|
||||
WaitingForSecureGoConfirmation = Waiting for confirmation in SecureGo plus App...
|
||||
CheckSecureGoStatus = Check Status
|
||||
TANCheckFailed = TAN verification failed
|
||||
|
||||
# Import
|
||||
ImportTransactions = Import Transactions
|
||||
ViewImportedTransactions = View Imported Transactions
|
||||
TransactionsImported = %s transactions imported
|
||||
TransactionsSkipped = %s transactions skipped (already exist)
|
||||
|
||||
#
|
||||
# Transaction List
|
||||
#
|
||||
TransactionList = Transaction List
|
||||
TransactionRef = Reference
|
||||
Counterparty = Counterparty
|
||||
AllStatuses = All Statuses
|
||||
StatusNew = New
|
||||
StatusMatched = Matched
|
||||
StatusReconciled = Reconciled
|
||||
StatusIgnored = Ignored
|
||||
SearchTransactions = Search Transactions
|
||||
NoTransactionsInDatabase = No transactions in database
|
||||
|
||||
# Status Labels
|
||||
NewTransaction = New Transaction
|
||||
TransactionMatched = Transaction Matched
|
||||
TransactionReconciled = Transaction Reconciled
|
||||
TransactionIgnored = Transaction Ignored
|
||||
New = New
|
||||
Matched = Matched
|
||||
Reconciled = Reconciled
|
||||
Ignored = Ignored
|
||||
|
||||
# Card page
|
||||
Transaction = Transaction
|
||||
DateValue = Value Date
|
||||
CounterpartyIBAN = Counterparty IBAN
|
||||
EndToEndId = End-to-End ID
|
||||
MandateId = Mandate Reference
|
||||
FindMatches = Find Matches
|
||||
SetAsIgnored = Set as Ignored
|
||||
Reopen = Reopen
|
||||
MatchesFound = %s possible matches found
|
||||
NoMatchesFound = No matches found
|
||||
Link = Link
|
||||
LinkCreated = Link created
|
||||
StatusUpdated = Status updated
|
||||
BackToList = Back to List
|
||||
Score = Match Score
|
||||
|
||||
# PDF Statements
|
||||
PDFStatements = PDF Statements
|
||||
FetchPDFStatements = Fetch PDF Statements
|
||||
StatementNumber = Statement Number
|
||||
StatementYear = Year
|
||||
StatementDate = Statement Date
|
||||
DownloadPDF = Download PDF
|
||||
NoPDFStatementsFound = No PDF statements found
|
||||
PDFStatementsImported = %s statements imported
|
||||
StatementAlreadyExists = Statement already exists
|
||||
|
||||
#
|
||||
# About page
|
||||
#
|
||||
About = About
|
||||
BankImportAbout = About BankImport
|
||||
BankImportAboutPage = BankImport about page
|
||||
|
||||
#
|
||||
# Home
|
||||
#
|
||||
BankImportArea = BankImport Home
|
||||
|
||||
#
|
||||
# Payment Confirmation
|
||||
#
|
||||
PaymentConfirmation = Payment Matching
|
||||
PaymentConfirmationDesc = Match bank transactions to invoices and create payments in Dolibarr.
|
||||
PendingPaymentMatches = %s new bank transactions waiting for matching
|
||||
PendingPaymentMatchesDesc = Review and confirm matches to create payments in Dolibarr.
|
||||
ReviewAndConfirm = Review matches
|
||||
ConfirmPayment = Confirm payment
|
||||
ConfirmAllHighScore = Confirm all high-score matches (score >= 80%%)
|
||||
PaymentCreatedSuccessfully = Payment created: %s - %s
|
||||
PaymentCreatedByBankImport = Automatically created by BankImport
|
||||
ErrorNoBankAccountConfigured = No Dolibarr bank account configured. Please configure in settings.
|
||||
NoBankAccountConfigured = No Dolibarr bank account mapped yet.
|
||||
TransactionAlreadyProcessed = Transaction already processed
|
||||
PaymentsCreatedSummary = %s payments created, %s failed
|
||||
NoNewMatchesFound = No new matches found
|
||||
Alternatives = more match(es)
|
||||
UnmatchedTransactions = %s new transactions without invoice match
|
||||
InvoiceAlreadyPaid = Invoice is already fully paid
|
||||
NoInvoicesSelected = No invoices selected
|
||||
Invoices = Invoices
|
||||
MatchByMultiInvoice = Multi-invoice payment
|
||||
SearchInvoiceManually = Search invoice manually
|
||||
SearchInvoiceByRef = Enter invoice reference...
|
||||
InvoiceRef = Invoice reference
|
||||
CustomerInvoice = Customer invoice
|
||||
SupplierInvoice = Supplier invoice
|
||||
SupplierRef = Supplier reference
|
||||
ManualSearch = Manual search
|
||||
All = All
|
||||
SelectInvoicesManually = Select invoices manually
|
||||
SupplierInvoices = Supplier invoices
|
||||
CustomerInvoices = Customer invoices
|
||||
Filter = Filter
|
||||
RemoveFilter = Remove filter
|
||||
TransactionAmount = Transaction amount
|
||||
Selected = Selected
|
||||
AmountRemaining = Remaining amount
|
||||
NoUnpaidInvoices = No unpaid invoices found
|
||||
CustomerRef = Customer reference
|
||||
ShowPaidInvoices = Show paid invoices
|
||||
PaidInvoices = Paid Invoices
|
||||
PaidInvoicesInfo = These invoices have already been paid. Click "Link Payment" to connect the existing payment with the bank entry.
|
||||
Paid = Paid
|
||||
LinkExistingPayment = Link Payment
|
||||
PaymentLinkedSuccessfully = Payment linked successfully
|
||||
NoPaymentFoundForInvoice = No payment found for this invoice
|
||||
|
||||
#
|
||||
# Bank Account Mapping
|
||||
#
|
||||
BankAccountMapping = Dolibarr Bank Account Mapping
|
||||
DolibarrBankAccount = Dolibarr Bank Account
|
||||
DolibarrBankAccountHelp = Select the Dolibarr bank account corresponding to the configured IBAN. Payments will be booked to this account.
|
||||
SelectBankAccount = -- Select bank account --
|
||||
|
||||
#
|
||||
# Statement Reconciliation
|
||||
#
|
||||
BankEntriesReconciled = %s bank entries reconciled with statement %s
|
||||
BankEntriesReconciledTotal = %s bank entries reconciled across %s statements
|
||||
ReconcileStatement = Reconcile statement
|
||||
ReconcileAllStatements = Reconcile all statements
|
||||
NoBankEntriesToReconcile = No open bank entries found to reconcile
|
||||
StatementMissingDates = Statement has no date range (from/to) - reconciliation not possible
|
||||
|
||||
#
|
||||
# Statement Lines
|
||||
#
|
||||
StatementLinesExtracted = %s transaction lines extracted from statement %s
|
||||
StatementLines = Transaction Lines
|
||||
BookingDate = Booking Date
|
||||
ValueDate = Value Date
|
||||
TransactionType = Transaction Type
|
||||
NoStatementLines = No transaction lines available
|
||||
|
||||
#
|
||||
# Pending Review Matches
|
||||
#
|
||||
PendingReconciliationMatches = Matches with Amount Deviation
|
||||
PendingReconciliationMatchesDesc = These matches were found via invoice numbers but the amount differs by more than 5 EUR. Please review and confirm.
|
||||
AmountStatement = Amount (Statement)
|
||||
AmountDolibarr = Amount (Dolibarr)
|
||||
Difference = Difference
|
||||
BankEntry = Bank Entry
|
||||
ReconciliationConfirmed = Match confirmed and reconciled
|
||||
Confirm = Confirm
|
||||
|
||||
#
|
||||
# Dashboard Widget
|
||||
#
|
||||
BoxBankImportPending = BankImport - Pending Matches
|
||||
|
||||
#
|
||||
# Unlink/Edit Payment
|
||||
#
|
||||
UnlinkPayment = Unlink Payment
|
||||
PaymentUnlinked = Payment link removed - transaction is now open again
|
||||
CannotUnlinkThisStatus = Cannot unlink with this status
|
||||
|
||||
#
|
||||
# Linked Objects Display
|
||||
#
|
||||
Payment = Payment
|
||||
LinkedInvoices = Linked Invoices
|
||||
NoInvoicesLinkedToPayment = No invoices linked to this payment
|
||||
85
lib/bankimport.lib.php
Executable file
85
lib/bankimport.lib.php
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
<?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 bankimport/lib/bankimport.lib.php
|
||||
* \ingroup bankimport
|
||||
* \brief Library files with common functions for BankImport
|
||||
*/
|
||||
|
||||
/**
|
||||
* Prepare admin pages header
|
||||
*
|
||||
* @return array<array{string,string,string}>
|
||||
*/
|
||||
function bankimportAdminPrepareHead()
|
||||
{
|
||||
global $langs, $conf;
|
||||
|
||||
// global $db;
|
||||
// $extrafields = new ExtraFields($db);
|
||||
// $extrafields->fetch_name_optionals_label('myobject');
|
||||
|
||||
$langs->load("bankimport@bankimport");
|
||||
|
||||
$h = 0;
|
||||
$head = array();
|
||||
|
||||
$head[$h][0] = dol_buildpath("/bankimport/admin/setup.php", 1);
|
||||
$head[$h][1] = $langs->trans("Settings");
|
||||
$head[$h][2] = 'settings';
|
||||
$h++;
|
||||
|
||||
/*
|
||||
$head[$h][0] = dol_buildpath("/bankimport/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("/bankimport/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("/bankimport/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:@bankimport:/bankimport/mypage.php?id=__ID__'
|
||||
//); // to add new tab
|
||||
//$this->tabs = array(
|
||||
// 'entity:-tabname:Title:@bankimport:/bankimport/mypage.php?id=__ID__'
|
||||
//); // to remove a tab
|
||||
complete_head_from_modules($conf, $langs, null, $head, $h, 'bankimport@bankimport');
|
||||
|
||||
complete_head_from_modules($conf, $langs, null, $head, $h, 'bankimport@bankimport', 'remove');
|
||||
|
||||
return $head;
|
||||
}
|
||||
329
list.php
Executable file
329
list.php
Executable file
|
|
@ -0,0 +1,329 @@
|
|||
<?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 bankimport/list.php
|
||||
* \ingroup bankimport
|
||||
* \brief List page for imported bank transactions
|
||||
*/
|
||||
|
||||
// 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/lib/date.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
|
||||
dol_include_once('/bankimport/class/banktransaction.class.php');
|
||||
dol_include_once('/bankimport/lib/bankimport.lib.php');
|
||||
|
||||
/**
|
||||
* @var Conf $conf
|
||||
* @var DoliDB $db
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
$langs->loadLangs(array("bankimport@bankimport", "banks", "bills"));
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$massaction = GETPOST('massaction', 'alpha');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
$toselect = GETPOST('toselect', 'array');
|
||||
$contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'bankimporttransactionlist';
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('bankimport', 'read')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Search parameters
|
||||
$search_ref = GETPOST('search_ref', 'alpha');
|
||||
$search_iban = GETPOST('search_iban', 'alpha');
|
||||
$search_name = GETPOST('search_name', 'alpha');
|
||||
$search_description = GETPOST('search_description', 'alpha');
|
||||
$search_amount_min = GETPOST('search_amount_min', 'alpha');
|
||||
$search_amount_max = GETPOST('search_amount_max', 'alpha');
|
||||
$search_status = GETPOST('search_status', 'intcomma');
|
||||
|
||||
// Date filter
|
||||
$search_date_from = dol_mktime(0, 0, 0, GETPOSTINT('search_date_frommonth'), GETPOSTINT('search_date_fromday'), GETPOSTINT('search_date_fromyear'));
|
||||
$search_date_to = dol_mktime(23, 59, 59, GETPOSTINT('search_date_tomonth'), GETPOSTINT('search_date_today'), GETPOSTINT('search_date_toyear'));
|
||||
|
||||
// Sorting
|
||||
$sortfield = GETPOST('sortfield', 'aZ09comma');
|
||||
$sortorder = GETPOST('sortorder', 'aZ09comma');
|
||||
if (!$sortfield) $sortfield = 'date_trans';
|
||||
if (!$sortorder) $sortorder = 'DESC';
|
||||
|
||||
// Pagination
|
||||
$limit = GETPOSTINT('limit') ? GETPOSTINT('limit') : $conf->liste_limit;
|
||||
$page = GETPOSTISSET('pageplusone') ? (GETPOSTINT('pageplusone') - 1) : GETPOSTINT('page');
|
||||
if (empty($page) || $page < 0) $page = 0;
|
||||
$offset = $limit * $page;
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// Reset search
|
||||
if (GETPOST('button_removefilter_x', 'alpha') || GETPOST('button_removefilter.x', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
|
||||
$search_ref = '';
|
||||
$search_iban = '';
|
||||
$search_name = '';
|
||||
$search_description = '';
|
||||
$search_amount_min = '';
|
||||
$search_amount_max = '';
|
||||
$search_status = '';
|
||||
$search_date_from = '';
|
||||
$search_date_to = '';
|
||||
$toselect = array();
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
$transaction = new BankImportTransaction($db);
|
||||
|
||||
$title = $langs->trans("TransactionList");
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-bankimport page-list');
|
||||
|
||||
print load_fiche_titre($title, '', 'bank');
|
||||
|
||||
// Build filter array
|
||||
$filter = array();
|
||||
if (!empty($search_ref)) $filter['ref'] = $search_ref;
|
||||
if (!empty($search_iban)) $filter['iban'] = $search_iban;
|
||||
if (!empty($search_name)) $filter['name'] = $search_name;
|
||||
if (!empty($search_description)) $filter['description'] = $search_description;
|
||||
if (!empty($search_amount_min)) $filter['amount_min'] = price2num($search_amount_min);
|
||||
if (!empty($search_amount_max)) $filter['amount_max'] = price2num($search_amount_max);
|
||||
if ($search_status !== '' && $search_status >= 0) $filter['status'] = (int) $search_status;
|
||||
if (!empty($search_date_from)) $filter['date_from'] = $search_date_from;
|
||||
if (!empty($search_date_to)) $filter['date_to'] = $search_date_to;
|
||||
|
||||
// Count total
|
||||
$totalRecords = $transaction->fetchAll('', '', 0, 0, $filter, 'count');
|
||||
|
||||
// Fetch records
|
||||
$records = $transaction->fetchAll($sortfield, $sortorder, $limit, $offset, $filter);
|
||||
|
||||
// Form
|
||||
$param = '';
|
||||
if (!empty($search_ref)) $param .= '&search_ref='.urlencode($search_ref);
|
||||
if (!empty($search_iban)) $param .= '&search_iban='.urlencode($search_iban);
|
||||
if (!empty($search_name)) $param .= '&search_name='.urlencode($search_name);
|
||||
if (!empty($search_description)) $param .= '&search_description='.urlencode($search_description);
|
||||
if (!empty($search_amount_min)) $param .= '&search_amount_min='.urlencode($search_amount_min);
|
||||
if (!empty($search_amount_max)) $param .= '&search_amount_max='.urlencode($search_amount_max);
|
||||
if ($search_status !== '' && $search_status >= 0) $param .= '&search_status='.urlencode($search_status);
|
||||
if (!empty($search_date_from)) {
|
||||
$param .= '&search_date_fromday='.dol_print_date($search_date_from, '%d');
|
||||
$param .= '&search_date_frommonth='.dol_print_date($search_date_from, '%m');
|
||||
$param .= '&search_date_fromyear='.dol_print_date($search_date_from, '%Y');
|
||||
}
|
||||
if (!empty($search_date_to)) {
|
||||
$param .= '&search_date_today='.dol_print_date($search_date_to, '%d');
|
||||
$param .= '&search_date_tomonth='.dol_print_date($search_date_to, '%m');
|
||||
$param .= '&search_date_toyear='.dol_print_date($search_date_to, '%Y');
|
||||
}
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" name="formulaire">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
|
||||
print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
|
||||
|
||||
// Navigation
|
||||
print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', is_array($records) ? count($records) : 0, $totalRecords, '', 0, '', '', $limit);
|
||||
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="tagtable liste listwithfilterbefore centpercent">';
|
||||
|
||||
// Header row with filters
|
||||
print '<tr class="liste_titre_filter">';
|
||||
|
||||
// Ref
|
||||
print '<td class="liste_titre">';
|
||||
print '<input type="text" class="flat maxwidth75" name="search_ref" value="'.dol_escape_htmltag($search_ref).'">';
|
||||
print '</td>';
|
||||
|
||||
// IBAN
|
||||
print '<td class="liste_titre">';
|
||||
print '<input type="text" class="flat maxwidth100" name="search_iban" value="'.dol_escape_htmltag($search_iban).'">';
|
||||
print '</td>';
|
||||
|
||||
// Date
|
||||
print '<td class="liste_titre center">';
|
||||
print '<div class="nowraponall">';
|
||||
print $form->selectDate($search_date_from, 'search_date_from', 0, 0, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("From"));
|
||||
print '</div>';
|
||||
print '<div class="nowraponall">';
|
||||
print $form->selectDate($search_date_to, 'search_date_to', 0, 0, 1, '', 1, 0, 0, '', '', '', '', 1, '', $langs->trans("To"));
|
||||
print '</div>';
|
||||
print '</td>';
|
||||
|
||||
// Name
|
||||
print '<td class="liste_titre">';
|
||||
print '<input type="text" class="flat maxwidth150" name="search_name" value="'.dol_escape_htmltag($search_name).'">';
|
||||
print '</td>';
|
||||
|
||||
// Description
|
||||
print '<td class="liste_titre">';
|
||||
print '<input type="text" class="flat maxwidth200" name="search_description" value="'.dol_escape_htmltag($search_description).'">';
|
||||
print '</td>';
|
||||
|
||||
// Amount
|
||||
print '<td class="liste_titre right">';
|
||||
print '<input type="text" class="flat maxwidth50" name="search_amount_min" placeholder="Min" value="'.dol_escape_htmltag($search_amount_min).'">';
|
||||
print ' - ';
|
||||
print '<input type="text" class="flat maxwidth50" name="search_amount_max" placeholder="Max" value="'.dol_escape_htmltag($search_amount_max).'">';
|
||||
print '</td>';
|
||||
|
||||
// Statement
|
||||
print '<td class="liste_titre">';
|
||||
print '</td>';
|
||||
|
||||
// Status
|
||||
print '<td class="liste_titre center">';
|
||||
$statusArray = array(
|
||||
'' => $langs->trans("AllStatuses"),
|
||||
'0' => $langs->trans("StatusNew"),
|
||||
'1' => $langs->trans("StatusMatched"),
|
||||
'2' => $langs->trans("StatusReconciled"),
|
||||
'9' => $langs->trans("StatusIgnored")
|
||||
);
|
||||
print $form->selectarray('search_status', $statusArray, $search_status, 0, 0, 0, '', 0, 0, 0, '', 'minwidth100');
|
||||
print '</td>';
|
||||
|
||||
// Action buttons
|
||||
print '<td class="liste_titre center">';
|
||||
print '<input type="image" class="liste_titre" src="'.img_picto('', 'search.png', '', 0, 1).'" name="button_search" value="'.dol_escape_htmltag($langs->trans("Search")).'" title="'.dol_escape_htmltag($langs->trans("Search")).'">';
|
||||
print ' ';
|
||||
print '<input type="image" class="liste_titre" src="'.img_picto('', 'searchclear.png', '', 0, 1).'" name="button_removefilter" value="'.dol_escape_htmltag($langs->trans("RemoveFilter")).'" title="'.dol_escape_htmltag($langs->trans("RemoveFilter")).'">';
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
// Header titles
|
||||
print '<tr class="liste_titre">';
|
||||
print_liste_field_titre($langs->trans("TransactionRef"), $_SERVER["PHP_SELF"], "ref", "", $param, "", $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans("AccountIBAN"), $_SERVER["PHP_SELF"], "iban", "", $param, "", $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans("Date"), $_SERVER["PHP_SELF"], "date_trans", "", $param, 'class="center"', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans("Counterparty"), $_SERVER["PHP_SELF"], "name", "", $param, "", $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans("Description"), $_SERVER["PHP_SELF"], "description", "", $param, "", $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans("Amount"), $_SERVER["PHP_SELF"], "amount", "", $param, 'class="right"', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans("PDFStatement"), $_SERVER["PHP_SELF"], "fk_statement", "", $param, 'class="center"', $sortfield, $sortorder);
|
||||
print_liste_field_titre($langs->trans("Status"), $_SERVER["PHP_SELF"], "status", "", $param, 'class="center"', $sortfield, $sortorder);
|
||||
print_liste_field_titre('', $_SERVER["PHP_SELF"], "", "", $param, 'class="center"', $sortfield, $sortorder);
|
||||
print '</tr>';
|
||||
|
||||
// Data rows
|
||||
if (is_array($records) && count($records) > 0) {
|
||||
foreach ($records as $obj) {
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Ref
|
||||
print '<td class="nowraponall">';
|
||||
print '<a href="'.dol_buildpath('/bankimport/card.php', 1).'?id='.$obj->id.'">'.dol_escape_htmltag($obj->ref).'</a>';
|
||||
print '</td>';
|
||||
|
||||
// IBAN
|
||||
print '<td class="nowraponall">';
|
||||
print dol_escape_htmltag(dol_trunc($obj->iban, 20));
|
||||
print '</td>';
|
||||
|
||||
// Date
|
||||
print '<td class="center nowraponall">';
|
||||
print dol_print_date($obj->date_trans, 'day');
|
||||
print '</td>';
|
||||
|
||||
// Name
|
||||
print '<td>';
|
||||
print dol_escape_htmltag($obj->name);
|
||||
print '</td>';
|
||||
|
||||
// Description
|
||||
print '<td class="small">';
|
||||
print dol_escape_htmltag(dol_trunc($obj->description, 60));
|
||||
print '</td>';
|
||||
|
||||
// Amount
|
||||
print '<td class="right nowraponall">';
|
||||
if ($obj->amount >= 0) {
|
||||
print '<span style="color: green;">+'.price($obj->amount, 0, $langs, 1, -1, 2, $obj->currency).'</span>';
|
||||
} else {
|
||||
print '<span style="color: red;">'.price($obj->amount, 0, $langs, 1, -1, 2, $obj->currency).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Statement link
|
||||
print '<td class="center nowraponall">';
|
||||
if (!empty($obj->fk_statement)) {
|
||||
print '<a href="'.dol_buildpath('/bankimport/pdfstatements.php', 1).'?action=view&id='.$obj->fk_statement.'&token='.newToken().'" target="_blank" title="'.$langs->trans("ViewPDFStatement").'">';
|
||||
print img_picto($langs->trans("ViewPDFStatement"), 'pdf');
|
||||
print '</a>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Status
|
||||
print '<td class="center">';
|
||||
print $obj->getLibStatut(5);
|
||||
print '</td>';
|
||||
|
||||
// Actions
|
||||
print '<td class="center nowraponall">';
|
||||
print '<a href="'.dol_buildpath('/bankimport/card.php', 1).'?id='.$obj->id.'">'.img_edit().'</a>';
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="9" class="opacitymedium center">';
|
||||
print $langs->trans("NoTransactionsInDatabase");
|
||||
print '</td></tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
// Buttons
|
||||
print '<div class="tabsAction">';
|
||||
print '<a class="butAction" href="'.dol_buildpath('/bankimport/statements.php', 1).'">'.$langs->trans("FetchStatements").'</a>';
|
||||
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.
|
||||
955
pdfstatements.php
Executable file
955
pdfstatements.php
Executable file
|
|
@ -0,0 +1,955 @@
|
|||
<?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 bankimport/pdfstatements.php
|
||||
* \ingroup bankimport
|
||||
* \brief Page to upload and manage PDF bank statements
|
||||
*/
|
||||
|
||||
// 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/lib/date.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
|
||||
dol_include_once('/bankimport/class/bankstatement.class.php');
|
||||
dol_include_once('/bankimport/lib/bankimport.lib.php');
|
||||
|
||||
/**
|
||||
* @var Conf $conf
|
||||
* @var DoliDB $db
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
$langs->loadLangs(array("bankimport@bankimport", "banks", "other"));
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
$year = GETPOSTISSET('year') ? GETPOSTINT('year') : (int) date('Y');
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('bankimport', 'read')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
$statement = new BankImportStatement($db);
|
||||
|
||||
// Upload PDF (supports multiple files)
|
||||
if ($action == 'upload' && !empty($_FILES['pdffile'])) {
|
||||
$uploadMode = GETPOST('upload_mode', 'alpha');
|
||||
$isAutoMode = ($uploadMode !== 'manual');
|
||||
|
||||
// Normalize $_FILES for multi-upload: always work with arrays
|
||||
$fileNames = is_array($_FILES['pdffile']['name']) ? $_FILES['pdffile']['name'] : array($_FILES['pdffile']['name']);
|
||||
$fileTmps = is_array($_FILES['pdffile']['tmp_name']) ? $_FILES['pdffile']['tmp_name'] : array($_FILES['pdffile']['tmp_name']);
|
||||
$fileSizes = is_array($_FILES['pdffile']['size']) ? $_FILES['pdffile']['size'] : array($_FILES['pdffile']['size']);
|
||||
$fileCount = count($fileNames);
|
||||
|
||||
$uploadedCount = 0;
|
||||
$errorCount = 0;
|
||||
$totalLinked = 0;
|
||||
$lastYear = (int) date('Y');
|
||||
|
||||
for ($fi = 0; $fi < $fileCount; $fi++) {
|
||||
$error = 0;
|
||||
|
||||
// Skip empty file slots
|
||||
if (empty($fileNames[$fi]) || empty($fileTmps[$fi])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate uploaded file
|
||||
if (!is_uploaded_file($fileTmps[$fi])) {
|
||||
setEventMessages($langs->trans("ErrorNoFileUploaded").': '.$fileNames[$fi], null, 'errors');
|
||||
$errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check MIME type
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $fileTmps[$fi]);
|
||||
finfo_close($finfo);
|
||||
if ($mimeType !== 'application/pdf') {
|
||||
setEventMessages($langs->trans("ErrorOnlyPDFAllowed").': '.$fileNames[$fi], null, 'errors');
|
||||
$errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check file size (max 10MB)
|
||||
if ($fileSizes[$fi] > 10 * 1024 * 1024) {
|
||||
setEventMessages($langs->trans("ErrorFileTooLarge").': '.$fileNames[$fi], null, 'errors');
|
||||
$errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse PDF metadata automatically
|
||||
$parsed = BankImportStatement::parsePdfMetadata($fileTmps[$fi]);
|
||||
|
||||
// Determine values: auto mode uses parsed data, manual mode uses form fields
|
||||
if ($isAutoMode && $parsed) {
|
||||
$statementNumber = $parsed['statement_number'];
|
||||
$statementYear = $parsed['statement_year'];
|
||||
$iban = $parsed['iban'];
|
||||
} else {
|
||||
// Manual mode (only for single file upload)
|
||||
$statementNumber = GETPOST('statement_number', 'alpha');
|
||||
$statementYear = GETPOSTINT('statement_year');
|
||||
$iban = GETPOST('iban', 'alpha');
|
||||
// Auto-fill from parsed data if form fields are empty
|
||||
if ($parsed) {
|
||||
if (empty($statementNumber) && !empty($parsed['statement_number'])) {
|
||||
$statementNumber = $parsed['statement_number'];
|
||||
}
|
||||
if (empty($statementYear) && !empty($parsed['statement_year'])) {
|
||||
$statementYear = $parsed['statement_year'];
|
||||
}
|
||||
if (empty($iban) && !empty($parsed['iban'])) {
|
||||
$iban = $parsed['iban'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show auto-detection info
|
||||
if ($parsed) {
|
||||
$autoMsg = $langs->trans("PdfAutoDetected").': '.$fileNames[$fi];
|
||||
if (!empty($statementNumber)) {
|
||||
$autoMsg .= ' | '.$statementNumber.'/'.$statementYear;
|
||||
}
|
||||
if (!empty($parsed['pdf_number'])) {
|
||||
$autoMsg .= ' (PDF-Nr. '.$parsed['pdf_number'].'/'.$parsed['pdf_year'].')';
|
||||
}
|
||||
if (!empty($parsed['iban'])) {
|
||||
$autoMsg .= ' | IBAN: '.$parsed['iban'];
|
||||
}
|
||||
if ($parsed['date_from'] && $parsed['date_to']) {
|
||||
$autoMsg .= ' | '.$langs->trans("Period").': '.dol_print_date($parsed['date_from'], 'day').' - '.dol_print_date($parsed['date_to'], 'day');
|
||||
}
|
||||
setEventMessages($autoMsg, null, 'mesgs');
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (empty($statementNumber)) {
|
||||
setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentities("StatementNumber")).': '.$fileNames[$fi], null, 'errors');
|
||||
$errorCount++;
|
||||
continue;
|
||||
}
|
||||
if (empty($statementYear)) {
|
||||
setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentities("Year")).': '.$fileNames[$fi], null, 'errors');
|
||||
$errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create new statement object for each file
|
||||
$stmt = new BankImportStatement($db);
|
||||
$stmt->iban = $iban;
|
||||
$stmt->statement_number = $statementNumber;
|
||||
$stmt->statement_year = $statementYear;
|
||||
|
||||
// Date fields
|
||||
if ($isAutoMode && $parsed) {
|
||||
$stmt->statement_date = $parsed['statement_date'];
|
||||
$stmt->date_from = $parsed['date_from'];
|
||||
$stmt->date_to = $parsed['date_to'];
|
||||
$stmt->opening_balance = $parsed['opening_balance'];
|
||||
$stmt->closing_balance = $parsed['closing_balance'];
|
||||
} else {
|
||||
$statementDate = dol_mktime(0, 0, 0, GETPOSTINT('statement_datemonth'), GETPOSTINT('statement_dateday'), GETPOSTINT('statement_dateyear'));
|
||||
$dateFrom = dol_mktime(0, 0, 0, GETPOSTINT('date_frommonth'), GETPOSTINT('date_fromday'), GETPOSTINT('date_fromyear'));
|
||||
$dateTo = dol_mktime(0, 0, 0, GETPOSTINT('date_tomonth'), GETPOSTINT('date_today'), GETPOSTINT('date_toyear'));
|
||||
$stmt->statement_date = $statementDate ?: ($parsed ? $parsed['statement_date'] : null);
|
||||
$stmt->date_from = $dateFrom ?: ($parsed ? $parsed['date_from'] : null);
|
||||
$stmt->date_to = $dateTo ?: ($parsed ? $parsed['date_to'] : null);
|
||||
$openBal = GETPOST('opening_balance', 'alpha');
|
||||
$closeBal = GETPOST('closing_balance', 'alpha');
|
||||
$stmt->opening_balance = ($openBal !== '' && $openBal !== null) ? (float) price2num($openBal) : ($parsed ? $parsed['opening_balance'] : null);
|
||||
$stmt->closing_balance = ($closeBal !== '' && $closeBal !== null) ? (float) price2num($closeBal) : ($parsed ? $parsed['closing_balance'] : null);
|
||||
}
|
||||
|
||||
$stmt->import_key = date('YmdHis').'_'.$user->id;
|
||||
|
||||
// Check duplicate
|
||||
if ($stmt->exists()) {
|
||||
setEventMessages($langs->trans("StatementAlreadyExists").': '.$statementNumber.'/'.$statementYear, null, 'warnings');
|
||||
$errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate filename and save file
|
||||
$dir = BankImportStatement::getStorageDir();
|
||||
|
||||
if ($parsed) {
|
||||
$newFilename = BankImportStatement::generateFilename($parsed);
|
||||
} else {
|
||||
$ibanPart = !empty($stmt->iban) ? preg_replace('/[^A-Z0-9]/', '', strtoupper($stmt->iban)) : 'KONTO';
|
||||
$newFilename = sprintf('Kontoauszug_%s_%d_%s.pdf',
|
||||
$ibanPart,
|
||||
$stmt->statement_year,
|
||||
str_pad($stmt->statement_number, 3, '0', STR_PAD_LEFT)
|
||||
);
|
||||
}
|
||||
|
||||
$stmt->filepath = $dir.'/'.$newFilename;
|
||||
|
||||
// Avoid overwriting existing files
|
||||
if (file_exists($stmt->filepath)) {
|
||||
$newFilename = pathinfo($newFilename, PATHINFO_FILENAME).'_'.date('His').'.pdf';
|
||||
$stmt->filepath = $dir.'/'.$newFilename;
|
||||
}
|
||||
|
||||
$stmt->filename = $newFilename;
|
||||
|
||||
// Move uploaded file
|
||||
if (!move_uploaded_file($fileTmps[$fi], $stmt->filepath)) {
|
||||
setEventMessages($langs->trans("ErrorFailedToSaveFile").': '.$fileNames[$fi], null, 'errors');
|
||||
$errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->filesize = filesize($stmt->filepath);
|
||||
|
||||
// Save to database
|
||||
$result = $stmt->create($user);
|
||||
|
||||
if ($result > 0) {
|
||||
// Link matching FinTS transactions to this statement
|
||||
$linked = $stmt->linkTransactions();
|
||||
$totalLinked += max(0, $linked);
|
||||
|
||||
// Parse PDF transaction lines and save to database
|
||||
$pdfLines = $stmt->parsePdfTransactions();
|
||||
if (!empty($pdfLines)) {
|
||||
$linesSaved = $stmt->saveStatementLines($pdfLines);
|
||||
if ($linesSaved > 0) {
|
||||
setEventMessages($langs->trans("StatementLinesExtracted", $linesSaved, $stmt->statement_number.'/'.$stmt->statement_year), null, 'mesgs');
|
||||
}
|
||||
}
|
||||
|
||||
// Copy PDF to Dolibarr's bank statement document directory
|
||||
$uploadBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
if ($uploadBankAccountId > 0) {
|
||||
$stmt->copyToDolibarrStatementDir($uploadBankAccountId);
|
||||
}
|
||||
|
||||
// Reconcile bank entries if bank account is configured
|
||||
if ($uploadBankAccountId > 0) {
|
||||
$reconciledCount = $stmt->reconcileBankEntries($user, $uploadBankAccountId);
|
||||
if ($reconciledCount > 0) {
|
||||
setEventMessages($langs->trans("BankEntriesReconciled", $reconciledCount, $stmt->statement_number.'/'.$stmt->statement_year), null, 'mesgs');
|
||||
}
|
||||
}
|
||||
|
||||
$uploadedCount++;
|
||||
$lastYear = $stmt->statement_year;
|
||||
} else {
|
||||
setEventMessages($stmt->error, null, 'errors');
|
||||
$errorCount++;
|
||||
// Clean up file on DB error
|
||||
if (file_exists($stmt->filepath)) {
|
||||
@unlink($stmt->filepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary message
|
||||
if ($uploadedCount > 0) {
|
||||
if ($uploadedCount == 1) {
|
||||
$msg = $langs->trans("StatementUploaded");
|
||||
} else {
|
||||
$msg = $langs->trans("StatementsUploaded", $uploadedCount);
|
||||
}
|
||||
if ($totalLinked > 0) {
|
||||
$msg .= ' | '.$langs->trans("TransactionsLinked", $totalLinked);
|
||||
}
|
||||
setEventMessages($msg, null, 'mesgs');
|
||||
// Redirect: for single upload use the year, for multi-upload show all
|
||||
if ($uploadedCount == 1) {
|
||||
header("Location: ".$_SERVER['PHP_SELF']."?year=".$lastYear);
|
||||
} else {
|
||||
header("Location: ".$_SERVER['PHP_SELF']."?year=0");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Download PDF
|
||||
if ($action == 'download') {
|
||||
$id = GETPOSTINT('id');
|
||||
|
||||
if ($statement->fetch($id) > 0) {
|
||||
$filepath = $statement->getFilePath();
|
||||
|
||||
if ($filepath && file_exists($filepath)) {
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: attachment; filename="'.basename($statement->filename).'"');
|
||||
header('Content-Length: '.filesize($filepath));
|
||||
header('Cache-Control: private');
|
||||
readfile($filepath);
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($langs->trans("FileNotFound"), null, 'errors');
|
||||
}
|
||||
} else {
|
||||
setEventMessages($langs->trans("RecordNotFound"), null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
// View PDF (inline)
|
||||
if ($action == 'view') {
|
||||
$id = GETPOSTINT('id');
|
||||
|
||||
if ($statement->fetch($id) > 0) {
|
||||
$filepath = $statement->getFilePath();
|
||||
|
||||
if ($filepath && file_exists($filepath)) {
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="'.basename($statement->filename).'"');
|
||||
header('Content-Length: '.filesize($filepath));
|
||||
header('Cache-Control: private');
|
||||
readfile($filepath);
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($langs->trans("FileNotFound"), null, 'errors');
|
||||
}
|
||||
} else {
|
||||
setEventMessages($langs->trans("RecordNotFound"), null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
// Reconcile single statement
|
||||
if ($action == 'reconcile') {
|
||||
if (!$user->hasRight('bankimport', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
$id = GETPOSTINT('id');
|
||||
$reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
|
||||
if (empty($reconcileBankAccountId)) {
|
||||
setEventMessages($langs->trans("ErrorNoBankAccountConfigured"), null, 'errors');
|
||||
} elseif ($statement->fetch($id) > 0) {
|
||||
// Parse statement lines if not yet done
|
||||
$existingLines = $statement->getStatementLines();
|
||||
if (is_array($existingLines) && empty($existingLines)) {
|
||||
$pdfLines = $statement->parsePdfTransactions();
|
||||
if (!empty($pdfLines)) {
|
||||
$statement->saveStatementLines($pdfLines);
|
||||
}
|
||||
}
|
||||
|
||||
$reconciledCount = $statement->reconcileBankEntries($user, $reconcileBankAccountId);
|
||||
if ($reconciledCount > 0) {
|
||||
setEventMessages($langs->trans("BankEntriesReconciled", $reconciledCount, $statement->statement_number.'/'.$statement->statement_year), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("NoBankEntriesToReconcile"), null, 'warnings');
|
||||
}
|
||||
}
|
||||
$action = '';
|
||||
}
|
||||
|
||||
// Reconcile all statements
|
||||
if ($action == 'reconcileall') {
|
||||
if (!$user->hasRight('bankimport', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
$reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
|
||||
if (empty($reconcileBankAccountId)) {
|
||||
setEventMessages($langs->trans("ErrorNoBankAccountConfigured"), null, 'errors');
|
||||
} else {
|
||||
$allStatements = $statement->fetchAll('statement_year,statement_number', 'ASC', 0, 0, array());
|
||||
$totalReconciled = 0;
|
||||
$stmtCount = 0;
|
||||
|
||||
if (is_array($allStatements)) {
|
||||
foreach ($allStatements as $stmt) {
|
||||
// Parse statement lines if not yet done
|
||||
$existingLines = $stmt->getStatementLines();
|
||||
if (is_array($existingLines) && empty($existingLines)) {
|
||||
$pdfLines = $stmt->parsePdfTransactions();
|
||||
if (!empty($pdfLines)) {
|
||||
$stmt->saveStatementLines($pdfLines);
|
||||
}
|
||||
}
|
||||
|
||||
$count = $stmt->reconcileBankEntries($user, $reconcileBankAccountId);
|
||||
if ($count > 0) {
|
||||
$totalReconciled += $count;
|
||||
$stmtCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($totalReconciled > 0) {
|
||||
setEventMessages($langs->trans("BankEntriesReconciledTotal", $totalReconciled, $stmtCount), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("NoBankEntriesToReconcile"), null, 'warnings');
|
||||
}
|
||||
}
|
||||
$action = '';
|
||||
}
|
||||
|
||||
// Confirm a pending reconciliation match
|
||||
if ($action == 'confirmreconcile') {
|
||||
if (!$user->hasRight('bankimport', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
$lineId = GETPOSTINT('lineid');
|
||||
$bankId = GETPOSTINT('bankid');
|
||||
$reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
|
||||
if ($lineId > 0 && $bankId > 0 && $reconcileBankAccountId > 0) {
|
||||
require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
|
||||
|
||||
// Get statement info from line
|
||||
$sqlLine = "SELECT sl.fk_statement, s.statement_number, s.statement_year";
|
||||
$sqlLine .= " FROM ".MAIN_DB_PREFIX."bankimport_statement_line sl";
|
||||
$sqlLine .= " JOIN ".MAIN_DB_PREFIX."bankimport_statement s ON s.rowid = sl.fk_statement";
|
||||
$sqlLine .= " WHERE sl.rowid = ".((int) $lineId);
|
||||
$resLine = $db->query($sqlLine);
|
||||
|
||||
if ($resLine && $db->num_rows($resLine) > 0) {
|
||||
$lineObj = $db->fetch_object($resLine);
|
||||
$numReleve = $lineObj->statement_number.'/'.$lineObj->statement_year;
|
||||
|
||||
// Reconcile the bank entry
|
||||
$bankLine = new AccountLine($db);
|
||||
$bankLine->fetch($bankId);
|
||||
$bankLine->num_releve = $numReleve;
|
||||
|
||||
$result = $bankLine->update_conciliation($user, 0, 1);
|
||||
if ($result >= 0) {
|
||||
// Update statement line status
|
||||
$sqlUpd = "UPDATE ".MAIN_DB_PREFIX."bankimport_statement_line SET";
|
||||
$sqlUpd .= " match_status = 'reconciled'";
|
||||
$sqlUpd .= " WHERE rowid = ".((int) $lineId);
|
||||
$db->query($sqlUpd);
|
||||
|
||||
setEventMessages($langs->trans("ReconciliationConfirmed"), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("Error"), null, 'errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
$action = '';
|
||||
}
|
||||
|
||||
// Delete confirmation
|
||||
if ($action == 'delete' && $confirm == 'yes') {
|
||||
$id = GETPOSTINT('id');
|
||||
|
||||
if ($statement->fetch($id) > 0) {
|
||||
$result = $statement->delete($user);
|
||||
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans("RecordDeleted"), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($statement->error, null, 'errors');
|
||||
}
|
||||
}
|
||||
$action = '';
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
$title = $langs->trans("PDFStatements");
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-bankimport page-pdfstatements');
|
||||
|
||||
print load_fiche_titre($title, '', 'bank');
|
||||
|
||||
// Reminder: check if statements are outdated
|
||||
$reminderEnabled = getDolGlobalString('BANKIMPORT_REMINDER_ENABLED', '1');
|
||||
if ($reminderEnabled) {
|
||||
$reminderMonths = getDolGlobalInt('BANKIMPORT_REMINDER_MONTHS') ?: 3;
|
||||
$lastEndDate = $statement->getLatestStatementEndDate();
|
||||
$thresholdDate = dol_time_plus_duree(dol_now(), -$reminderMonths, 'm');
|
||||
|
||||
if ($lastEndDate === null) {
|
||||
print '<div class="warning">';
|
||||
print img_warning().' '.$langs->trans("ReminderNoStatements");
|
||||
print '</div><br>';
|
||||
} elseif ($lastEndDate < $thresholdDate) {
|
||||
$monthsAgo = (int) round((dol_now() - $lastEndDate) / (30 * 24 * 3600));
|
||||
print '<div class="warning">';
|
||||
print img_warning().' '.$langs->trans("ReminderOutdatedStatements", dol_print_date($lastEndDate, 'day'), $monthsAgo);
|
||||
print '</div><br>';
|
||||
}
|
||||
}
|
||||
|
||||
// Info box
|
||||
print '<div class="info" style="margin-bottom: 15px;">';
|
||||
print '<strong>'.$langs->trans("PDFStatementsInfo").'</strong><br>';
|
||||
print $langs->trans("PDFStatementsInfoDesc");
|
||||
print '</div>';
|
||||
|
||||
// Delete confirmation dialog
|
||||
if ($action == 'delete') {
|
||||
$id = GETPOSTINT('id');
|
||||
$stmt = new BankImportStatement($db);
|
||||
$stmt->fetch($id);
|
||||
|
||||
$formconfirm = $form->formconfirm(
|
||||
$_SERVER["PHP_SELF"].'?id='.$id.'&year='.$year,
|
||||
$langs->trans('DeleteStatement'),
|
||||
$langs->trans('ConfirmDeleteStatement', $stmt->statement_number.'/'.$stmt->statement_year),
|
||||
'delete',
|
||||
'',
|
||||
0,
|
||||
1
|
||||
);
|
||||
print $formconfirm;
|
||||
}
|
||||
|
||||
// Upload form
|
||||
$defaultMode = getDolGlobalString('BANKIMPORT_UPLOAD_MODE') ?: 'auto';
|
||||
$uploadMode = GETPOST('upload_mode', 'alpha') ?: $defaultMode;
|
||||
|
||||
print '<div class="fichecenter">';
|
||||
print '<div class="fichehalfleft">';
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" enctype="multipart/form-data" id="uploadform">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="upload">';
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans("UploadPDFStatement").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Upload mode selection
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="titlefield">'.$langs->trans("UploadMode").'</td>';
|
||||
print '<td>';
|
||||
print '<label style="margin-right: 15px;"><input type="radio" name="upload_mode" value="auto" id="mode_auto"'.($uploadMode == 'auto' ? ' checked' : '').' onchange="toggleUploadMode()"> '.$langs->trans("UploadModeAuto").'</label>';
|
||||
print '<label><input type="radio" name="upload_mode" value="manual" id="mode_manual"'.($uploadMode == 'manual' ? ' checked' : '').' onchange="toggleUploadMode()"> '.$langs->trans("UploadModeManual").'</label>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// PDF file (always visible, multiple in auto mode)
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="fieldrequired">'.$langs->trans("File").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="file" name="pdffile[]" id="pdffile_input" accept=".pdf,application/pdf" multiple required>';
|
||||
print '<br><span class="opacitymedium small" id="multi_hint">'.$langs->trans("MultipleFilesHint").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// --- Manual fields (hidden when auto mode) ---
|
||||
|
||||
// IBAN
|
||||
print '<tr class="oddeven manual-field">';
|
||||
print '<td>'.$langs->trans("IBAN").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" class="flat minwidth200" name="iban" value="'.dol_escape_htmltag(GETPOST('iban', 'alpha')).'" placeholder="DE89 3704 0044 0532 0130 00">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Year
|
||||
print '<tr class="oddeven manual-field">';
|
||||
print '<td class="fieldrequired">'.$langs->trans("Year").'</td>';
|
||||
print '<td>';
|
||||
$years = array();
|
||||
for ($y = (int) date('Y'); $y >= ((int) date('Y') - 10); $y--) {
|
||||
$years[$y] = $y;
|
||||
}
|
||||
print $form->selectarray('statement_year', $years, GETPOSTISSET('statement_year') ? GETPOSTINT('statement_year') : $year, 0, 0, 0, '', 0, 0, 0, '', 'minwidth100');
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Statement number
|
||||
print '<tr class="oddeven manual-field">';
|
||||
print '<td class="fieldrequired">'.$langs->trans("StatementNumber").'</td>';
|
||||
print '<td>';
|
||||
$nextNum = $statement->getNextStatementNumber($year);
|
||||
print '<input type="text" class="flat width75" name="statement_number" value="'.dol_escape_htmltag(GETPOSTISSET('statement_number') ? GETPOST('statement_number', 'alpha') : '').'">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Statement date
|
||||
print '<tr class="oddeven manual-field">';
|
||||
print '<td>'.$langs->trans("StatementDate").'</td>';
|
||||
print '<td>';
|
||||
print $form->selectDate(GETPOSTISSET('statement_dateday') ? dol_mktime(0, 0, 0, GETPOSTINT('statement_datemonth'), GETPOSTINT('statement_dateday'), GETPOSTINT('statement_dateyear')) : -1, 'statement_date', 0, 0, 1, '', 1, 0);
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Period from
|
||||
print '<tr class="oddeven manual-field">';
|
||||
print '<td>'.$langs->trans("DateFrom").'</td>';
|
||||
print '<td>';
|
||||
print $form->selectDate(GETPOSTISSET('date_fromday') ? dol_mktime(0, 0, 0, GETPOSTINT('date_frommonth'), GETPOSTINT('date_fromday'), GETPOSTINT('date_fromyear')) : -1, 'date_from', 0, 0, 1, '', 1, 0);
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Period to
|
||||
print '<tr class="oddeven manual-field">';
|
||||
print '<td>'.$langs->trans("DateTo").'</td>';
|
||||
print '<td>';
|
||||
print $form->selectDate(GETPOSTISSET('date_today') ? dol_mktime(0, 0, 0, GETPOSTINT('date_tomonth'), GETPOSTINT('date_today'), GETPOSTINT('date_toyear')) : -1, 'date_to', 0, 0, 1, '', 1, 0);
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Opening balance
|
||||
print '<tr class="oddeven manual-field">';
|
||||
print '<td>'.$langs->trans("OpeningBalance").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" class="flat width100" name="opening_balance" value="'.dol_escape_htmltag(GETPOST('opening_balance', 'alpha')).'" placeholder="1.234,56">';
|
||||
print ' EUR';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Closing balance
|
||||
print '<tr class="oddeven manual-field">';
|
||||
print '<td>'.$langs->trans("ClosingBalance").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="text" class="flat width100" name="closing_balance" value="'.dol_escape_htmltag(GETPOST('closing_balance', 'alpha')).'" placeholder="1.345,67">';
|
||||
print ' EUR';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print '<div class="center" style="margin-top: 10px;">';
|
||||
print '<input type="submit" class="button button-save" value="'.$langs->trans("Upload").'">';
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
// JavaScript for toggling upload modes
|
||||
print '<script type="text/javascript">
|
||||
function toggleUploadMode() {
|
||||
var isManual = document.getElementById("mode_manual").checked;
|
||||
var manualFields = document.querySelectorAll(".manual-field");
|
||||
var fileInput = document.getElementById("pdffile_input");
|
||||
var multiHint = document.getElementById("multi_hint");
|
||||
for (var i = 0; i < manualFields.length; i++) {
|
||||
manualFields[i].style.display = isManual ? "" : "none";
|
||||
}
|
||||
// In manual mode: single file only. In auto mode: multiple files allowed
|
||||
if (isManual) {
|
||||
fileInput.removeAttribute("multiple");
|
||||
multiHint.style.display = "none";
|
||||
} else {
|
||||
fileInput.setAttribute("multiple", "multiple");
|
||||
multiHint.style.display = "";
|
||||
}
|
||||
}
|
||||
// Initial state
|
||||
document.addEventListener("DOMContentLoaded", function() { toggleUploadMode(); });
|
||||
</script>';
|
||||
|
||||
print '</div>'; // fichehalfleft
|
||||
print '</div>'; // fichecenter
|
||||
|
||||
print '<div class="clearboth"></div><br>';
|
||||
|
||||
// Year filter for list - only show years that have statements
|
||||
$yearsFilter = array(0 => $langs->trans("AllStatements"));
|
||||
$availableYears = $statement->getAvailableYears();
|
||||
foreach ($availableYears as $yKey => $yVal) {
|
||||
$yearsFilter[$yKey] = $yVal;
|
||||
}
|
||||
// If current year not in list, add it
|
||||
if (!isset($yearsFilter[(int) date('Y')])) {
|
||||
$yearsFilter[(int) date('Y')] = (string) date('Y');
|
||||
krsort($yearsFilter);
|
||||
}
|
||||
print '<form method="GET" action="'.$_SERVER["PHP_SELF"].'">';
|
||||
print '<div class="center" style="margin-bottom: 15px;">';
|
||||
print '<strong>'.$langs->trans("Year").':</strong> ';
|
||||
print $form->selectarray('year', $yearsFilter, $year, 0, 0, 0, '', 0, 0, 0, '', 'minwidth100');
|
||||
print ' <input type="submit" class="button smallpaddingimp" value="'.$langs->trans("Filter").'">';
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
// Reconcile All button
|
||||
$reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||
if (!empty($reconcileBankAccountId)) {
|
||||
print '<div class="right" style="margin-bottom: 10px;">';
|
||||
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=reconcileall&year='.$year.'&token='.newToken().'">';
|
||||
print img_picto('', 'bank', 'class="pictofixedwidth"').$langs->trans("ReconcileAllStatements");
|
||||
print '</a>';
|
||||
print '</div>';
|
||||
} else {
|
||||
print '<div class="warning" style="margin-bottom: 10px;">';
|
||||
print img_warning().' '.$langs->trans("NoBankAccountConfigured");
|
||||
print ' <a href="'.dol_buildpath('/bankimport/admin/setup.php', 1).'">'.$langs->trans("GoToSetup").'</a>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// List of existing PDF statements
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th class="center" width="80">'.$langs->trans("StatementNumber").'</th>';
|
||||
print '<th>'.$langs->trans("IBAN").'</th>';
|
||||
print '<th class="center">'.$langs->trans("StatementDate").'</th>';
|
||||
print '<th class="center">'.$langs->trans("Period").'</th>';
|
||||
print '<th class="right">'.$langs->trans("OpeningBalance").'</th>';
|
||||
print '<th class="right">'.$langs->trans("ClosingBalance").'</th>';
|
||||
print '<th class="right">'.$langs->trans("Size").'</th>';
|
||||
print '<th class="center">'.$langs->trans("DateCreation").'</th>';
|
||||
print '<th class="center" width="200">'.$langs->trans("Actions").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
$filter = array();
|
||||
if ($year > 0) {
|
||||
$filter['year'] = $year;
|
||||
}
|
||||
$records = $statement->fetchAll('statement_year,statement_number', 'DESC', 100, 0, $filter);
|
||||
|
||||
if (is_array($records) && count($records) > 0) {
|
||||
foreach ($records as $obj) {
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Statement number
|
||||
print '<td class="center nowraponall">';
|
||||
print '<strong>'.dol_escape_htmltag($obj->statement_number).'</strong>/'.$obj->statement_year;
|
||||
print '</td>';
|
||||
|
||||
// IBAN
|
||||
print '<td>';
|
||||
if ($obj->iban) {
|
||||
print dol_escape_htmltag($obj->iban);
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Statement date
|
||||
print '<td class="center">';
|
||||
if ($obj->statement_date) {
|
||||
print dol_print_date($obj->statement_date, 'day');
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Period
|
||||
print '<td class="center nowraponall">';
|
||||
if ($obj->date_from && $obj->date_to) {
|
||||
print dol_print_date($obj->date_from, 'day').' - '.dol_print_date($obj->date_to, 'day');
|
||||
} elseif ($obj->date_from) {
|
||||
print $langs->trans("From").' '.dol_print_date($obj->date_from, 'day');
|
||||
} elseif ($obj->date_to) {
|
||||
print $langs->trans("To").' '.dol_print_date($obj->date_to, 'day');
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Opening balance
|
||||
print '<td class="right nowraponall">';
|
||||
if ($obj->opening_balance !== null) {
|
||||
$color = $obj->opening_balance >= 0 ? '' : 'color: red;';
|
||||
print '<span style="'.$color.'">'.price($obj->opening_balance, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Closing balance
|
||||
print '<td class="right nowraponall">';
|
||||
if ($obj->closing_balance !== null) {
|
||||
$color = $obj->closing_balance >= 0 ? '' : 'color: red;';
|
||||
print '<span style="'.$color.'">'.price($obj->closing_balance, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Size
|
||||
print '<td class="right">';
|
||||
if ($obj->filesize) {
|
||||
print dol_print_size($obj->filesize, 1);
|
||||
} else {
|
||||
print '-';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Creation date
|
||||
print '<td class="center nowraponall">';
|
||||
print dol_print_date($obj->datec, 'day');
|
||||
print '</td>';
|
||||
|
||||
// Actions
|
||||
print '<td class="center nowraponall" style="white-space: nowrap;">';
|
||||
if ($obj->filepath && file_exists($obj->filepath)) {
|
||||
// View (inline)
|
||||
print '<a style="margin: 0 6px;" href="'.$_SERVER["PHP_SELF"].'?action=view&id='.$obj->id.'&token='.newToken().'" target="_blank" title="'.$langs->trans("View").'">';
|
||||
print img_picto($langs->trans("View"), 'eye');
|
||||
print '</a>';
|
||||
|
||||
// Download
|
||||
print '<a style="margin: 0 6px;" href="'.$_SERVER["PHP_SELF"].'?action=download&id='.$obj->id.'&token='.newToken().'" title="'.$langs->trans("Download").'">';
|
||||
print img_picto($langs->trans("Download"), 'download');
|
||||
print '</a>';
|
||||
}
|
||||
|
||||
// Reconcile
|
||||
if (!empty($reconcileBankAccountId) && $obj->date_from && $obj->date_to) {
|
||||
print '<a style="margin: 0 6px;" href="'.$_SERVER["PHP_SELF"].'?action=reconcile&id='.$obj->id.'&year='.$year.'&token='.newToken().'" title="'.$langs->trans("ReconcileStatement").'">';
|
||||
print img_picto($langs->trans("ReconcileStatement"), 'bank');
|
||||
print '</a>';
|
||||
}
|
||||
|
||||
// Delete
|
||||
print '<a style="margin: 0 6px;" href="'.$_SERVER["PHP_SELF"].'?action=delete&id='.$obj->id.'&year='.$year.'&token='.newToken().'" title="'.$langs->trans("Delete").'">';
|
||||
print img_picto($langs->trans("Delete"), 'delete');
|
||||
print '</a>';
|
||||
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="9" class="opacitymedium center">';
|
||||
print $langs->trans("NoPDFStatementsFound");
|
||||
print '</td></tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
// Pending review matches
|
||||
$sqlPending = "SELECT sl.rowid as line_id, sl.fk_statement, sl.line_number, sl.date_booking, sl.amount as stmt_amount,";
|
||||
$sqlPending .= " sl.name as stmt_name, sl.fk_bank,";
|
||||
$sqlPending .= " b.rowid as bank_id, b.datev, b.amount as bank_amount, b.label as bank_label,";
|
||||
$sqlPending .= " s.statement_number, s.statement_year";
|
||||
$sqlPending .= " FROM ".MAIN_DB_PREFIX."bankimport_statement_line sl";
|
||||
$sqlPending .= " JOIN ".MAIN_DB_PREFIX."bankimport_statement s ON s.rowid = sl.fk_statement";
|
||||
$sqlPending .= " JOIN ".MAIN_DB_PREFIX."bank b ON b.rowid = sl.fk_bank";
|
||||
$sqlPending .= " WHERE sl.match_status = 'pending_review'";
|
||||
$sqlPending .= " AND sl.entity = ".((int) $conf->entity);
|
||||
$sqlPending .= " ORDER BY s.statement_year, s.statement_number, sl.line_number";
|
||||
|
||||
$resPending = $db->query($sqlPending);
|
||||
if ($resPending && $db->num_rows($resPending) > 0) {
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="8">';
|
||||
print img_warning().' <strong>'.$langs->trans("PendingReconciliationMatches").'</strong>';
|
||||
print ' - '.$langs->trans("PendingReconciliationMatchesDesc");
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th class="center">'.$langs->trans("StatementNumber").'</th>';
|
||||
print '<th class="center">'.$langs->trans("BookingDate").'</th>';
|
||||
print '<th>'.$langs->trans("Name").'</th>';
|
||||
print '<th class="right">'.$langs->trans("AmountStatement").'</th>';
|
||||
print '<th class="right">'.$langs->trans("AmountDolibarr").'</th>';
|
||||
print '<th class="right">'.$langs->trans("Difference").'</th>';
|
||||
print '<th class="center">'.$langs->trans("BankEntry").'</th>';
|
||||
print '<th class="center">'.$langs->trans("Action").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
while ($pendObj = $db->fetch_object($resPending)) {
|
||||
$diff = abs(abs((float) $pendObj->stmt_amount) - abs((float) $pendObj->bank_amount));
|
||||
$diffColor = ($diff > 10) ? 'color: red; font-weight: bold;' : 'color: #e68a00;';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Statement number
|
||||
print '<td class="center nowraponall">'.$pendObj->statement_number.'/'.$pendObj->statement_year.'</td>';
|
||||
|
||||
// Booking date
|
||||
print '<td class="center">'.dol_print_date($db->jdate($pendObj->date_booking), 'day').'</td>';
|
||||
|
||||
// Name
|
||||
print '<td>'.dol_escape_htmltag($pendObj->stmt_name).'</td>';
|
||||
|
||||
// Amount from PDF statement
|
||||
print '<td class="right nowraponall">';
|
||||
$stmtColor = $pendObj->stmt_amount >= 0 ? '' : 'color: red;';
|
||||
print '<span style="'.$stmtColor.'">'.price($pendObj->stmt_amount, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
print '</td>';
|
||||
|
||||
// Amount from Dolibarr bank
|
||||
print '<td class="right nowraponall">';
|
||||
$bankColor = $pendObj->bank_amount >= 0 ? '' : 'color: red;';
|
||||
print '<span style="'.$bankColor.'">'.price($pendObj->bank_amount, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
print '</td>';
|
||||
|
||||
// Difference
|
||||
print '<td class="right nowraponall">';
|
||||
print '<span style="'.$diffColor.'">'.price($diff, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
print '</td>';
|
||||
|
||||
// Bank entry link
|
||||
print '<td class="center">';
|
||||
print '<a href="'.DOL_URL_ROOT.'/compta/bank/line.php?rowid='.$pendObj->bank_id.'" target="_blank">#'.$pendObj->bank_id.'</a>';
|
||||
print '</td>';
|
||||
|
||||
// Action: confirm button
|
||||
print '<td class="center nowraponall">';
|
||||
print '<a class="butAction butActionSmall" href="'.$_SERVER["PHP_SELF"].'?action=confirmreconcile&lineid='.$pendObj->line_id.'&bankid='.$pendObj->bank_id.'&year='.$year.'&token='.newToken().'">';
|
||||
print $langs->trans("Confirm");
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
}
|
||||
$db->free($resPending);
|
||||
|
||||
// Statistics
|
||||
$totalCount = $statement->fetchAll('', '', 0, 0, array(), 'count');
|
||||
$yearCount = is_array($records) ? count($records) : 0;
|
||||
|
||||
print '<div class="opacitymedium" style="margin-top: 10px;">';
|
||||
if ($year > 0) {
|
||||
print $langs->trans("Total").': <strong>'.$yearCount.'</strong> '.$langs->trans("StatementsInYear", $year);
|
||||
print ' | '.$langs->trans("AllStatements").': <strong>'.$totalCount.'</strong>';
|
||||
} else {
|
||||
print $langs->trans("Total").': <strong>'.$totalCount.'</strong> '.$langs->trans("AllStatements");
|
||||
}
|
||||
print '</div>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
34
sql/dolibarr_allversions.sql
Executable file
34
sql/dolibarr_allversions.sql
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
--
|
||||
-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version.
|
||||
--
|
||||
|
||||
-- v1.1: Add fk_statement to transaction table
|
||||
ALTER TABLE llx_bankimport_transaction ADD COLUMN fk_statement INTEGER AFTER fk_societe;
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_statement (fk_statement);
|
||||
|
||||
-- v1.3: Add statement_line table for parsed PDF transactions
|
||||
CREATE TABLE IF NOT EXISTS llx_bankimport_statement_line (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_statement INTEGER NOT NULL,
|
||||
entity INTEGER DEFAULT 1 NOT NULL,
|
||||
line_number INTEGER DEFAULT 0,
|
||||
date_booking DATE NOT NULL,
|
||||
date_value DATE,
|
||||
transaction_type VARCHAR(100),
|
||||
amount DOUBLE(24,8) NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'EUR',
|
||||
name VARCHAR(255),
|
||||
description TEXT,
|
||||
fk_bank INTEGER,
|
||||
datec DATETIME,
|
||||
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_fk_statement (fk_statement);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_entity (entity);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_date_booking (date_booking);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_amount (amount);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_fk_bank (fk_bank);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD UNIQUE INDEX uk_bankimport_stmtline (fk_statement, line_number, entity);
|
||||
|
||||
-- v1.4: Add match_status to statement_line for approval workflow
|
||||
ALTER TABLE llx_bankimport_statement_line ADD COLUMN match_status VARCHAR(20) DEFAULT NULL AFTER fk_bank;
|
||||
14
sql/llx_bankimport_statement.key.sql
Executable file
14
sql/llx_bankimport_statement.key.sql
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
-- 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.
|
||||
|
||||
-- Indexes for llx_bankimport_statement
|
||||
|
||||
ALTER TABLE llx_bankimport_statement ADD UNIQUE INDEX uk_bankimport_statement (statement_number, statement_year, iban, entity);
|
||||
ALTER TABLE llx_bankimport_statement ADD INDEX idx_bankimport_statement_entity (entity);
|
||||
ALTER TABLE llx_bankimport_statement ADD INDEX idx_bankimport_statement_iban (iban);
|
||||
ALTER TABLE llx_bankimport_statement ADD INDEX idx_bankimport_statement_year (statement_year);
|
||||
ALTER TABLE llx_bankimport_statement ADD INDEX idx_bankimport_statement_date (statement_date);
|
||||
30
sql/llx_bankimport_statement.sql
Executable file
30
sql/llx_bankimport_statement.sql
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
-- 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.
|
||||
|
||||
-- Table for PDF bank statements
|
||||
|
||||
CREATE TABLE llx_bankimport_statement (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
entity INTEGER DEFAULT 1 NOT NULL,
|
||||
iban VARCHAR(34),
|
||||
statement_number VARCHAR(20) NOT NULL,
|
||||
statement_year INTEGER NOT NULL,
|
||||
statement_date DATE,
|
||||
date_from DATE,
|
||||
date_to DATE,
|
||||
opening_balance DOUBLE(24,8),
|
||||
closing_balance DOUBLE(24,8),
|
||||
currency VARCHAR(3) DEFAULT 'EUR',
|
||||
filename VARCHAR(255),
|
||||
filepath VARCHAR(512),
|
||||
filesize INTEGER,
|
||||
import_key VARCHAR(50),
|
||||
datec DATETIME,
|
||||
fk_user_creat INTEGER,
|
||||
note_private TEXT,
|
||||
note_public TEXT
|
||||
) ENGINE=InnoDB;
|
||||
15
sql/llx_bankimport_statement_line.key.sql
Executable file
15
sql/llx_bankimport_statement_line.key.sql
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
-- 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.
|
||||
|
||||
-- Indexes for llx_bankimport_statement_line
|
||||
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_fk_statement (fk_statement);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_entity (entity);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_date_booking (date_booking);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_amount (amount);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_fk_bank (fk_bank);
|
||||
ALTER TABLE llx_bankimport_statement_line ADD UNIQUE INDEX uk_bankimport_stmtline (fk_statement, line_number, entity);
|
||||
34
sql/llx_bankimport_statement_line.sql
Executable file
34
sql/llx_bankimport_statement_line.sql
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
-- 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.
|
||||
|
||||
-- Table for individual transaction lines parsed from PDF bank statements
|
||||
|
||||
CREATE TABLE llx_bankimport_statement_line (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_statement INTEGER NOT NULL, -- Link to llx_bankimport_statement
|
||||
entity INTEGER DEFAULT 1 NOT NULL,
|
||||
line_number INTEGER DEFAULT 0, -- Position within statement (1, 2, 3...)
|
||||
|
||||
-- Transaction data from PDF
|
||||
date_booking DATE NOT NULL, -- Buchungstag (Bu-Tag)
|
||||
date_value DATE, -- Wertstellungstag (Wert)
|
||||
transaction_type VARCHAR(100), -- Vorgangsart (e.g. Überweisungsgutschr., Basislastschrift)
|
||||
amount DOUBLE(24,8) NOT NULL, -- Amount (positive = credit, negative = debit)
|
||||
currency VARCHAR(3) DEFAULT 'EUR',
|
||||
|
||||
-- Counterparty and description
|
||||
name VARCHAR(255), -- Counterparty name (first detail line)
|
||||
description TEXT, -- Full description (all detail lines)
|
||||
|
||||
-- Matching
|
||||
fk_bank INTEGER, -- Link to llx_bank when reconciled
|
||||
match_status VARCHAR(20) DEFAULT NULL, -- NULL=unmatched, reconciled=auto, pending_review=needs approval
|
||||
|
||||
-- Timestamps
|
||||
datec DATETIME,
|
||||
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
28
sql/llx_bankimport_transaction.key.sql
Executable file
28
sql/llx_bankimport_transaction.key.sql
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
-- 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.
|
||||
|
||||
-- Indexes for llx_bankimport_transaction
|
||||
|
||||
ALTER TABLE llx_bankimport_transaction ADD UNIQUE INDEX uk_bankimport_transaction_ref (ref, entity);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_entity (entity);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_iban (iban);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_date_trans (date_trans);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_name (name);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_amount (amount);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_status (status);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_bank (fk_bank);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_facture (fk_facture);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_facture_fourn (fk_facture_fourn);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_societe (fk_societe);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_import_key (import_key);
|
||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_statement (fk_statement);
|
||||
|
||||
-- Foreign keys (optional, depends on your setup)
|
||||
-- ALTER TABLE llx_bankimport_transaction ADD CONSTRAINT fk_bankimport_transaction_fk_bank FOREIGN KEY (fk_bank) REFERENCES llx_bank(rowid);
|
||||
-- ALTER TABLE llx_bankimport_transaction ADD CONSTRAINT fk_bankimport_transaction_fk_facture FOREIGN KEY (fk_facture) REFERENCES llx_facture(rowid);
|
||||
-- ALTER TABLE llx_bankimport_transaction ADD CONSTRAINT fk_bankimport_transaction_fk_facture_fourn FOREIGN KEY (fk_facture_fourn) REFERENCES llx_facture_fourn(rowid);
|
||||
-- ALTER TABLE llx_bankimport_transaction ADD CONSTRAINT fk_bankimport_transaction_fk_societe FOREIGN KEY (fk_societe) REFERENCES llx_societe(rowid);
|
||||
62
sql/llx_bankimport_transaction.sql
Executable file
62
sql/llx_bankimport_transaction.sql
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
-- 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.
|
||||
|
||||
CREATE TABLE llx_bankimport_transaction (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
ref VARCHAR(128) NOT NULL, -- Unique reference (hash of transaction)
|
||||
entity INTEGER DEFAULT 1 NOT NULL, -- Multi-company id
|
||||
|
||||
-- Bank account info
|
||||
iban VARCHAR(34), -- IBAN of the account
|
||||
bic VARCHAR(11), -- BIC of the bank
|
||||
|
||||
-- Transaction data
|
||||
datec DATETIME, -- Creation date in system
|
||||
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
date_trans DATE NOT NULL, -- Transaction date (booking date)
|
||||
date_value DATE, -- Value date
|
||||
|
||||
-- Counterparty
|
||||
name VARCHAR(255), -- Counterparty name
|
||||
counterparty_iban VARCHAR(34), -- Counterparty IBAN
|
||||
counterparty_bic VARCHAR(11), -- Counterparty BIC
|
||||
|
||||
-- Amount
|
||||
amount DOUBLE(24,8) NOT NULL, -- Amount (positive = credit, negative = debit)
|
||||
currency VARCHAR(3) DEFAULT 'EUR', -- Currency code
|
||||
|
||||
-- Description
|
||||
label VARCHAR(255), -- Short label/booking text
|
||||
description TEXT, -- Full description/reference
|
||||
end_to_end_id VARCHAR(128), -- End-to-end ID from SEPA
|
||||
mandate_id VARCHAR(128), -- Mandate ID for direct debits
|
||||
|
||||
-- Matching with Dolibarr
|
||||
fk_bank INTEGER, -- Link to llx_bank (bank line)
|
||||
fk_facture INTEGER, -- Link to llx_facture (customer invoice)
|
||||
fk_facture_fourn INTEGER, -- Link to llx_facture_fourn (supplier invoice)
|
||||
fk_paiement INTEGER, -- Link to llx_paiement
|
||||
fk_paiementfourn INTEGER, -- Link to llx_paiementfourn
|
||||
fk_salary INTEGER, -- Link to llx_salary (salary payment)
|
||||
fk_don INTEGER, -- Link to llx_don (donation)
|
||||
fk_loan INTEGER, -- Link to llx_loan (loan payment)
|
||||
fk_societe INTEGER, -- Link to llx_societe (third party)
|
||||
fk_statement INTEGER, -- Link to llx_bankimport_statement (PDF statement)
|
||||
|
||||
-- Status
|
||||
status SMALLINT DEFAULT 0, -- 0=new, 1=matched, 2=reconciled, 9=ignored
|
||||
import_key VARCHAR(64), -- Import batch key
|
||||
|
||||
-- User tracking
|
||||
fk_user_creat INTEGER, -- User who imported
|
||||
fk_user_modif INTEGER, -- User who last modified
|
||||
fk_user_match INTEGER, -- User who matched/reconciled
|
||||
date_match DATETIME, -- Date of matching
|
||||
|
||||
note_private TEXT, -- Private notes
|
||||
note_public TEXT -- Public notes
|
||||
) ENGINE=InnoDB;
|
||||
708
statements.php
Executable file
708
statements.php
Executable file
|
|
@ -0,0 +1,708 @@
|
|||
<?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 bankimport/statements.php
|
||||
* \ingroup bankimport
|
||||
* \brief Page to fetch and display bank statements via FinTS
|
||||
*/
|
||||
|
||||
// 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/lib/date.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
|
||||
dol_include_once('/bankimport/class/fints.class.php');
|
||||
dol_include_once('/bankimport/class/banktransaction.class.php');
|
||||
dol_include_once('/bankimport/class/bankimportcron.class.php');
|
||||
dol_include_once('/bankimport/lib/bankimport.lib.php');
|
||||
|
||||
/**
|
||||
* @var Conf $conf
|
||||
* @var DoliDB $db
|
||||
* @var Translate $langs
|
||||
* @var User $user
|
||||
*/
|
||||
|
||||
$langs->loadLangs(array("bankimport@bankimport", "banks"));
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('bankimport', 'write')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Date filters
|
||||
$date_fromday = GETPOSTINT('date_fromday');
|
||||
$date_frommonth = GETPOSTINT('date_frommonth');
|
||||
$date_fromyear = GETPOSTINT('date_fromyear');
|
||||
$date_today = GETPOSTINT('date_today');
|
||||
$date_tomonth = GETPOSTINT('date_tomonth');
|
||||
$date_toyear = GETPOSTINT('date_toyear');
|
||||
|
||||
$dateFrom = 0;
|
||||
$dateTo = 0;
|
||||
|
||||
if ($date_fromday && $date_frommonth && $date_fromyear) {
|
||||
$dateFrom = dol_mktime(0, 0, 0, $date_frommonth, $date_fromday, $date_fromyear);
|
||||
}
|
||||
if ($date_today && $date_tomonth && $date_toyear) {
|
||||
$dateTo = dol_mktime(23, 59, 59, $date_tomonth, $date_today, $date_toyear);
|
||||
}
|
||||
|
||||
// Default: last 30 days
|
||||
if (empty($dateFrom)) {
|
||||
$dateFrom = dol_time_plus_duree(dol_now(), -30, 'd');
|
||||
}
|
||||
if (empty($dateTo)) {
|
||||
$dateTo = dol_now();
|
||||
}
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
$transactions = array();
|
||||
$balance = array();
|
||||
$error = 0;
|
||||
$tanRequired = false;
|
||||
$tanChallenge = '';
|
||||
$importResult = null;
|
||||
|
||||
// Import transactions to database
|
||||
if ($action == 'import' && !empty($_SESSION['fints_transactions'])) {
|
||||
$fints = new BankImportFinTS($db);
|
||||
$iban = $fints->getIban();
|
||||
|
||||
$transImporter = new BankImportTransaction($db);
|
||||
$importResult = $transImporter->importFromFinTS($_SESSION['fints_transactions'], $iban, $user);
|
||||
|
||||
if ($importResult['imported'] > 0) {
|
||||
setEventMessages($langs->trans("TransactionsImported", $importResult['imported']), null, 'mesgs');
|
||||
}
|
||||
if ($importResult['skipped'] > 0) {
|
||||
setEventMessages($langs->trans("TransactionsSkipped", $importResult['skipped']), null, 'warnings');
|
||||
}
|
||||
if (!empty($importResult['errors'])) {
|
||||
setEventMessages(implode('<br>', $importResult['errors']), null, 'errors');
|
||||
}
|
||||
|
||||
// Clear session after import
|
||||
unset($_SESSION['fints_transactions']);
|
||||
unset($_SESSION['fints_balance']);
|
||||
}
|
||||
|
||||
// Resume pending cron TAN
|
||||
if ($action == 'resumecron') {
|
||||
$cronJob = new BankImportCron($db);
|
||||
$result = $cronJob->resumePendingAction();
|
||||
|
||||
if ($result > 0 || $result == 0) {
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans("TANConfirmedImportComplete"), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("WaitingForSecureGoConfirmation"), null, 'warnings');
|
||||
}
|
||||
} else {
|
||||
setEventMessages($langs->trans("TANCheckFailed").': '.$cronJob->error, null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action == 'fetch') {
|
||||
$fints = new BankImportFinTS($db);
|
||||
|
||||
if (!$fints->isConfigured()) {
|
||||
setEventMessages($langs->trans("FinTSNotConfigured"), null, 'errors');
|
||||
$error++;
|
||||
} elseif (!$fints->isLibraryAvailable()) {
|
||||
setEventMessages($langs->trans("FinTSLibraryNotFound"), null, 'errors');
|
||||
$error++;
|
||||
} else {
|
||||
// Login first
|
||||
$loginResult = $fints->login();
|
||||
|
||||
if ($loginResult < 0) {
|
||||
setEventMessages($langs->trans("LoginFailed").': '.$fints->error, null, 'errors');
|
||||
$error++;
|
||||
} elseif ($loginResult == 0) {
|
||||
// TAN required
|
||||
$tanRequired = true;
|
||||
$tanChallenge = $fints->tanChallenge;
|
||||
|
||||
// Check if decoupled (SecureGo Plus)
|
||||
if ($fints->selectedTanMode && $fints->selectedTanMode->isDecoupled()) {
|
||||
setEventMessages($langs->trans("SecureGoPlusConfirmRequired"), null, 'warnings');
|
||||
// Store state in session for polling
|
||||
$_SESSION['fints_state'] = $fints->persist();
|
||||
$_SESSION['fints_action'] = 'login';
|
||||
} else {
|
||||
setEventMessages($langs->trans("TANRequired").': '.$tanChallenge, null, 'warnings');
|
||||
}
|
||||
} else {
|
||||
// Login successful - log bank parameters for diagnostics
|
||||
$bankParams = $fints->getBankParameters();
|
||||
dol_syslog("BankImport: Bank parameters: ".json_encode($bankParams), LOG_DEBUG);
|
||||
|
||||
// Check what statement methods are supported
|
||||
$hikazsVersions = $bankParams['HIKAZS'] ?? array();
|
||||
$hicazsVersions = $bankParams['HICAZS'] ?? array();
|
||||
$hiekaVersions = $bankParams['HIEKAS'] ?? array();
|
||||
dol_syslog("BankImport: HIKAZS versions: ".implode(',', $hikazsVersions)." | HICAZS: ".implode(',', $hicazsVersions)." | HIEKAS: ".implode(',', $hiekaVersions), LOG_DEBUG);
|
||||
|
||||
// Fetch statements
|
||||
$result = $fints->fetchStatements($dateFrom, $dateTo);
|
||||
|
||||
if ($result === 0) {
|
||||
// TAN required for statements
|
||||
$tanRequired = true;
|
||||
$tanChallenge = $fints->tanChallenge;
|
||||
// Store state in session for polling
|
||||
$_SESSION['fints_state'] = $fints->persist();
|
||||
$_SESSION['fints_pending_action'] = serialize($fints->getPendingAction());
|
||||
$_SESSION['fints_action'] = 'statements';
|
||||
$_SESSION['fints_datefrom'] = $dateFrom;
|
||||
$_SESSION['fints_dateto'] = $dateTo;
|
||||
setEventMessages($langs->trans("SecureGoPlusConfirmRequired"), null, 'warnings');
|
||||
} elseif ($result < 0) {
|
||||
setEventMessages($langs->trans("FetchFailed").': '.$fints->error, null, 'errors');
|
||||
// Show supported versions for diagnostics
|
||||
if (!empty($hikazsVersions) || !empty($hicazsVersions)) {
|
||||
$diagMsg = 'Bank unterstützt: HIKAZS v'.implode(',v', $hikazsVersions);
|
||||
if (!empty($hicazsVersions)) {
|
||||
$diagMsg .= ' | HICAZS v'.implode(',v', $hicazsVersions);
|
||||
}
|
||||
setEventMessages($diagMsg, null, 'warnings');
|
||||
}
|
||||
$error++;
|
||||
} elseif (is_array($result)) {
|
||||
$transactions = $result['transactions'] ?? array();
|
||||
$balance = $result['balance'] ?? array();
|
||||
$isPartial = $result['partial'] ?? false;
|
||||
$infoMsg = $result['info'] ?? '';
|
||||
|
||||
if (empty($transactions)) {
|
||||
setEventMessages($langs->trans("NoTransactionsFound"), null, 'warnings');
|
||||
} else {
|
||||
setEventMessages($langs->trans("TransactionsFound", count($transactions)), null, 'mesgs');
|
||||
// Store in session for import
|
||||
$_SESSION['fints_transactions'] = $transactions;
|
||||
$_SESSION['fints_balance'] = $balance;
|
||||
|
||||
// Show info about partial results (older data not available)
|
||||
if ($isPartial && !empty($infoMsg)) {
|
||||
setEventMessages($infoMsg, null, 'warnings');
|
||||
}
|
||||
}
|
||||
|
||||
// Save session state for cronjob before closing
|
||||
$cronState = $fints->persist();
|
||||
if (!empty($cronState)) {
|
||||
dolibarr_set_const($db, 'BANKIMPORT_CRON_STATE', $cronState, 'chaine', 0, '', $conf->entity);
|
||||
dol_syslog("BankImport: Saved session state for cronjob", LOG_DEBUG);
|
||||
}
|
||||
|
||||
// Only close on success
|
||||
$fints->close();
|
||||
}
|
||||
// NOTE: Don't call close() when TAN is required ($result === 0)
|
||||
// The connection must stay open for checkDecoupledTan()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check decoupled TAN status (SecureGo Plus polling)
|
||||
if ($action == 'checktan') {
|
||||
if (!empty($_SESSION['fints_state']) && !empty($_SESSION['fints_pending_action'])) {
|
||||
$fints = new BankImportFinTS($db);
|
||||
$fints->restore($_SESSION['fints_state']);
|
||||
$fints->setPendingAction(unserialize($_SESSION['fints_pending_action']));
|
||||
|
||||
$result = $fints->checkDecoupledTan();
|
||||
|
||||
if ($result > 0) {
|
||||
// TAN confirmed
|
||||
$savedAction = $_SESSION['fints_action'] ?? 'statements';
|
||||
$savedDateFrom = $_SESSION['fints_datefrom'] ?? $dateFrom;
|
||||
$savedDateTo = $_SESSION['fints_dateto'] ?? $dateTo;
|
||||
|
||||
unset($_SESSION['fints_state']);
|
||||
unset($_SESSION['fints_pending_action']);
|
||||
unset($_SESSION['fints_action']);
|
||||
unset($_SESSION['fints_datefrom']);
|
||||
unset($_SESSION['fints_dateto']);
|
||||
|
||||
// Fetch statements after TAN confirmation
|
||||
$result = $fints->fetchStatements($savedDateFrom, $savedDateTo);
|
||||
|
||||
if ($result === 0) {
|
||||
// Another TAN required (unlikely but possible)
|
||||
$_SESSION['fints_state'] = $fints->persist();
|
||||
$_SESSION['fints_action'] = 'statements';
|
||||
$_SESSION['fints_datefrom'] = $savedDateFrom;
|
||||
$_SESSION['fints_dateto'] = $savedDateTo;
|
||||
setEventMessages($langs->trans("SecureGoPlusConfirmRequired"), null, 'warnings');
|
||||
} elseif (is_array($result)) {
|
||||
$transactions = $result['transactions'] ?? array();
|
||||
$balance = $result['balance'] ?? array();
|
||||
setEventMessages($langs->trans("TransactionsFound", count($transactions)), null, 'mesgs');
|
||||
|
||||
// Store transactions for import
|
||||
$_SESSION['fints_transactions'] = $transactions;
|
||||
$_SESSION['fints_balance'] = $balance;
|
||||
|
||||
// Save session state for cronjob before closing
|
||||
$cronState = $fints->persist();
|
||||
if (!empty($cronState)) {
|
||||
dolibarr_set_const($db, 'BANKIMPORT_CRON_STATE', $cronState, 'chaine', 0, '', $conf->entity);
|
||||
dol_syslog("BankImport: Saved session state for cronjob after TAN confirmation", LOG_DEBUG);
|
||||
}
|
||||
|
||||
$fints->close();
|
||||
} else {
|
||||
setEventMessages($langs->trans("FetchFailed").': '.$fints->error, null, 'errors');
|
||||
}
|
||||
} elseif ($result == 0) {
|
||||
// Still waiting
|
||||
setEventMessages($langs->trans("WaitingForSecureGoConfirmation"), null, 'warnings');
|
||||
$_SESSION['fints_state'] = $fints->persist();
|
||||
} else {
|
||||
setEventMessages($langs->trans("TANCheckFailed").': '.$fints->error, null, 'errors');
|
||||
unset($_SESSION['fints_state']);
|
||||
}
|
||||
} else {
|
||||
setEventMessages("Keine aktive Session. Bitte erneut abrufen.", null, 'errors');
|
||||
unset($_SESSION['fints_state']);
|
||||
unset($_SESSION['fints_pending_action']);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
$title = $langs->trans("BankStatements");
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-bankimport page-statements');
|
||||
|
||||
print load_fiche_titre($title, '', 'bank');
|
||||
|
||||
// Check configuration
|
||||
$fints = new BankImportFinTS($db);
|
||||
if (!$fints->isConfigured()) {
|
||||
print '<div class="error">';
|
||||
print $langs->trans("FinTSNotConfigured");
|
||||
print ' <a href="'.dol_buildpath('/bankimport/admin/setup.php', 1).'">'.$langs->trans("GoToSetup").'</a>';
|
||||
print '</div>';
|
||||
llxFooter();
|
||||
$db->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check for pending cron TAN notification
|
||||
$cronNotification = BankImportCron::getNotification();
|
||||
if ($cronNotification !== null) {
|
||||
$notifType = $cronNotification['type'];
|
||||
$notifDate = $cronNotification['date'];
|
||||
|
||||
if ($notifType === 'tan_required') {
|
||||
print '<div class="warning">';
|
||||
print '<strong>'.$langs->trans("AutoImportTANRequired").'</strong><br>';
|
||||
print $langs->trans("AutoImportTANRequiredDesc");
|
||||
print ' <a class="button buttongen" href="'.$_SERVER["PHP_SELF"].'?action=resumecron&token='.newToken().'">'.$langs->trans("CheckSecureGoStatus").'</a>';
|
||||
print '</div><br>';
|
||||
} elseif (in_array($notifType, array('login_error', 'fetch_error', 'config_error', 'error'))) {
|
||||
print '<div class="error">';
|
||||
print '<strong>'.$langs->trans("AutoImportError").'</strong><br>';
|
||||
print $langs->trans("AutoImportErrorDesc", dol_print_date($notifDate, 'dayhour'));
|
||||
print '</div><br>';
|
||||
}
|
||||
}
|
||||
|
||||
// Check for old data warning
|
||||
$lastFetch = getDolGlobalInt('BANKIMPORT_LAST_FETCH');
|
||||
if ($lastFetch > 0 && (time() - $lastFetch) > 14 * 86400) {
|
||||
print '<div class="warning">';
|
||||
print $langs->trans("LastFetchWarning", dol_print_date($lastFetch, 'day'));
|
||||
print '</div><br>';
|
||||
}
|
||||
|
||||
if (!$fints->isLibraryAvailable()) {
|
||||
print '<div class="error">';
|
||||
print $langs->trans("FinTSLibraryNotFound");
|
||||
print '<br><code>cd '.dirname(__FILE__).' && composer install</code>';
|
||||
print '</div>';
|
||||
llxFooter();
|
||||
$db->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Filter form
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="fetch">';
|
||||
|
||||
print '<div class="fichecenter">';
|
||||
print '<table class="border centpercent">';
|
||||
|
||||
// IBAN display
|
||||
print '<tr>';
|
||||
print '<td class="titlefield">'.$langs->trans("Account").'</td>';
|
||||
print '<td><strong>'.dol_escape_htmltag($fints->getIban()).'</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
// Date from
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans("DateFrom").'</td>';
|
||||
print '<td>';
|
||||
print $form->selectDate($dateFrom, 'date_from', 0, 0, 0, '', 1, 1);
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Date to
|
||||
print '<tr>';
|
||||
print '<td>'.$langs->trans("DateTo").'</td>';
|
||||
print '<td>';
|
||||
print $form->selectDate($dateTo, 'date_to', 0, 0, 0, '', 1, 1);
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '<div class="center">';
|
||||
print '<input type="submit" class="button" value="'.$langs->trans("FetchStatements").'">';
|
||||
|
||||
// SecureGo Plus polling button
|
||||
if (!empty($_SESSION['fints_state'])) {
|
||||
print ' <a class="button" href="'.$_SERVER["PHP_SELF"].'?action=checktan&token='.newToken().'">'.$langs->trans("CheckSecureGoStatus").'</a>';
|
||||
}
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
// JavaScript for automatic TAN polling
|
||||
if (!empty($_SESSION['fints_state'])) {
|
||||
print '<script type="text/javascript">
|
||||
var tanPollingInterval = null;
|
||||
var tanPollingCount = 0;
|
||||
var tanPollingMaxAttempts = 60; // 3 minutes at 3 second intervals
|
||||
|
||||
function startTanPolling() {
|
||||
if (tanPollingInterval) return;
|
||||
|
||||
document.getElementById("tan-status-container").style.display = "block";
|
||||
updateTanStatus("Warte auf Bestätigung in SecureGo Plus App...", "info");
|
||||
|
||||
tanPollingInterval = setInterval(checkTanStatus, 3000);
|
||||
// First check immediately
|
||||
checkTanStatus();
|
||||
}
|
||||
|
||||
function stopTanPolling() {
|
||||
if (tanPollingInterval) {
|
||||
clearInterval(tanPollingInterval);
|
||||
tanPollingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTanStatus(message, type) {
|
||||
var statusEl = document.getElementById("tan-status-message");
|
||||
statusEl.innerHTML = message;
|
||||
statusEl.className = "tan-status-" + type;
|
||||
}
|
||||
|
||||
function checkTanStatus() {
|
||||
tanPollingCount++;
|
||||
|
||||
if (tanPollingCount > tanPollingMaxAttempts) {
|
||||
stopTanPolling();
|
||||
updateTanStatus("Zeitüberschreitung. Bitte erneut versuchen.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
updateTanStatus("Prüfe Status... (" + tanPollingCount + "/" + tanPollingMaxAttempts + ")", "info");
|
||||
|
||||
fetch("'.dol_buildpath('/bankimport/ajax/checktan.php', 1).'?token='.newToken().'", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === "success") {
|
||||
stopTanPolling();
|
||||
updateTanStatus("Erfolgreich! " + data.count + " Buchungen abgerufen.", "success");
|
||||
|
||||
// Display balance and transactions
|
||||
displayBalance(data.balance);
|
||||
if (data.transactions && data.transactions.length > 0) {
|
||||
displayTransactions(data.transactions);
|
||||
}
|
||||
|
||||
// Hide polling container after success
|
||||
setTimeout(function() {
|
||||
document.getElementById("tan-status-container").style.display = "none";
|
||||
}, 3000);
|
||||
|
||||
} else if (data.status === "waiting") {
|
||||
updateTanStatus("Warte auf Bestätigung in SecureGo Plus App... (" + tanPollingCount + "/" + tanPollingMaxAttempts + ")", "info");
|
||||
|
||||
} else if (data.status === "tan_required") {
|
||||
// Another TAN required, keep polling
|
||||
updateTanStatus("Weitere TAN erforderlich - bitte erneut bestätigen", "warning");
|
||||
|
||||
} else {
|
||||
stopTanPolling();
|
||||
updateTanStatus("Fehler: " + data.message, "error");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error:", error);
|
||||
// Don\'t stop on network errors, just retry
|
||||
updateTanStatus("Netzwerkfehler, versuche erneut...", "warning");
|
||||
});
|
||||
}
|
||||
|
||||
function displayBalance(balance) {
|
||||
if (!balance || !balance.amount) return;
|
||||
|
||||
var color = balance.amount >= 0 ? "green" : "red";
|
||||
var date = balance.date || "";
|
||||
var html = "<br><div class=\"info\" style=\"padding: 10px; background: #e8f4e8; border: 1px solid #4CAF50; border-radius: 4px; margin-bottom: 15px;\">";
|
||||
html += "<strong>'.$langs->trans("AccountBalance").':</strong> ";
|
||||
html += "<span style=\"color: " + color + "; font-size: 1.2em; font-weight: bold;\">" + formatCurrency(balance.amount) + " " + (balance.currency || "EUR") + "</span>";
|
||||
html += " <span class=\"opacitymedium\">('.$langs->trans("AsOf").' " + date + ")</span>";
|
||||
html += "</div>";
|
||||
|
||||
document.getElementById("balance-container").innerHTML = html;
|
||||
}
|
||||
|
||||
function displayTransactions(transactions) {
|
||||
var html = "<br><h3>'.$langs->trans("Transactions").' (" + transactions.length + ")</h3>";
|
||||
html += "<table class=\"noborder centpercent\">";
|
||||
html += "<tr class=\"liste_titre\">";
|
||||
html += "<th>'.$langs->trans("Date").'</th>";
|
||||
html += "<th>'.$langs->trans("Name").'</th>";
|
||||
html += "<th>'.$langs->trans("Description").'</th>";
|
||||
html += "<th class=\"right\">'.$langs->trans("Amount").'</th>";
|
||||
html += "</tr>";
|
||||
|
||||
var totalCredit = 0;
|
||||
var totalDebit = 0;
|
||||
|
||||
transactions.forEach(function(trans) {
|
||||
var date = new Date(trans.date * 1000);
|
||||
var dateStr = date.toLocaleDateString("de-DE");
|
||||
var amountClass = trans.amount >= 0 ? "green" : "red";
|
||||
var amountPrefix = trans.amount >= 0 ? "+" : "";
|
||||
|
||||
if (trans.amount >= 0) {
|
||||
totalCredit += trans.amount;
|
||||
} else {
|
||||
totalDebit += Math.abs(trans.amount);
|
||||
}
|
||||
|
||||
html += "<tr class=\"oddeven\">";
|
||||
html += "<td class=\"nowraponall\">" + dateStr + "</td>";
|
||||
html += "<td>" + escapeHtml(trans.name || "") + "</td>";
|
||||
html += "<td class=\"small\">" + escapeHtml((trans.reference || "").substring(0, 80)) + "</td>";
|
||||
html += "<td class=\"right nowraponall\"><span style=\"color: " + amountClass + ";\">" + amountPrefix + formatCurrency(trans.amount) + " EUR</span></td>";
|
||||
html += "</tr>";
|
||||
});
|
||||
|
||||
// Totals row
|
||||
var balance = totalCredit - totalDebit;
|
||||
var balanceColor = balance >= 0 ? "green" : "red";
|
||||
html += "<tr class=\"liste_total\">";
|
||||
html += "<td colspan=\"3\" class=\"right\">'.$langs->trans("Total").'</td>";
|
||||
html += "<td class=\"right nowraponall\">";
|
||||
html += "<span style=\"color: green;\">+" + formatCurrency(totalCredit) + " EUR</span>";
|
||||
html += " / ";
|
||||
html += "<span style=\"color: red;\">-" + formatCurrency(totalDebit) + " EUR</span>";
|
||||
html += " = ";
|
||||
html += "<strong style=\"color: " + balanceColor + ";\">" + formatCurrency(balance) + " EUR</strong>";
|
||||
html += "</td>";
|
||||
html += "</tr>";
|
||||
|
||||
html += "</table>";
|
||||
|
||||
// Import button
|
||||
html += "<div class=\"center\" style=\"margin-top: 15px;\">";
|
||||
html += "<form method=\"POST\" action=\"" + window.location.pathname + "\">";
|
||||
html += "<input type=\"hidden\" name=\"token\" value=\"'.newToken().'\">";
|
||||
html += "<input type=\"hidden\" name=\"action\" value=\"import\">";
|
||||
html += "<input type=\"submit\" class=\"button button-save\" value=\"'.$langs->trans("ImportTransactions").'\">";
|
||||
html += " <a class=\"button\" href=\"'.dol_buildpath('/bankimport/list.php', 1).'\">'.$langs->trans("ViewImportedTransactions").'</a>";
|
||||
html += "</form>";
|
||||
html += "</div>";
|
||||
|
||||
document.getElementById("transactions-container").innerHTML = html;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
var div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatCurrency(amount) {
|
||||
return amount.toFixed(2).replace(".", ",").replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||
}
|
||||
|
||||
// Start polling automatically when page loads
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
startTanPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#tan-status-container {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.tan-status-info { color: #0066cc; }
|
||||
.tan-status-success { color: #008800; font-weight: bold; }
|
||||
.tan-status-warning { color: #cc6600; }
|
||||
.tan-status-error { color: #cc0000; }
|
||||
#tan-status-spinner {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="tan-status-container" style="display: none;">
|
||||
<div id="tan-status-spinner"></div>
|
||||
<span id="tan-status-message" class="tan-status-info">Initialisiere...</span>
|
||||
</div>';
|
||||
}
|
||||
|
||||
print '<div id="balance-container"></div>';
|
||||
print '<div id="transactions-container">';
|
||||
|
||||
// Display account balance from bank
|
||||
if (!empty($balance)) {
|
||||
print '<br>';
|
||||
print '<div class="info" style="padding: 10px; background: #e8f4e8; border: 1px solid #4CAF50; border-radius: 4px; margin-bottom: 15px;">';
|
||||
print '<strong>'.$langs->trans("AccountBalance").':</strong> ';
|
||||
$balColor = $balance['amount'] >= 0 ? 'green' : 'red';
|
||||
print '<span style="color: '.$balColor.'; font-size: 1.2em; font-weight: bold;">'.price($balance['amount'], 0, $langs, 1, -1, 2, $balance['currency']).'</span>';
|
||||
print ' <span class="opacitymedium">('.$langs->trans("AsOf").' '.dol_print_date(strtotime($balance['date']), 'day').')</span>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Display transactions
|
||||
if (!empty($transactions)) {
|
||||
print '<br>';
|
||||
print '<h3>'.$langs->trans("Transactions").' ('.count($transactions).')</h3>';
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("Date").'</th>';
|
||||
print '<th>'.$langs->trans("Name").'</th>';
|
||||
print '<th>'.$langs->trans("Description").'</th>';
|
||||
print '<th class="right">'.$langs->trans("Amount").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
$totalCredit = 0;
|
||||
$totalDebit = 0;
|
||||
|
||||
foreach ($transactions as $trans) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td class="nowraponall">'.dol_print_date($trans['date'], 'day').'</td>';
|
||||
print '<td>'.dol_escape_htmltag($trans['name']).'</td>';
|
||||
print '<td class="small">'.dol_escape_htmltag(dol_trunc($trans['reference'], 80)).'</td>';
|
||||
print '<td class="right nowraponall">';
|
||||
|
||||
if ($trans['amount'] >= 0) {
|
||||
$totalCredit += $trans['amount'];
|
||||
print '<span class="amount" style="color: green;">+'.price($trans['amount'], 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
} else {
|
||||
$totalDebit += abs($trans['amount']);
|
||||
print '<span class="amount" style="color: red;">'.price($trans['amount'], 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
}
|
||||
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Totals
|
||||
print '<tr class="liste_total">';
|
||||
print '<td colspan="3" class="right">'.$langs->trans("Total").'</td>';
|
||||
print '<td class="right nowraponall">';
|
||||
print '<span style="color: green;">+'.price($totalCredit, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
print ' / ';
|
||||
print '<span style="color: red;">-'.price($totalDebit, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||
print ' = ';
|
||||
$balance = $totalCredit - $totalDebit;
|
||||
$balanceColor = $balance >= 0 ? 'green' : 'red';
|
||||
print '<strong style="color: '.$balanceColor.';">'.price($balance, 0, $langs, 1, -1, 2, 'EUR').'</strong>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
// Import button
|
||||
print '<div class="center" style="margin-top: 15px;">';
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="import">';
|
||||
print '<input type="submit" class="button button-save" value="'.$langs->trans("ImportTransactions").'">';
|
||||
print ' <a class="button" href="'.dol_buildpath('/bankimport/list.php', 1).'">'.$langs->trans("ViewImportedTransactions").'</a>';
|
||||
print '</form>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
print '</div>'; // End transactions-container
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
22
vendor/autoload.php
vendored
Executable file
22
vendor/autoload.php
vendored
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInitcfc07b7e6c4a3dcfdcd6e754983b1a9b::getLoader();
|
||||
579
vendor/composer/ClassLoader.php
vendored
Executable file
579
vendor/composer/ClassLoader.php
vendored
Executable file
|
|
@ -0,0 +1,579 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, self>
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
||||
396
vendor/composer/InstalledVersions.php
vendored
Executable file
396
vendor/composer/InstalledVersions.php
vendored
Executable file
|
|
@ -0,0 +1,396 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||
* @internal
|
||||
*/
|
||||
private static $selfDir = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private static function getSelfDir()
|
||||
{
|
||||
if (self::$selfDir === null) {
|
||||
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||
}
|
||||
|
||||
return self::$selfDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = self::getSelfDir();
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
21
vendor/composer/LICENSE
vendored
Executable file
21
vendor/composer/LICENSE
vendored
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
11
vendor/composer/autoload_classmap.php
vendored
Executable file
11
vendor/composer/autoload_classmap.php
vendored
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'BankImportFinTS' => $baseDir . '/class/fints.class.php',
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
);
|
||||
11
vendor/composer/autoload_namespaces.php
vendored
Executable file
11
vendor/composer/autoload_namespaces.php
vendored
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Tests\\Fhp' => array($vendorDir . '/nemiah/php-fints/lib'),
|
||||
'Fhp' => array($vendorDir . '/nemiah/php-fints/lib'),
|
||||
);
|
||||
9
vendor/composer/autoload_psr4.php
vendored
Executable file
9
vendor/composer/autoload_psr4.php
vendored
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
38
vendor/composer/autoload_real.php
vendored
Executable file
38
vendor/composer/autoload_real.php
vendored
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInitcfc07b7e6c4a3dcfdcd6e754983b1a9b
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInitcfc07b7e6c4a3dcfdcd6e754983b1a9b', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInitcfc07b7e6c4a3dcfdcd6e754983b1a9b', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInitcfc07b7e6c4a3dcfdcd6e754983b1a9b::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
39
vendor/composer/autoload_static.php
vendored
Executable file
39
vendor/composer/autoload_static.php
vendored
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInitcfc07b7e6c4a3dcfdcd6e754983b1a9b
|
||||
{
|
||||
public static $prefixesPsr0 = array (
|
||||
'T' =>
|
||||
array (
|
||||
'Tests\\Fhp' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/nemiah/php-fints/lib',
|
||||
),
|
||||
),
|
||||
'F' =>
|
||||
array (
|
||||
'Fhp' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/nemiah/php-fints/lib',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'BankImportFinTS' => __DIR__ . '/../..' . '/class/fints.class.php',
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixesPsr0 = ComposerStaticInitcfc07b7e6c4a3dcfdcd6e754983b1a9b::$prefixesPsr0;
|
||||
$loader->classMap = ComposerStaticInitcfc07b7e6c4a3dcfdcd6e754983b1a9b::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
58
vendor/composer/installed.json
vendored
Executable file
58
vendor/composer/installed.json
vendored
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "nemiah/php-fints",
|
||||
"version": "3.7.0",
|
||||
"version_normalized": "3.7.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nemiah/phpFinTS.git",
|
||||
"reference": "08257e10229db2d4ca8c54ed7fec0f390b332519"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nemiah/phpFinTS/zipball/08257e10229db2d4ca8c54ed7fec0f390b332519",
|
||||
"reference": "08257e10229db2d4ca8c54ed7fec0f390b332519",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=8.0",
|
||||
"psr/log": "^1|^2|^3"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"php-mock/php-mock-phpunit": "^2.6",
|
||||
"phpunit/phpunit": "^9.5"
|
||||
},
|
||||
"suggest": {
|
||||
"abcaeffchen/sephpa": "1.*",
|
||||
"monolog/monolog": "Allow sending log messages to a variety of different handlers",
|
||||
"nemiah/php-sepa-xml": "dev-master"
|
||||
},
|
||||
"time": "2025-10-14T15:05:56+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Fhp": "lib/",
|
||||
"Tests\\Fhp": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHP Library for the protocols fints and hbci",
|
||||
"homepage": "https://github.com/nemiah/phpFinTS",
|
||||
"support": {
|
||||
"issues": "https://github.com/nemiah/phpFinTS/issues",
|
||||
"source": "https://github.com/nemiah/phpFinTS/tree/3.7"
|
||||
},
|
||||
"install-path": "../nemiah/php-fints"
|
||||
}
|
||||
],
|
||||
"dev": true,
|
||||
"dev-package-names": []
|
||||
}
|
||||
38
vendor/composer/installed.php
vendored
Executable file
38
vendor/composer/installed.php
vendored
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => 'dolibarr/bankimport',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'dolibarr-module',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'dolibarr/bankimport' => array(
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'dolibarr-module',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'nemiah/php-fints' => array(
|
||||
'pretty_version' => '3.7.0',
|
||||
'version' => '3.7.0.0',
|
||||
'reference' => '08257e10229db2d4ca8c54ed7fec0f390b332519',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../nemiah/php-fints',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/log' => array(
|
||||
'dev_requirement' => false,
|
||||
'replaced' => array(
|
||||
0 => '*',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
25
vendor/composer/platform_check.php
vendored
Executable file
25
vendor/composer/platform_check.php
vendored
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 80000)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||
);
|
||||
}
|
||||
30
vendor/nemiah/php-fints/.php-cs-fixer.php
vendored
Executable file
30
vendor/nemiah/php-fints/.php-cs-fixer.php
vendored
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
// This is based on the `@Symfony` rule set documented in `vendor/friendsofphp/php-cs-fixer/doc/ruleSets/Symfony.rst`.
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->in(__DIR__ . '/lib');
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
// We essentially use the Symfony style guide.
|
||||
'@Symfony' => true,
|
||||
|
||||
// But then we have some exclusions, i.e. we disable some of the checks/rules from Symfony:
|
||||
// Logic
|
||||
'yoda_style' => FALSE, // Allow both Yoda-style and regular comparisons.
|
||||
|
||||
// Whitespace
|
||||
'blank_line_before_statement' => FALSE, // Don't put blank lines before `return` statements.
|
||||
'concat_space' => FALSE, // Allow spaces around string concatenation operator.
|
||||
'blank_line_after_opening_tag' => FALSE, // Allow file-level @noinspection suppressions to live on the `<?php` line.
|
||||
'single_line_throw' => FALSE, // Allow `throw` statements to span multiple lines.
|
||||
|
||||
// phpDoc
|
||||
'phpdoc_align' => FALSE, // Don't add spaces within phpDoc just to make parameter names / descriptions align.
|
||||
'phpdoc_annotation_without_dot' => FALSE, // Allow terminating dot on @param and such.
|
||||
'phpdoc_no_alias_tag' => FALSE, // Allow @link in addition to @see.
|
||||
'phpdoc_separation' => FALSE, // Don't put blank line between @params, @throws and @return.
|
||||
'phpdoc_summary' => FALSE, // Don't force terminating dot on the first line.
|
||||
])
|
||||
->setFinder($finder);
|
||||
12
vendor/nemiah/php-fints/.travis.yml
vendored
Executable file
12
vendor/nemiah/php-fints/.travis.yml
vendored
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
language: php
|
||||
install: composer install
|
||||
script:
|
||||
- ./disallowtabs.sh
|
||||
- ./csfixer-check.sh
|
||||
- ./phplint.sh ./lib/
|
||||
- ./vendor/bin/phpunit
|
||||
dist: bionic
|
||||
php:
|
||||
- '8.0'
|
||||
- '8.1.0'
|
||||
- '8.2.0'
|
||||
200
vendor/nemiah/php-fints/DEVELOPER-GUIDE.md
vendored
Executable file
200
vendor/nemiah/php-fints/DEVELOPER-GUIDE.md
vendored
Executable file
|
|
@ -0,0 +1,200 @@
|
|||
## Specification
|
||||
|
||||
This library implements parts of FinTS V3.0, which is specified [here](https://www.hbci-zka.de/spec/3_0.htm).
|
||||
Before version 3, the standard was called HBCI.
|
||||
Today, HBCI still refers to the security part of the specification.
|
||||
The specification is split into several documents:
|
||||
|
||||
* [Hauptdokument] Unimportant index document that gives an overview of the other documents.
|
||||
* [Formals] Wire format and basic protocol (dialog, BPD, UPD).
|
||||
* [Rückmeldungen] Directory of response codes that the bank can send.
|
||||
* [Security HBCI] Protocol for encryption and cryptographic signatures.
|
||||
* [Security PIN/TAN] Protocol for PIN/TAN authentication, based on the HBCI security protocol, but without actual encryption/signatures.
|
||||
* [Messages Geschäftsvorfälle] Various business transactions (account statements, wire transfers, etc.).
|
||||
* [Messages Finanzdatenformate] Wire formats other than the FinTS format itself (e.g. DTAUS and MT 940) that are used for certain transactions.
|
||||
|
||||
[Hauptdokument]: https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Master_2018-11-29.pdf
|
||||
[Formals]: https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Formals_2017-10-06_final_version.pdf
|
||||
[Rückmeldungen]: https://www.hbci-zka.de/dokumente/spezifikation_deutsch/FinTS_Rueckmeldungscodes_2019-07-22_final_version.pdf
|
||||
[Security HBCI]: https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Security_Sicherheitsverfahren_HBCI_Rel_20181129_final_version.pdf
|
||||
[Security PIN/TAN]: https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Security_Sicherheitsverfahren_PINTAN_2018-02-23_final_version.pdf
|
||||
[Messages Geschäftsvorfälle]: https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2015-08-07_final_version.pdf
|
||||
[Messages Finanzdatenformate]: https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Finanzdatenformate_2010-08-06_final_version.pdf
|
||||
|
||||
Note that there is also an [archive](https://www.hbci-zka.de/spec/spec_archiv.htm) with older versions of the HBCI specification.
|
||||
|
||||
|
||||
## High-level concepts and structure of the library
|
||||
|
||||
A **message** (implemented in `Fhp\Protocol\Message`) is a series of **segments** (implemented in `Fhp\Segment\...`),
|
||||
which themselves consist of **data elements (DEs)** and **data element groups (DEGs)**.
|
||||
Each segment starts with an identifier to declare its type. Example:
|
||||
```
|
||||
HNHBK:1:3+000000000079+300+dialogID+2'HKEND:2:1+dialogID'HNHBS:3:1+2'
|
||||
```
|
||||
This message contains three segments.
|
||||
The middle segment is `HKEND` version `1` at position `2`. It only contains one data element with value `dialogId`.
|
||||
The first segment is `HNHBK`. It contains four data elements `000000000079`, ..., `2`.
|
||||
|
||||
The **syntactical representation (wire format)** (see [Formals]) used when transmitting/storing messages and segments is
|
||||
described below and implemented in `Fhp\Syntax`.
|
||||
|
||||
The **protocol** specifies which messages a client can send (requests) and how the server can respond to them (responses).
|
||||
There is exactly one response per request, the client needs to wait for the response before sending another request.
|
||||
While FinTs is specified independently of the underlying transport layer, all banks use TLS 3.0 in practice, so it's
|
||||
very similar to HTTPS and the server addresses are given as `https://` URIs as well.
|
||||
|
||||
The most basic protocol is the **dialog** (see [Formals], implemented in `Fhp\Protocol\DialogInitialization` and `FinTs`),
|
||||
which combines elements of a handshake (as in TCP/TLS) and a session (i.e. authentication through cookies/tokens).
|
||||
Before sending any other messages, the client must initialize a dialog.
|
||||
Most dialogs require authentication, so the initialization is comparable to a login.
|
||||
|
||||
While all of the above is part of the FinTS infrastructure, the **business transactions** (the part of the protocol that is
|
||||
ultimately useful for users) are specified separately in [Messages Geschäftsvorfälle] (implemented in `Fhp\Action\...`).
|
||||
In addition to these, bank can specify their own actions (would be implemented in separate dependent libraries, but
|
||||
currently there are none).
|
||||
|
||||
Separately from the FinTS specification, this library implements a **user-facing API** in `Fhp\Model` (in addition to
|
||||
the `FinTs` class and the `Fhp\Action` namespace) to provide a simpler and more stable interface over time.
|
||||
|
||||
NOTE: The PHP namespaces `DataElementGroups`, `Dialog` and `Response` (and their contents) are deprecated.
|
||||
|
||||
|
||||
## Segment/DEG schema
|
||||
|
||||
### Segment classes
|
||||
In this library, segments are implemented in classes named like `HKTANv6` that inherit from `BaseSegment`.
|
||||
Each of these classes is mapped 1:1 from the corresponding chapter of the specification document.
|
||||
The class name consists of the segment identifier (e.g. `HKTAN`) and the version (just an integer prefixed by `v`).
|
||||
|
||||
The segment identifier itself has two parts:
|
||||
In `HXyyy`, the `H` is constant (for "HBCI"), `yyy` is an abbreviation of the name of the respective functionality that
|
||||
the segment provides, and `X` is one of `K`, `I` or `N` depending on whether it is a request segment sent by the client
|
||||
("Kunde" = customer), a response segment sent by the server ("(Kredit-)Institut" = bank) or both ("Nachricht" = message).
|
||||
The `HIyyyS` segments suffixed by `S` contain meta information (called "parameters") that describe what constitues a
|
||||
valid `HKyyy` request.
|
||||
|
||||
The class `HXyyyvN` that implements version `N` of a particular segment type `yyy` can either be placed in the PHP
|
||||
namespace `Fhp\Segment\HXyyy` (to group versions together, used especially when `X`=`N`) or `Fhp\Segment\yyy` (to group
|
||||
corresponding request and reponse segments together).
|
||||
|
||||
### DEG classes
|
||||
Analogously to segments, data element groups (DEGs) are mapped to classes that inherit from `BaseDeg`.
|
||||
The class name is the title of the sub-section that specifies the DEG structure in the specification document.
|
||||
The version suffix (`VN` with capital `V`) is optional for DEGs.
|
||||
The naming does not have to be as strictly deterministic as with segments because DEGs are referenced explicitly in
|
||||
code from the respective segments that contain them.
|
||||
|
||||
### Segment/DEG interfaces
|
||||
Especially when there are multiple supported versions of the same segment/DEG type (e.g. `HKKAZv6` and `HKKAZv7`), it
|
||||
makes sense to also have a common `interface HKKAZ` implemented by all versions that defines getters for the common
|
||||
fields, so that business logic code can access all versions transparently.
|
||||
|
||||
### Member fields / data elements
|
||||
|
||||
As an example, consider the `HIUPDv6` class that implements the "Kontoinformation" segment from page 88 (PDF page 96)
|
||||
from the [Formals] document.
|
||||
|
||||
Within each segment/DEG class, the elements defined in the specification are translated one by one to class fields.
|
||||
It is important that the fields occur in the *same order* as in the specification and that no fields are left out, because
|
||||
the absolute index/position of each field in the segment class must map to its index in HBCI's wire format.
|
||||
Segment classes may inherit from other segment classes, in which case the fields of the parent class come first.
|
||||
The name of the field/element is taken directly from the specification, so it is usually in German.
|
||||
Special characters like Umlauts are replaced with their expanded versions and the whole name is transformed to camel case.
|
||||
|
||||
The element types (specified in [Formals] section B.4) are mapped to PHP types as follows:
|
||||
- `jn` (yes/no) becomes `bool`.
|
||||
- `num` (numerical) and `dig` (single digit) become `int`.
|
||||
- `float` and `wrt` (amounts) become `float`.
|
||||
- `an` (alpha-numerical), `txt` (text), `id` (identifiers), `ctr` (country codes, despite being numerical) and `cur`
|
||||
(currency codes) become `string` and the maximum length is documented in phpDoc.
|
||||
- `code` (enum) is resolved to the type of the underlying value type (usually `string` or `int`) and the allowed values
|
||||
are documented in phpDoc.
|
||||
- `bin` uses the `Fhp\Syntax\Bin` class and the maximum length is documented in phpDoc.
|
||||
- `dat` and `tim` are currently mapped to `string`, but could get their own class in `Fhp\DataTypes` in future.
|
||||
- Any data element group is implemented as a separate DEG class (see above), so that the PHP field can reference that
|
||||
class name as its type.
|
||||
|
||||
The "Status" of each element is mapped as follows:
|
||||
- Mandatory fields ("M" in the specification) use the plain type as described above.
|
||||
- Optional fields ("O") append `|null`.
|
||||
- Conditional fields ("C") are resolved as much as possible given the context of the segment and this library in general
|
||||
(often only one of the conditions is always satisfied, so it is clear whether the field is mandatory, optional or
|
||||
disallowed whenever it is used in this library), and otherwise mapped to `|null` as well.
|
||||
|
||||
The "Anzahl" (cardinality) is mapped as follows:
|
||||
- If only `1` is allowed, use the plain (possibly nullable) type as described above.
|
||||
- Otherwise append `[]` to the type and add a `@Max(N)` annotation for the maximum number. E.g. a `jn` field that can be
|
||||
repeated at most 20 times becomes `@var int[] @Max(20)`.
|
||||
- If `0` is within the allowed range, add `|null`, e.g. `@var int[]|null @Max(20)`.
|
||||
|
||||
Any other information that is given in the specification like maximum length, restrictions on when the field can be
|
||||
used or not, and the guidelines ("Belegungsrichtlinien"), are added to the phpDoc in English (translated) if useful.
|
||||
|
||||
|
||||
## Wire format
|
||||
|
||||
Segments are terminated (and thus also delimited) by `'`.
|
||||
They contain a series of DEs and DEGs, delimited by `:` and `+`.
|
||||
Note that these delimiters aren't very consistent (see below).
|
||||
|
||||
The values essentially form an ordered tree, where the leaves are data elements (similar to scalar values), inner nodes
|
||||
are data element groups (similar to compound values) and the roots are segments.
|
||||
The entries of a segment (i.e. the second level of the tree), which can be a mix of DEs and DEGs, are delimited by `+`.
|
||||
The entries of any lower-level inner node are delimited by `:`.
|
||||
Such a tree can be arbitrarily deep, which leads to ambiguities.
|
||||
For instance, the wire format of a DEG `X` that contains nothing but a nested DEG `Y` is indistinguishable from the wire
|
||||
format of `Y` alone.
|
||||
With repeated and optional elements, there are even more ambiguities, but the specification requires "empty" elements to
|
||||
clarify in such situations.
|
||||
|
||||
While the wire format's syntax can be recognized and parsed without knowing what the contents mean (similar to JSON or
|
||||
XML), the FinTs wire format is not self-describing on the semantic level, so the segment definitions are needed to
|
||||
understand the contents (similar to protocol buffers and other binary serialization formats).
|
||||
However, each segment begins with a `Segmentkopf` as its first entry, which declares the type of the segment and allows
|
||||
picking the right segment definition/schema for parsing.
|
||||
|
||||
### Prettifier scripts
|
||||
|
||||
For debugging purposes, it can be useful to render serialized messages/segments in a more readable (self-describing)
|
||||
format, specifically with the field names inlined.
|
||||
The `prettify_message.php` and `prettify_segment.php` scripts do exactly that, given the wire format on stdin.
|
||||
|
||||
|
||||
## PSD2 PIN/TAN authentication
|
||||
|
||||
The PIN/TAN specification piggy-backs on the HBCI encryption/signature specification that was originally intended for
|
||||
cryptographically secure communication based on private keys contained in physical tokens like HBCI chip cards.
|
||||
Nowadays, the surrounding transport layer (TLS) already provides the cryptographic security based on the public-key
|
||||
infrastructure of the WWW, so that proper encryption and signatures are not necessary anymore on the FinTS level.
|
||||
Nevertheless, all messages need to be wrapped in the respective envelope (implemented in `Fhp\Protocol\Message`).
|
||||
This envelope also carries PIN and TAN whenever they need to be sent.
|
||||
|
||||
In addition to the envelope, the `HKTAN` family of segments are used to communicate whether a TAN is needed, what it
|
||||
needs to look like, what it is for when it is being sent, and whether it was accepted on the bank side.
|
||||
`HKTAN` version 6 is equivalent to PSD2.
|
||||
|
||||
In practice, with PSD2 for the first time, several German banks started asking for a TAN even upon login, which can
|
||||
sometimes lead to problems depending on how they implement the protocol, given that `HKTAN` was designed to authenticate
|
||||
business transactions *within* a dialog, but is now used to *initialize* a dialog as well.
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
Very few banks provide a sandbox environment with fake accounts for testing purposes.
|
||||
So most developers manual tests against their real bank accounts.
|
||||
To minify the impact on these accounts (undesired transactions, account locks due to failed login attempts), automated
|
||||
integration testing (against fake backends) has proven very useful (in addition to the usual regression-catching
|
||||
benefits of those tests).
|
||||
|
||||
The `CLILogger` class can be used to record requests/responses during a (manually scripted) dialog with the bank.
|
||||
The recorded messages can then be filled into a `PhpUnit` test.
|
||||
When used through `FinTs::setLogger()`, the logger already replaces the most important sensitive values
|
||||
(username, PIN, ...) with placeholders.
|
||||
But the recorded segments regularly still contain personal information (e.g. IBANs and names of other parties involved
|
||||
in transactions, descriptions of transactions, etc.).
|
||||
In order to remove this information, it is advisable not to change the overall length (i.e. using the `Ins` mode in the
|
||||
code editor to overwrite it with some dummy data), because the wire format hard-codes the length of subsequent data in
|
||||
various places and simply removing it would result in parsing failures.
|
||||
For examples, see `Tests\Fhp\Integration\...`.
|
||||
|
||||
21
vendor/nemiah/php-fints/LICENSE
vendored
Executable file
21
vendor/nemiah/php-fints/LICENSE
vendored
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2016 Markus Schindler <mail@markus-schindler.de>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
89
vendor/nemiah/php-fints/README.md
vendored
Executable file
89
vendor/nemiah/php-fints/README.md
vendored
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
# PHP FinTS/HBCI library
|
||||
|
||||
[](https://travis-ci.org/nemiah/phpFinTS)
|
||||
|
||||
A PHP library implementing the following functions of the FinTS/HBCI protocol:
|
||||
|
||||
* Get accounts
|
||||
* Get balance
|
||||
* Get transactions
|
||||
* Execute direct debit
|
||||
* Execute transfer
|
||||
* Note that any other functions mentioned in
|
||||
[section C of the specification](https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Geschaeftsvorfaelle_2015-08-07_final_version.pdf)
|
||||
should be relatively straightfoward to implement.
|
||||
|
||||
Forked from [mschindler83/fints-hbci-php](https://github.com/mschindler83/fints-hbci-php), but then mostly reimplemented.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Before using this library (or any other FinTS library), you have to register your application with
|
||||
[Die Deutsche Kreditwirtschaft](https://www.hbci-zka.de/register/hersteller.htm) in order to get your registration
|
||||
number.
|
||||
Note that this process can take several weeks.
|
||||
First you receive your registration number **after a couple days, but then you have to wait anywhere between 0 and 8+ weeks**
|
||||
for the registration to reach your bank's server. If you have multiple banks, it probably reaches them at different times.
|
||||
|
||||
Then install the library via composer:
|
||||
|
||||
```
|
||||
composer require nemiah/php-fints
|
||||
```
|
||||
|
||||
See the examples in the "[Samples](/Samples)" folder to get started on your code.
|
||||
Fill out the required configuration in `init.php` (server details can be obtained at
|
||||
[www.hbci-zka.de](https://www.hbci-zka.de) after registration).
|
||||
Then execute `tanModesAndMedia.php` and later `login.php`.
|
||||
Once you are able to login without any issues, you can move on to the other examples.
|
||||
|
||||
## Banks with special needs
|
||||
|
||||
If you are developing an online banking application with this library, please be aware of the following exceptions:
|
||||
|
||||
### Hypovereinsbank
|
||||
|
||||
The BLZ 71120078 will throw an "Unbekanntes Kreditinstitut" exception when used with the URL https://hbci-01.hypovereinsbank.de/bank/hbci.
|
||||
You have to use BLZ 70020270 instead.
|
||||
```
|
||||
if (trim($url) == 'https://hbci-01.hypovereinsbank.de/bank/hbci')
|
||||
$blz = '70020270';
|
||||
```
|
||||
|
||||
### ING Diba
|
||||
|
||||
This bank does not support PSD2:
|
||||
```
|
||||
if(trim($blz) == "50010517")
|
||||
$fints->selectTanMode(new Fhp\Model\NoPsd2TanMode());
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
Contributions are welcome! See the [developer guide](DEVELOPER-GUIDE.md) for some background information.
|
||||
|
||||
We use a slightly modified version of the [Symfony Coding-Style](https://symfony.com/doc/current/contributing/code/standards.html).
|
||||
Please run
|
||||
```
|
||||
composer update
|
||||
```
|
||||
and
|
||||
```
|
||||
composer cs-fix
|
||||
```
|
||||
|
||||
before sending a PR.
|
||||
|
||||
### Bank compatibility
|
||||
|
||||
Different banks implement different versions of the HBCI and FinTS specifications, and they also interpret the
|
||||
specification differently sometimes. In addition, banks behave differently (within the boundaries of the specification)
|
||||
when it comes to validation (some may tolerate slightly wrong requests), TANs (some ask for TANs more often than others)
|
||||
and allowed parameters (not all banks support all parameter combinations).
|
||||
|
||||
This library aims to be compatible with all banks that support [FinTS V3.0](https://www.hbci-zka.de/spec/3_0.htm) and
|
||||
PIN/TAN-based authentication according to PSD2 regulations, which includes most relevant German banks. Currently, it
|
||||
works with the most popular banks at least, and probably with most others too. Some corner cases (e.g. Mehrfach-TANs or
|
||||
SMS-Abbuchungskonto for mTAN fees) are not and probably will not be supported.
|
||||
Those banks with a dedicated [integration test](/lib/Tests/Fhp/Integration) have been tested most extensively.
|
||||
|
||||
If you encounter any problems with your particular bank, please check for open GitHub issues or open a new one.
|
||||
20
vendor/nemiah/php-fints/Samples/accounts.php
vendored
Executable file
20
vendor/nemiah/php-fints/Samples/accounts.php
vendored
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Displays the available accounts
|
||||
*/
|
||||
|
||||
// See login.php, it returns a FinTs instance that is already logged in.
|
||||
/** @var \Fhp\FinTs $fints */
|
||||
$fints = require_once 'login.php';
|
||||
|
||||
// Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
|
||||
// hard-coded and not call getSEPAAccounts() at all.
|
||||
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
|
||||
$fints->execute($getSepaAccounts);
|
||||
if ($getSepaAccounts->needsTan()) {
|
||||
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
|
||||
}
|
||||
print_r($getSepaAccounts->getAccounts());
|
||||
35
vendor/nemiah/php-fints/Samples/balance.php
vendored
Executable file
35
vendor/nemiah/php-fints/Samples/balance.php
vendored
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
<?php /** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Displays the current balance of all accounts.
|
||||
*/
|
||||
|
||||
// See login.php, it returns a FinTs instance that is already logged in.
|
||||
/** @var \Fhp\FinTs $fints */
|
||||
$fints = require_once 'login.php';
|
||||
|
||||
// Just pick the first account for the request, though we will request the balance of all accounts.
|
||||
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
|
||||
$fints->execute($getSepaAccounts);
|
||||
if ($getSepaAccounts->needsTan()) {
|
||||
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
|
||||
}
|
||||
$oneAccount = $getSepaAccounts->getAccounts()[0];
|
||||
|
||||
$getBalance = \Fhp\Action\GetBalance::create($oneAccount, true);
|
||||
$fints->execute($getBalance);
|
||||
if ($getBalance->needsTan()) {
|
||||
handleStrongAuthentication($getBalance); // See login.php for the implementation.
|
||||
}
|
||||
|
||||
/** @var \Fhp\Segment\SAL\HISAL $hisal */
|
||||
foreach ($getBalance->getBalances() as $hisal) {
|
||||
$accNo = $hisal->getAccountInfo()->getAccountNumber();
|
||||
if ($hisal->getKontoproduktbezeichnung() !== null) {
|
||||
$accNo .= ' (' . $hisal->getKontoproduktbezeichnung() . ')';
|
||||
}
|
||||
$amnt = $hisal->getGebuchterSaldo()->getAmount();
|
||||
$curr = $hisal->getGebuchterSaldo()->getCurrency();
|
||||
$date = $hisal->getGebuchterSaldo()->getTimestamp()->format('Y-m-d');
|
||||
echo "On $accNo you have $amnt $curr as of $date.\n";
|
||||
}
|
||||
18
vendor/nemiah/php-fints/Samples/bpd.php
vendored
Executable file
18
vendor/nemiah/php-fints/Samples/bpd.php
vendored
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Fetches the BPD (bank parameter data) without having any user-specific credentials. This is mostly useful
|
||||
* for advanced applications or to explore the bank's FinTS features without having/risking own credentials.
|
||||
*/
|
||||
require '../vendor/autoload.php';
|
||||
|
||||
$options = new \Fhp\Options\FinTsOptions();
|
||||
$options->url = 'https://banking-dkb.s-fints-pt-dkb.de/fints30'; // HBCI / FinTS Url can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm (use the PIN/TAN URL)
|
||||
$options->bankCode = '12030000'; // Your bank code / Bankleitzahl
|
||||
$options->productName = 'Dummy'; // The number you receive after registration / FinTS-Registrierungsnummer. Not all banks require this just to retrieve the BPD.
|
||||
$options->productVersion = '1.0'; // Your own Software product version
|
||||
$bpd = \Fhp\FinTs::fetchBpd($options, new \Tests\Fhp\CLILogger());
|
||||
|
||||
print_r($bpd); // Tip: Put a breakpoint on this line.
|
||||
252
vendor/nemiah/php-fints/Samples/browser.php
vendored
Executable file
252
vendor/nemiah/php-fints/Samples/browser.php
vendored
Executable file
|
|
@ -0,0 +1,252 @@
|
|||
<?php /** @noinspection PhpUnhandledExceptionInspection */
|
||||
/** @noinspection PhpComposerExtensionStubsInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Does the whole login procedure in a browser and then displays the current balance of all accounts.
|
||||
*
|
||||
* To run it:
|
||||
* 1. $ php -S 0.0.0.0:8080 -t ./Samples
|
||||
* 2. http://localhost:8080/browser.php
|
||||
*/
|
||||
|
||||
// IMPORTANT: This implementation serves only to demonstrate how the phpFinTS library can be used in a web application
|
||||
// setting. It follows no coding best practices. Given that these applications handle sensitive data like bank
|
||||
// credentials and financial information, any real application should follow security-related best practices like XSRF
|
||||
// protection, encryption, etc., and this application should not be deployed to a publicly accessible web server.
|
||||
|
||||
require '../vendor/autoload.php';
|
||||
function exception_error_handler($errno, $errstr, $errfile, $errline)
|
||||
{
|
||||
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||
}
|
||||
set_error_handler('exception_error_handler');
|
||||
|
||||
$request = json_decode(file_get_contents('php://input'));
|
||||
|
||||
if (isset($request->action)) {
|
||||
$options = new \Fhp\Options\FinTsOptions();
|
||||
$options->productName = $request->productName;
|
||||
$options->productVersion = $request->productVersion;
|
||||
$options->url = $request->url;
|
||||
$options->bankCode = $request->bankCode;
|
||||
$credentials = \Fhp\Options\Credentials::create($request->username, $request->pin);
|
||||
|
||||
$persistedInstance = $persistedAction = null;
|
||||
function handleRequest(\stdClass $request, \Fhp\FinTs $fints)
|
||||
{
|
||||
global $persistedAction;
|
||||
switch ($request->action) {
|
||||
case 'getTanModes':
|
||||
return array_map(function ($mode) {
|
||||
return [
|
||||
'id' => $mode->getId(), 'name' => $mode->getName(), 'isDecoupled' => $mode->isDecoupled(),
|
||||
'needsTanMedium' => $mode->needsTanMedium(),
|
||||
];
|
||||
}, array_values($fints->getTanModes()));
|
||||
case 'getTanMedia':
|
||||
return array_map(function ($medium) {
|
||||
return ['name' => $medium->getName(), 'phoneNumber' => $medium->getPhoneNumber()];
|
||||
}, $fints->getTanMedia(intval($request->tanmode)));
|
||||
case 'login':
|
||||
$fints->selectTanMode(intval($request->tanmode), $request->tanmedium ?? null);
|
||||
$login = $fints->login();
|
||||
if ($login->needsTan()) {
|
||||
$tanRequest = $login->getTanRequest();
|
||||
$persistedAction = serialize($login);
|
||||
return ['result' => 'needsTan', 'challenge' => $tanRequest->getChallenge()];
|
||||
}
|
||||
return ['result' => 'success'];
|
||||
case 'submitTan':
|
||||
$fints->submitTan(unserialize($persistedAction), $request->tan);
|
||||
$persistedAction = null;
|
||||
return ['result' => 'success'];
|
||||
case 'checkDecoupledSubmission':
|
||||
if ($fints->checkDecoupledSubmission(unserialize($persistedAction))) {
|
||||
$persistedAction = null;
|
||||
return ['result' => 'success'];
|
||||
} else {
|
||||
// IMPORTANT: If you pull this example code apart in your real application code, remember that after
|
||||
// calling checkDecoupledSubmission(), you need to call $fints->persist() again, just like this
|
||||
// example code will do after we return from handleRequest() here.
|
||||
return ['result' => 'ongoing'];
|
||||
}
|
||||
case 'getBalances':
|
||||
$getAccounts = \Fhp\Action\GetSEPAAccounts::create();
|
||||
$fints->execute($getAccounts);
|
||||
if ($getAccounts->needsTan()) {
|
||||
throw new \Fhp\UnsupportedException(
|
||||
"This simple example code does not support strong authentication on GetSEPAAccounts calls. " .
|
||||
"But in your real application, you can do so analogously to how login() is handled above."
|
||||
);
|
||||
}
|
||||
|
||||
$getBalances = \Fhp\Action\GetBalance::create($getAccounts->getAccounts()[0], true);
|
||||
$fints->execute($getBalances);
|
||||
if ($getAccounts->needsTan()) {
|
||||
throw new \Fhp\UnsupportedException(
|
||||
"This simple example code does not support strong authentication on GetBalance calls. " .
|
||||
"But in your real application, you can do so analogously to how login() is handled above."
|
||||
);
|
||||
}
|
||||
|
||||
$balances = [];
|
||||
foreach ($getBalances->getBalances() as $balance) {
|
||||
$sdo = $balance->getGebuchterSaldo();
|
||||
$balances[$balance->getAccountInfo()->getAccountNumber()] =
|
||||
$sdo->getAmount() . ' ' . $sdo->getCurrency();
|
||||
}
|
||||
return $balances;
|
||||
case 'logout':
|
||||
$fints->close();
|
||||
return ['result' => 'success'];
|
||||
default:
|
||||
throw new \InvalidArgumentException("Unknown action $request->action");
|
||||
}
|
||||
}
|
||||
|
||||
$sessionfile = __DIR__ . "/session_$request->sessionid.data";
|
||||
if (file_exists($sessionfile)) {
|
||||
list($persistedInstance, $persistedAction) = unserialize(file_get_contents($sessionfile));
|
||||
}
|
||||
$fints = \Fhp\FinTs::new($options, $credentials, $persistedInstance);
|
||||
$response = handleRequest($request, $fints);
|
||||
file_put_contents($sessionfile, serialize([$fints->persist(), $persistedAction]));
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<title>phpFinTS Beispielanwendung</title>
|
||||
<style>
|
||||
fieldset { border: none; }
|
||||
td:first-child { text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>phpFinTS Beispielanwendung</h1>
|
||||
<p>Diese Beispielanwendung meldet sich im Onlinebanking an und holt die aktuellen Kontostände ab.</p>
|
||||
<p><b>HINWEIS:</b> Wenn sich Bank oder Benutzer ändern, sollte diese Seite erst neu geladen werden!</p>
|
||||
<form id="form">
|
||||
<input type="hidden" name="sessionid" id="sessionid"/>
|
||||
<fieldset id="fieldset">
|
||||
<table>
|
||||
<tr><td><a target="_blank" href="https://www.hbci-zka.de/register/prod_register.htm">Registrierungsnummer</a>:</td>
|
||||
<td><input type="text" name="productName"/></td></tr>
|
||||
<tr><td>Produktversion:</td><td><input type="text" name="productVersion" value="1.0"/></td></tr>
|
||||
<tr><td>Bank URL:</td><td><input type="text" name="url" value="https://banking-dkb.s-fints-pt-dkb.de/fints30"/></td></tr>
|
||||
<tr><td>Bankleitzahl:</td><td><input type="text" name="bankCode" value="12030000"/></td></tr>
|
||||
<tr><td>Benutzerkennung:</td><td><input type="text" name="username"/></td></tr>
|
||||
<tr><td>Passwort/PIN:</td><td><input type="password" name="pin"/></td></tr>
|
||||
<tr id="tanmodeRow" style="display: none"><td>TAN-Modus:</td><td><select name="tanmode" id="tanmode"></select></td></tr>
|
||||
<tr id="tanmediumRow" style="display: none"><td>TAN-Medium:</td><td><select name="tanmedium" id="tanmedium"></select></td></tr>
|
||||
<tr><td></td><td><button id="submit">Los geht's</button></td></tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
</form>
|
||||
<pre id="output"></pre>
|
||||
<script>
|
||||
document.getElementById('sessionid').value = new Date().getTime();
|
||||
document.getElementById('submit').onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('form');
|
||||
const formData = Object.fromEntries([...new FormData(form)]);
|
||||
const fieldset = document.getElementById('fieldset');
|
||||
async function post(action, additionalParams) {
|
||||
const response = await fetch('browser.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({action, ...formData, ...additionalParams}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
if (response.headers.get('Content-Type').startsWith('text/html')) { // PHP error
|
||||
document.getElementById('output').innerHTML = await response.text();
|
||||
throw new Error('PHP error, click OK to see details below.');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
fieldset.disabled = true;
|
||||
document.getElementById('output').innerText = '';
|
||||
try {
|
||||
// First the user needs to select a TAN mode. If they haven't already, maybe we need to fetch them first.
|
||||
const tanmode = document.getElementById('tanmode');
|
||||
if (!tanmode.value) {
|
||||
while (tanmode.firstChild) tanmode.firstChild.remove();
|
||||
for (const mode of await post('getTanModes')) {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', mode.id);
|
||||
option.appendChild(document.createTextNode(mode.name));
|
||||
option.tanmode = mode;
|
||||
tanmode.appendChild(option);
|
||||
}
|
||||
document.getElementById('tanmodeRow').style.display = '';
|
||||
alert('Bitte einen TAN-Modus auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
// If the TAN mode requires it, the user also needs to select a TAN medium.
|
||||
const selectedMode = tanmode.options[tanmode.selectedIndex].tanmode;
|
||||
const tanmedium = document.getElementById('tanmedium');
|
||||
if (selectedMode.needsTanMedium && !tanmedium.value) {
|
||||
while (tanmedium.firstChild) tanmedium.firstChild.remove();
|
||||
for (const medium of await post('getTanMedia')) {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', medium.name);
|
||||
let text = medium.name;
|
||||
if (medium.phoneNumber) text += ` (${medium.phoneNumber})`;
|
||||
option.appendChild(document.createTextNode(text));
|
||||
tanmedium.appendChild(option);
|
||||
}
|
||||
document.getElementById('tanmediumRow').style.display = '';
|
||||
alert('Bitte ein TAN-Medium auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper function for TAN/decoupled authentication handling.
|
||||
async function handleStrongAuthentication(responsePromise) {
|
||||
let response = await responsePromise;
|
||||
if (response.result === 'needsTan') {
|
||||
if (selectedMode.isDecoupled) {
|
||||
do {
|
||||
alert('Bitte bestätigen Sie die Aktion auf Ihrem Gerät und klicken Sie dann auf OK.');
|
||||
response = await post('checkDecoupledSubmission');
|
||||
} while (response.result === 'ongoing');
|
||||
} else {
|
||||
const tan = prompt('Bitte die TAN eingeben. Bank sagt: ' + response.challenge);
|
||||
response = await post('submitTan', {tan});
|
||||
}
|
||||
}
|
||||
if (response.result !== 'success') {
|
||||
throw new Error(`Unexpected result ${response.result}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Now we have everything we need to log in.
|
||||
await handleStrongAuthentication(post('login'));
|
||||
|
||||
// Now that we're logged in, we can grab the balances.
|
||||
const balances = await post('getBalances');
|
||||
document.getElementById('output').innerText = JSON.stringify(balances);
|
||||
|
||||
// And let's log out.
|
||||
await post('logout');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
alert(e);
|
||||
} finally {
|
||||
fieldset.disabled = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
51
vendor/nemiah/php-fints/Samples/directDebit_Sephpa.php
vendored
Executable file
51
vendor/nemiah/php-fints/Samples/directDebit_Sephpa.php
vendored
Executable file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUndefinedMethodInspection */
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
/** @noinspection PhpUndefinedNamespaceInspection */
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Send a direct debit request
|
||||
*
|
||||
* Note: The phpFinTs library only implements the FinTS protocol. For SEPA transfers, you need a separate library to
|
||||
* produce the SEPA XML data, which is then wrapped into FinTS requests. This example uses the Sephpa library
|
||||
* (see https://github.com/AbcAeffchen/Sephpa), which you need to install separately.
|
||||
*/
|
||||
|
||||
// See login.php, it returns a FinTs instance that is already logged in.
|
||||
/** @var \Fhp\FinTs $fints */
|
||||
$fints = require_once 'login.php';
|
||||
|
||||
// Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
|
||||
// hard-coded and not call getSEPAAccounts() at all.
|
||||
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
|
||||
$fints->execute($getSepaAccounts);
|
||||
if ($getSepaAccounts->needsTan()) {
|
||||
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
|
||||
}
|
||||
$oneAccount = $getSepaAccounts->getAccounts()[0];
|
||||
|
||||
// generate a SepaDirectDebit object (pain.008.003.02).
|
||||
$directDebitFile = new \AbcAeffchenSephpa\SephpaDirectDebit(
|
||||
'Name of Application',
|
||||
'Message Identifier',
|
||||
\AbcAeffchenSephpa\SephpaDirectDebit::SEPA_PAIN_008_003_02
|
||||
);
|
||||
/*
|
||||
*
|
||||
* Configure the Direct Debit File
|
||||
* $directDebitCollection = $directDebitFile->addCollection([...]);
|
||||
* $directDebitCollection->addPayment([...]);
|
||||
*
|
||||
* See documentation:
|
||||
* https://github.com/AbcAeffchen/Sephpa
|
||||
*
|
||||
*/
|
||||
$xml = $directDebitFile->generateOutput(['zipToOneFile' => false])[0]['data'];
|
||||
|
||||
$sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $xml);
|
||||
$fints->execute($sendSEPADirectDebit);
|
||||
if ($sendSEPADirectDebit->needsTan()) {
|
||||
handleStrongAuthentication($sendSEPADirectDebit); // See login.php for the implementation.
|
||||
}
|
||||
67
vendor/nemiah/php-fints/Samples/directDebit_phpSepaXml.php
vendored
Executable file
67
vendor/nemiah/php-fints/Samples/directDebit_phpSepaXml.php
vendored
Executable file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUndefinedNamespaceInspection */
|
||||
/** @noinspection PhpUndefinedMethodInspection */
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Send a direct debit request
|
||||
*
|
||||
* Note: The phpFinTs library only implements the FinTS protocol. For SEPA transfers, you need a separate library to
|
||||
* produce the SEPA XML data, which is then wrapped into FinTS requests. This example uses the phpSepaXml library
|
||||
* (see https://github.com/nemiah/phpSepaXml), which you need to install separately.
|
||||
*/
|
||||
|
||||
use nemiah\phpSepaXml\SEPACreditor;
|
||||
use nemiah\phpSepaXml\SEPADebitor;
|
||||
use nemiah\phpSepaXml\SEPADirectDebitBasic;
|
||||
|
||||
// See login.php, it returns a FinTs instance that is already logged in.
|
||||
/** @var \Fhp\FinTs $fints */
|
||||
$fints = require_once 'login.php';
|
||||
|
||||
$dt = new \DateTime();
|
||||
$dt->add(new \DateInterval('P1D'));
|
||||
|
||||
$sepaDD = new SEPADirectDebitBasic([
|
||||
'messageID' => time(),
|
||||
'paymentID' => time()
|
||||
]);
|
||||
|
||||
$sepaDD->setCreditor(new SEPACreditor([ //this is you
|
||||
'name' => 'My Company',
|
||||
'iban' => 'DE68210501700012345678',
|
||||
'bic' => 'DEUTDEDB400',
|
||||
'identifier' => 'DE98ZZZ09999999999',
|
||||
]));
|
||||
|
||||
$sepaDD->addDebitor(new SEPADebitor([ //this is who you want to get money from
|
||||
'transferID' => 'R20170100',
|
||||
'mandateID' => 'aeicznaeibcnt',
|
||||
'mandateDateOfSignature' => '2017-05-05',
|
||||
'name' => 'Max Mustermann',
|
||||
'iban' => 'CH9300762011623852957',
|
||||
'bic' => 'GENODEF1P15',
|
||||
'amount' => 48.78,
|
||||
'currency' => 'EUR',
|
||||
'info' => 'R20170100 vom 09.05.2017',
|
||||
'requestedCollectionDate' => $dt,
|
||||
'sequenceType' => 'OOFF',
|
||||
'type' => 'CORE',
|
||||
]));
|
||||
|
||||
// Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
|
||||
// hard-coded and not call getSEPAAccounts() at all.
|
||||
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
|
||||
$fints->execute($getSepaAccounts);
|
||||
if ($getSepaAccounts->needsTan()) {
|
||||
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
|
||||
}
|
||||
$oneAccount = $getSepaAccounts->getAccounts()[0];
|
||||
|
||||
$sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $sepaDD->toXML('pain.008.001.02'));
|
||||
$fints->execute($sendSEPADirectDebit);
|
||||
if ($sendSEPADirectDebit->needsTan()) {
|
||||
handleStrongAuthentication($sendSEPADirectDebit); // See login.php for the implementation.
|
||||
}
|
||||
24
vendor/nemiah/php-fints/Samples/init.php
vendored
Executable file
24
vendor/nemiah/php-fints/Samples/init.php
vendored
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Creates a new FinTs instance. This file mainly contains the configuration data for the phpFInTS library.
|
||||
*/
|
||||
require '../vendor/autoload.php';
|
||||
|
||||
// The configuration options up here are considered static wrt. the library's internal state and its requests.
|
||||
// That is, even if you persist the FinTs instance, you need to be able to reproduce all this information from some
|
||||
// application-specific storage (e.g. your database) in order to use the phpFinTS library.
|
||||
$options = new \Fhp\Options\FinTsOptions();
|
||||
$options->url = ''; // HBCI / FinTS Url can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm (use the PIN/TAN URL)
|
||||
$options->bankCode = ''; // Your bank code / Bankleitzahl
|
||||
$options->productName = ''; // The number you receive after registration / FinTS-Registrierungsnummer
|
||||
$options->productVersion = '1.0'; // Your own Software product version
|
||||
$credentials = \Fhp\Options\Credentials::create('username', 'pin'); // This is NOT the PIN of your bank card!
|
||||
$fints = \Fhp\FinTs::new($options, $credentials);
|
||||
$fints->setLogger(new \Tests\Fhp\CLILogger());
|
||||
|
||||
// Usage:
|
||||
// $fints = require_once 'init.php';
|
||||
return $fints;
|
||||
236
vendor/nemiah/php-fints/Samples/login.php
vendored
Executable file
236
vendor/nemiah/php-fints/Samples/login.php
vendored
Executable file
|
|
@ -0,0 +1,236 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
use Fhp\CurlException;
|
||||
use Fhp\Protocol\ServerException;
|
||||
use Fhp\Protocol\UnexpectedResponseException;
|
||||
|
||||
/**
|
||||
* SAMPLE - Creates a new FinTs instance (init.php) and makes sure its logged in.
|
||||
*/
|
||||
|
||||
/** @var \Fhp\FinTs $fints */
|
||||
$fints = require_once 'init.php';
|
||||
|
||||
/**
|
||||
* This function as well as handleTan() and handleDecoupled() below are key to how FinTS works in times of PSD2
|
||||
* regulations.
|
||||
* Most actions like wire transfers, getting statements and even logging in can ask for strong authentication (a TAN or
|
||||
* some form of confirmation on a "decoupled" device that the user has access to), but won't always. Whether strong
|
||||
* authentication required depends on the kind of action, when it was last executed, other parameters like the amount
|
||||
* (of a wire transfer) or time span (of a statement request) and generally the security concept of the particular bank.
|
||||
* The authentication requirements may or may not be consistent with the kinds of authentication that the same bank
|
||||
* requires for the same action in the web-based online banking interface. Also, banks may change these requirements
|
||||
* over time, so just because your particular bank does or does not need a TAN for login today does not mean that it
|
||||
* stays that way. There is a general tendency towards less intrusive strong authentication, i.e. requiring it for fewer
|
||||
* actions (based on heuristics), less often (e.g. only every 90 days) or in a decoupled mode where the user only needs
|
||||
* to tap a single button.
|
||||
*
|
||||
* The strong authentification can be implemented in many different ways. Each application that uses the phpFinTS
|
||||
* library has to implement its own way of asking users for a TAN or for decoupled confirmation, which varies depending
|
||||
* on its user interfaces. The implementation does not have to be in a single function like this -- it can be inlined
|
||||
* with the calling code, or live elsewhere. The TAN/confirmation can be obtained while the same PHP script is still
|
||||
* running (i.e. handleStrongAuthentication() is a blocking function that only returns once the authentication is done,
|
||||
* which is useful for a CLI application), but it is also possible to interrupt the PHP process entirely while asking
|
||||
* for the TAN/confirmation and resume it later (which is useful for a web application).
|
||||
*
|
||||
* @param \Fhp\BaseAction $action Some action that requires strong authentication.
|
||||
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
|
||||
*/
|
||||
function handleStrongAuthentication(\Fhp\BaseAction $action)
|
||||
{
|
||||
global $fints;
|
||||
if ($fints->getSelectedTanMode()->isDecoupled()) {
|
||||
handleDecoupled($action);
|
||||
} else {
|
||||
handleTan($action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function handles strong authentication for the case where the user needs to enter a TAN into the PHP
|
||||
* application.
|
||||
*
|
||||
* @param \Fhp\BaseAction $action Some action that requires a TAN.
|
||||
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
|
||||
*/
|
||||
function handleTan(Fhp\BaseAction $action)
|
||||
{
|
||||
global $fints, $options, $credentials;
|
||||
|
||||
// Find out what sort of TAN we need, tell the user about it.
|
||||
$tanRequest = $action->getTanRequest();
|
||||
echo 'The bank requested a TAN.';
|
||||
if ($tanRequest->getChallenge() !== null) {
|
||||
echo ' Instructions: ' . $tanRequest->getChallenge();
|
||||
}
|
||||
echo "\n";
|
||||
if ($tanRequest->getTanMediumName() !== null) {
|
||||
echo 'Please use this device: ' . $tanRequest->getTanMediumName() . "\n";
|
||||
}
|
||||
|
||||
// Challenge Image for PhotoTan/ChipTan
|
||||
if ($tanRequest->getChallengeHhdUc()) {
|
||||
try {
|
||||
$flicker = new \Fhp\Model\FlickerTan\TanRequestChallengeFlicker($tanRequest->getChallengeHhdUc());
|
||||
echo 'There is a challenge flicker.' . PHP_EOL;
|
||||
// save or output svg
|
||||
$flickerPattern = $flicker->getFlickerPattern();
|
||||
// other renderers can be implemented with this pattern
|
||||
$svg = new \Fhp\Model\FlickerTan\SvgRenderer($flickerPattern);
|
||||
echo $svg->getImage();
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// was not a flicker
|
||||
$challengeImage = new \Fhp\Model\TanRequestChallengeImage(
|
||||
$tanRequest->getChallengeHhdUc()
|
||||
);
|
||||
echo 'There is a challenge image.' . PHP_EOL;
|
||||
// Save the challenge image somewhere
|
||||
// Alternative: HTML sample code
|
||||
echo '<img src="data:' . htmlspecialchars($challengeImage->getMimeType()) . ';base64,' . base64_encode($challengeImage->getData()) . '" />' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: Instead of printing the above to the console, you can relay the information (challenge and TAN medium)
|
||||
// to the user in any other way (through your REST API, a push notification, ...). If waiting for the TAN requires
|
||||
// you to interrupt this PHP process and the TAN will arrive in a fresh (HTTP/REST/...) request, you can do so:
|
||||
if ($optionallyPersistEverything = false) {
|
||||
$persistedAction = serialize($action);
|
||||
$persistedFints = $fints->persist();
|
||||
|
||||
// These are two strings (watch out, they are NOT necessarily UTF-8 encoded), which you can store anywhere.
|
||||
// This example code stores them in a text file, but you might write them to your database (use a BLOB, not a
|
||||
// CHAR/TEXT field to allow for arbitrary encoding) or in some other storage (possibly base64-encoded to make it
|
||||
// ASCII).
|
||||
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
|
||||
}
|
||||
|
||||
// Ask the user for the TAN. ----------------------------------------------------------------------------------------
|
||||
// IMPORTANT: In your real application, you cannot use fgets(STDIN) of course (unless you're running PHP only as a
|
||||
// command line application). So you instead want to send a response to the user. This means that, after executing
|
||||
// the first half of handleTan() above, your real application will terminate the PHP process. The second half of
|
||||
// handleTan() will likely live elsewhere in your application code (i.e. you will have two functions for the TAN
|
||||
// handling, not just one like in this simplified example). You *only* need to carry over the $persistedInstance
|
||||
// and the $persistedAction (which are simple strings) by storing them in some database or file where you can load
|
||||
// them again in a new PHP process when the user sends the TAN.
|
||||
echo "Please enter the TAN:\n";
|
||||
$tan = trim(fgets(STDIN));
|
||||
|
||||
// Optional: If the state was persisted above, we can restore it now (imagine this is a new PHP process).
|
||||
if ($optionallyPersistEverything) {
|
||||
$restoredState = file_get_contents(__DIR__ . '/state.txt');
|
||||
list($persistedInstance, $persistedAction) = unserialize($restoredState);
|
||||
$fints = \Fhp\FinTs::new($options, $credentials, $persistedInstance);
|
||||
$action = unserialize($persistedAction);
|
||||
}
|
||||
|
||||
echo "Submitting TAN: $tan\n";
|
||||
$fints->submitTan($action, $tan);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function handles strong authentication for the case where the user needs to confirm the action on another
|
||||
* device. Note: Depending on the banks you need compatibility with, you may not need to implement decoupled
|
||||
* authentication at all, i.e., you could filter out any decoupled TanModes when letting the user choose.
|
||||
*
|
||||
* @param \Fhp\BaseAction $action Some action that requires decoupled authentication.
|
||||
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
|
||||
*/
|
||||
function handleDecoupled(Fhp\BaseAction $action)
|
||||
{
|
||||
global $fints, $options, $credentials;
|
||||
|
||||
$tanMode = $fints->getSelectedTanMode();
|
||||
$tanRequest = $action->getTanRequest();
|
||||
echo 'The bank requested authentication on another device.';
|
||||
if ($tanRequest->getChallenge() !== null) {
|
||||
echo ' Instructions: ' . $tanRequest->getChallenge();
|
||||
}
|
||||
echo "\n";
|
||||
if ($tanRequest->getTanMediumName() !== null) {
|
||||
echo 'Please check this device: ' . $tanRequest->getTanMediumName() . "\n";
|
||||
}
|
||||
|
||||
// Just like in handleTan() above, we have the option to interrupt the PHP process at this point. In fact, the
|
||||
// for-loop below that deals with the polling may even be running on the client side of your larger application,
|
||||
// polling your application server regularly, which spawns a new PHP process each time. Here, we demonstrate this by
|
||||
// persisting the instance to a local file and restoring it (even though that's not technically necessary for a
|
||||
// single-process CLI script like this).
|
||||
if ($optionallyPersistEverything = false) {
|
||||
$persistedAction = serialize($action);
|
||||
$persistedFints = $fints->persist();
|
||||
// See handleTan() for how to deal with this in practice.
|
||||
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
|
||||
}
|
||||
|
||||
// IMPORTANT: In your real application, you don't have to use sleep() in PHP. You can persist the state in the same
|
||||
// way as in handleTan() and restore it later. This allows you to use some other timer mechanism (e.g. in the user's
|
||||
// browser). This PHP sample code just serves to show the *logic* of the polling. Alternatively, you can even do
|
||||
// without polling entirely and just let the user confirm manually in all cases (i.e. only implement the `else`
|
||||
// branch below).
|
||||
if ($tanMode->allowsAutomatedPolling()) {
|
||||
echo "Polling server to detect when the decoupled authentication is complete.\n";
|
||||
sleep($tanMode->getFirstDecoupledCheckDelaySeconds());
|
||||
for ($attempt = 0;
|
||||
$tanMode->getMaxDecoupledChecks() === 0 || $attempt < $tanMode->getMaxDecoupledChecks();
|
||||
++$attempt
|
||||
) {
|
||||
// Optional: If the state was persisted above, we can restore it now (imagine this is a new PHP process).
|
||||
if ($optionallyPersistEverything) {
|
||||
$restoredState = file_get_contents(__DIR__ . '/state.txt');
|
||||
list($persistedInstance, $persistedAction) = unserialize($restoredState);
|
||||
$fints = \Fhp\FinTs::new($options, $credentials, $persistedInstance);
|
||||
$action = unserialize($persistedAction);
|
||||
}
|
||||
|
||||
// Check if we're done.
|
||||
if ($fints->checkDecoupledSubmission($action)) {
|
||||
echo "Confirmed.\n";
|
||||
return;
|
||||
}
|
||||
echo "Still waiting...\n";
|
||||
|
||||
// THIS IS CRUCIAL if you're using persistence in between polls. You must re-persist() the instance after
|
||||
// calling checkDecoupledSubmission() and before calling it the next time. Don't reuse the
|
||||
// $persistedInstance from above multiple times.
|
||||
if ($optionallyPersistEverything) {
|
||||
$persistedAction = serialize($action);
|
||||
$persistedFints = $fints->persist();
|
||||
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
|
||||
}
|
||||
|
||||
sleep($tanMode->getPeriodicDecoupledCheckDelaySeconds());
|
||||
}
|
||||
throw new RuntimeException("Not confirmed after $attempt attempts, which is the limit.");
|
||||
} elseif ($tanMode->allowsManualConfirmation()) {
|
||||
echo "Please type 'done' and hit Return when you've completed the authentication on the other device.\n";
|
||||
while (trim(fgets(STDIN)) !== 'done') {
|
||||
echo "Try again.\n";
|
||||
}
|
||||
echo "Confirming that the action is done.\n";
|
||||
if (!$fints->checkDecoupledSubmission($action)) {
|
||||
throw new RuntimeException(
|
||||
"You confirmed that the authentication for action was copmleted, but the server does not think so."
|
||||
);
|
||||
}
|
||||
echo "Confirmed\n";
|
||||
} else {
|
||||
throw new AssertionError('Server allows neither automated polling nor manual confirmation');
|
||||
}
|
||||
}
|
||||
|
||||
// Select TAN mode and possibly medium. If you're not sure what this is about, read and run tanModesAndMedia.php first.
|
||||
$tanMode = 900; // This is just a placeholder you need to fill!
|
||||
$tanMedium = null; // This is just a placeholder you may need to fill.
|
||||
$fints->selectTanMode($tanMode, $tanMedium);
|
||||
|
||||
// Log in.
|
||||
$login = $fints->login();
|
||||
if ($login->needsTan()) {
|
||||
handleStrongAuthentication($login);
|
||||
}
|
||||
|
||||
// Usage:
|
||||
// $fints = require_once 'login.php';
|
||||
return $fints;
|
||||
47
vendor/nemiah/php-fints/Samples/statementOfAccount.php
vendored
Executable file
47
vendor/nemiah/php-fints/Samples/statementOfAccount.php
vendored
Executable file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Displays the statement of account for a specific time range and account.
|
||||
*/
|
||||
|
||||
// See login.php, it returns a FinTs instance that is already logged in.
|
||||
/** @var \Fhp\FinTs $fints */
|
||||
$fints = require_once 'login.php';
|
||||
|
||||
// Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
|
||||
// hard-coded and not call getSEPAAccounts() at all.
|
||||
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
|
||||
$fints->execute($getSepaAccounts);
|
||||
if ($getSepaAccounts->needsTan()) {
|
||||
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
|
||||
}
|
||||
$oneAccount = $getSepaAccounts->getAccounts()[0];
|
||||
|
||||
$from = new \DateTime('2022-07-15');
|
||||
$to = new \DateTime();
|
||||
$getStatement = \Fhp\Action\GetStatementOfAccount::create($oneAccount, $from, $to, false, true);
|
||||
$fints->execute($getStatement);
|
||||
if ($getStatement->needsTan()) {
|
||||
handleStrongAuthentication($getStatement); // See login.php for the implementation.
|
||||
}
|
||||
|
||||
$soa = $getStatement->getStatement();
|
||||
foreach ($soa->getStatements() as $statement) {
|
||||
echo $statement->getDate()->format('Y-m-d') . ': Start Saldo: '
|
||||
. ($statement->getCreditDebit() == \Fhp\Model\StatementOfAccount\Statement::CD_DEBIT ? '-' : '')
|
||||
. $statement->getStartBalance() . PHP_EOL;
|
||||
echo 'Transactions:' . PHP_EOL;
|
||||
echo '=======================================' . PHP_EOL;
|
||||
foreach ($statement->getTransactions() as $transaction) {
|
||||
echo "Booked : " . ($transaction->getBooked() ? "true" : "false") . PHP_EOL;
|
||||
echo 'Amount : ' . ($transaction->getCreditDebit() == \Fhp\Model\StatementOfAccount\Transaction::CD_DEBIT ? '-' : '') . $transaction->getAmount() . PHP_EOL;
|
||||
echo 'Booking text: ' . $transaction->getBookingText() . PHP_EOL;
|
||||
echo 'Name : ' . $transaction->getName() . PHP_EOL;
|
||||
echo 'Description : ' . $transaction->getMainDescription() . PHP_EOL;
|
||||
echo 'EREF : ' . $transaction->getEndToEndID() . PHP_EOL;
|
||||
echo '=======================================' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
}
|
||||
echo 'Found ' . count($soa->getStatements()) . ' statements.' . PHP_EOL;
|
||||
40
vendor/nemiah/php-fints/Samples/statementOfHoldings.php
vendored
Executable file
40
vendor/nemiah/php-fints/Samples/statementOfHoldings.php
vendored
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Displays the statement of account for a specific depot.
|
||||
*/
|
||||
|
||||
// See login.php, it returns a FinTs instance that is already logged in.
|
||||
/** @var \Fhp\FinTs $fints */
|
||||
$fints = require_once 'login.php';
|
||||
|
||||
// Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
|
||||
// hard-coded and not call getSEPAAccounts() at all.
|
||||
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
|
||||
$fints->execute($getSepaAccounts);
|
||||
if ($getSepaAccounts->needsTan()) {
|
||||
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
|
||||
}
|
||||
$oneAccount = $getSepaAccounts->getAccounts()[0];
|
||||
|
||||
$getStatement = \Fhp\Action\GetDepotAufstellung::create($oneAccount);
|
||||
$fints->execute($getStatement);
|
||||
if ($getStatement->needsTan()) {
|
||||
handleStrongAuthentication($getStatement); // See login.php for the implementation.
|
||||
}
|
||||
|
||||
$soa = $getStatement->getStatement();
|
||||
foreach ($soa->getHoldings() as $holding) {
|
||||
echo '=======================================' . PHP_EOL;
|
||||
echo 'Name : ' . $holding->getName() . PHP_EOL;
|
||||
echo 'Amount : ' . $holding->getAmount() . PHP_EOL;
|
||||
echo 'Price : ' . $holding->getPrice() . ' ' . $holding->getCurrency() . PHP_EOL;
|
||||
echo 'WKN : ' . $holding->getWKN() . PHP_EOL;
|
||||
echo 'ISIN : ' . $holding->getISIN() . PHP_EOL;
|
||||
echo 'B-Datum : ' . $holding->getDate()->format('Y-m-d') . PHP_EOL;
|
||||
echo '=======================================' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
echo 'Found ' . count($soa->getHoldings()) . ' statements.' . PHP_EOL;
|
||||
echo 'Depotwert: ' . $getStatement->getDepotWert() . PHP_EOL;
|
||||
71
vendor/nemiah/php-fints/Samples/tanModesAndMedia.php
vendored
Executable file
71
vendor/nemiah/php-fints/Samples/tanModesAndMedia.php
vendored
Executable file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Creates a new FinTs instance (init.php) and lets the user select the TAN mode they want to use.
|
||||
*/
|
||||
|
||||
/** @var \Fhp\FinTs $fints */
|
||||
$fints = require_once 'init.php';
|
||||
|
||||
// First, the user has to decide which TAN mode they want to use.
|
||||
// NOTE: There is a special case for banks that do not support PSD2, use NoPsd2TanMode for those.
|
||||
$tanModes = $fints->getTanModes();
|
||||
if (empty($tanModes)) {
|
||||
echo 'Your bank does not support any TAN modes!';
|
||||
return;
|
||||
}
|
||||
|
||||
echo "Here are the available TAN modes:\n";
|
||||
$tanModeNames = array_map(function (\Fhp\Model\TanMode $tanMode) {
|
||||
return $tanMode->getName();
|
||||
}, $tanModes);
|
||||
print_r($tanModeNames);
|
||||
|
||||
echo "Which one do you want to use? Index:\n";
|
||||
$tanModeIndex = trim(fgets(STDIN));
|
||||
if (!is_numeric($tanModeIndex) || !array_key_exists(intval($tanModeIndex), $tanModes)) {
|
||||
echo 'Invalid index!';
|
||||
return;
|
||||
}
|
||||
$tanMode = $tanModes[intval($tanModeIndex)];
|
||||
echo 'You selected ' . $tanMode->getName() . "\n";
|
||||
|
||||
// In case the selected TAN mode requires a TAN medium (e.g. if the user picked mTAN, they may have to pick the mobile
|
||||
// device on which they want to receive TANs), let the user pick that too.
|
||||
if ($tanMode->needsTanMedium()) {
|
||||
$tanMedia = $fints->getTanMedia($tanMode);
|
||||
if (empty($tanMedia)) {
|
||||
echo 'Your bank did not provide any TAN media, even though it requires selecting one!';
|
||||
return;
|
||||
}
|
||||
|
||||
echo "Here are the available TAN media:\n";
|
||||
$tanMediaNames = array_map(function (\Fhp\Model\TanMedium $tanMedium) {
|
||||
return $tanMedium->getName();
|
||||
}, $tanMedia);
|
||||
print_r($tanMediaNames);
|
||||
|
||||
echo "Which one do you want to use? Index:\n";
|
||||
$tanMediumIndex = trim(fgets(STDIN));
|
||||
if (!is_numeric($tanMediumIndex) || !array_key_exists(intval($tanMediumIndex), $tanMedia)) {
|
||||
echo 'Invalid index!';
|
||||
return;
|
||||
}
|
||||
$tanMedium = $tanMedia[intval($tanMediumIndex)];
|
||||
echo 'You selected ' . $tanMedium->getName() . "\n";
|
||||
} else {
|
||||
$tanMedium = null;
|
||||
}
|
||||
|
||||
// Announce the selection to the FinTS library.
|
||||
$fints->selectTanMode($tanMode, $tanMedium);
|
||||
|
||||
// Within your application, you should persist these choices somewhere (e.g. database), so that the user does not have
|
||||
// to select them again the future. Note that it is sufficient to persist the ID/name, i.e. this is equivalent:
|
||||
$fints->selectTanMode($tanMode->getId(), $tanMedium->getName());
|
||||
|
||||
// Now you could do $fints->login(), see login.php for that. For this example, we'll just close the connection.
|
||||
$fints->close();
|
||||
echo 'Done';
|
||||
54
vendor/nemiah/php-fints/Samples/transfer.php
vendored
Executable file
54
vendor/nemiah/php-fints/Samples/transfer.php
vendored
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
/** @noinspection PhpUndefinedMethodInspection */
|
||||
/** @noinspection PhpUndefinedNamespaceInspection */
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
|
||||
/**
|
||||
* SAMPLE - Execute a SEPA transfer.
|
||||
*
|
||||
* Note: The phpFinTs library only implements the FinTS protocol. For SEPA transfers, you need a separate library to
|
||||
* produce the SEPA XML data, which is then wrapped into FinTS requests. This example uses the phpSepaXml library
|
||||
* (see https://github.com/nemiah/phpSepaXml), which you need to install separately.
|
||||
*/
|
||||
|
||||
use nemiah\phpSepaXml\SEPACreditor;
|
||||
use nemiah\phpSepaXml\SEPADebitor;
|
||||
use nemiah\phpSepaXml\SEPATransfer;
|
||||
|
||||
// See login.php, it returns a FinTs instance that is already logged in.
|
||||
/** @var \Fhp\FinTs $fints */
|
||||
$fints = require_once 'login.php';
|
||||
|
||||
$dt = new \DateTime();
|
||||
$dt->add(new \DateInterval('P1D'));
|
||||
|
||||
$sepaDD = new SEPATransfer([
|
||||
'messageID' => time(),
|
||||
'paymentID' => time(),
|
||||
]);
|
||||
|
||||
$sepaDD->setDebitor(new SEPADebitor([ //this is you
|
||||
'name' => 'My Company',
|
||||
'iban' => 'DE68210501700012345678',
|
||||
'bic' => 'DEUTDEDB400', //,
|
||||
//'identifier' => 'DE98ZZZ09999999999'
|
||||
]));
|
||||
|
||||
$sepaDD->addCreditor(new SEPACreditor([ //this is who you want to send money to
|
||||
//'paymentID' => '20170403652',
|
||||
'info' => '20170403652',
|
||||
'name' => 'Max Mustermann',
|
||||
'iban' => 'CH9300762011623852957',
|
||||
'bic' => 'GENODEF1P15',
|
||||
'amount' => 48.78,
|
||||
'currency' => 'EUR',
|
||||
'reqestedExecutionDate' => $dt,
|
||||
]));
|
||||
|
||||
$sendSEPATransfer = \Fhp\Action\SendSEPATransfer::create($oneAccount, $sepaDD->toXML());
|
||||
$fints->execute($sendSEPATransfer);
|
||||
if ($sendSEPATransfer->needsTan()) {
|
||||
handleStrongAuthentication($sendSEPATransfer); // See login.php for the implementation.
|
||||
}
|
||||
39
vendor/nemiah/php-fints/composer.json
vendored
Executable file
39
vendor/nemiah/php-fints/composer.json
vendored
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "nemiah/php-fints",
|
||||
"description": "PHP Library for the protocols fints and hbci",
|
||||
"homepage": "https://github.com/nemiah/phpFinTS",
|
||||
"version": "3.7.0",
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Fhp": "lib/",
|
||||
"Tests\\Fhp": "lib/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"ext-curl": "*",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"php-mock/php-mock-phpunit": "^2.6",
|
||||
"friendsofphp/php-cs-fixer": "^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"monolog/monolog": "Allow sending log messages to a variety of different handlers",
|
||||
"abcaeffchen/sephpa": "1.*",
|
||||
"nemiah/php-sepa-xml": "dev-master"
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"phpunit"
|
||||
],
|
||||
"check": [
|
||||
"@cs"
|
||||
],
|
||||
"cs": "php-cs-fixer fix -v --diff --dry-run",
|
||||
"cs-fix": "php-cs-fixer fix -v --diff"
|
||||
}
|
||||
}
|
||||
36
vendor/nemiah/php-fints/csfixer-check.sh
vendored
Executable file
36
vendor/nemiah/php-fints/csfixer-check.sh
vendored
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# When this is run as part of a Travis test for a pull request, then it ensures that none of the touched files has any
|
||||
# PHP CS Fixer warnings.
|
||||
# From: https://github.com/FriendsOfPHP/PHP-CS-Fixer#using-php-cs-fixer-on-ci
|
||||
|
||||
if [ -z "$TRAVIS_COMMIT_RANGE" ]
|
||||
then
|
||||
# TRAVIS_COMMIT_RANGE "is empty for builds triggered by the initial commit of a new branch"
|
||||
# From: https://docs.travis-ci.com/user/environment-variables/
|
||||
echo "Variable TRAVIS_COMMIT_RANGE not set, falling back to full git diff"
|
||||
TRAVIS_COMMIT_RANGE=.
|
||||
fi
|
||||
|
||||
IFS='
|
||||
'
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB "$TRAVIS_COMMIT_RANGE")
|
||||
if [ "$?" -ne "0" ]
|
||||
then
|
||||
echo "Error: git diff response code > 0, aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${CHANGED_FILES}" ]
|
||||
then
|
||||
echo "0 changed files found, exiting"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# February 2022: PHP CS FIXER is currently not PHP 8.1 compatible:
|
||||
# "you may experience code modified in a wrong way"
|
||||
# "To ignore this requirement please set `PHP_CS_FIXER_IGNORE_ENV`."
|
||||
export PHP_CS_FIXER_IGNORE_ENV="1"
|
||||
|
||||
if ! echo "${CHANGED_FILES}" | grep -qE "^(\\.php_cs(\\.dist)?|composer\\.lock)$"; then EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "${CHANGED_FILES}"); else EXTRA_ARGS=''; fi
|
||||
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php -v --dry-run --stop-on-violation --using-cache=no ${EXTRA_ARGS} || (echo "php-cs-fixer failed" && exit 1)
|
||||
37
vendor/nemiah/php-fints/disallowtabs.sh
vendored
Executable file
37
vendor/nemiah/php-fints/disallowtabs.sh
vendored
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# When this is run as part of a Travis test for a pull request, then it ensures that none of the added lines (compared
|
||||
# to the base branch of the pull request) use tabs for indentations.
|
||||
# Adapted from https://github.com/mrc/git-hook-library/blob/master/pre-commit.no-tabs
|
||||
|
||||
# Abort if any of the inner commands (particularly the git commands) fails.
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ -z ${TRAVIS_PULL_REQUEST} ]; then
|
||||
echo "Expected environment variable TRAVIS_PULL_REQUEST"
|
||||
exit 2
|
||||
elif [ "${TRAVIS_PULL_REQUEST}" == "false" ]; then
|
||||
echo "Not a Travis pull request, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Make sure that we have a local copy of the relevant commits (otherwise git diff won't work).
|
||||
git remote set-branches --add origin ${TRAVIS_BRNACH}
|
||||
git fetch
|
||||
|
||||
# Compute the diff from the PR's target branch to its HEAD commit.
|
||||
target_branch="origin/${TRAVIS_BRANCH}"
|
||||
the_diff=$(git diff "${target_branch}...HEAD")
|
||||
|
||||
# Make sure that there are no tabs in the indentation part of added lines.
|
||||
if echo "${the_diff}" | egrep '^\+\s* ' >/dev/null; then
|
||||
echo -e "\e[31mError: The changes contain a tab for indentation\e[0m, which is against this repo's policy."
|
||||
echo "Target branch: origin/${TRAVIS_BRANCH}"
|
||||
echo "Commit range: ${TRAVIS_COMMIT_RANGE}"
|
||||
echo "The following tabs were detected:"
|
||||
echo "${the_diff}" | egrep '^(\+\s* |\+\+\+|@@)'
|
||||
exit 1
|
||||
else
|
||||
echo "No new tabs detected."
|
||||
fi
|
||||
129
vendor/nemiah/php-fints/lib/Fhp/Action/GetBalance.php
vendored
Executable file
129
vendor/nemiah/php-fints/lib/Fhp/Action/GetBalance.php
vendored
Executable file
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\PaginateableAction;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\Message;
|
||||
use Fhp\Protocol\UnexpectedResponseException;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\BaseSegment;
|
||||
use Fhp\Segment\Common\Kti;
|
||||
use Fhp\Segment\Common\Kto;
|
||||
use Fhp\Segment\Common\KtvV3;
|
||||
use Fhp\Segment\SAL\HISAL;
|
||||
use Fhp\Segment\SAL\HKSALv4;
|
||||
use Fhp\Segment\SAL\HKSALv5;
|
||||
use Fhp\Segment\SAL\HKSALv6;
|
||||
use Fhp\Segment\SAL\HKSALv7;
|
||||
use Fhp\UnsupportedException;
|
||||
|
||||
/**
|
||||
* Runs an HKSAL request the current balance of the given account.
|
||||
*/
|
||||
class GetBalance extends PaginateableAction
|
||||
{
|
||||
// Request (not available after serialization, i.e. not available in processResponse()).
|
||||
/** @var SEPAAccount */
|
||||
private $account;
|
||||
/** @var bool */
|
||||
private $allAccounts;
|
||||
|
||||
// Response
|
||||
/** @var HISAL[] */
|
||||
private $response = [];
|
||||
|
||||
/**
|
||||
* @param SEPAAccount $account The account to get the balance for. This can be constructed based on information
|
||||
* that the user entered, or it can be {@link SEPAAccount} instance retrieved from {@link GetSEPAAccounts}.
|
||||
* @param bool $allAccounts If set to true, will return balances for all accounts of the user. You still need to
|
||||
* pass one of the accounts into $account, though.
|
||||
*/
|
||||
public static function create(SEPAAccount $account, bool $allAccounts = false): GetBalance
|
||||
{
|
||||
$result = new GetBalance();
|
||||
$result->account = $account;
|
||||
$result->allAccounts = $allAccounts;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*/
|
||||
public function serialize(): string
|
||||
{
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
parent::__serialize(),
|
||||
$this->account, $this->allAccounts,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*
|
||||
* @param string $serialized
|
||||
* @return void
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
self::__unserialize(unserialize($serialized));
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void
|
||||
{
|
||||
list(
|
||||
$parentSerialized,
|
||||
$this->account, $this->allAccounts
|
||||
) = $serialized;
|
||||
|
||||
is_array($parentSerialized) ?
|
||||
parent::__unserialize($parentSerialized) :
|
||||
parent::unserialize($parentSerialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HISAL[]
|
||||
*/
|
||||
public function getBalances()
|
||||
{
|
||||
$this->ensureDone();
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
/** @var BaseSegment $hisals */
|
||||
$hisals = $bpd->requireLatestSupportedParameters('HISALS');
|
||||
switch ($hisals->getVersion()) {
|
||||
case 4:
|
||||
return HKSALv4::create(Kto::fromAccount($this->account));
|
||||
case 5:
|
||||
return HKSALv5::create(KtvV3::fromAccount($this->account), $this->allAccounts);
|
||||
case 6:
|
||||
return HKSALv6::create(KtvV3::fromAccount($this->account), $this->allAccounts);
|
||||
case 7:
|
||||
return HKSALv7::create(Kti::fromAccount($this->account), $this->allAccounts);
|
||||
default:
|
||||
throw new UnsupportedException('Unsupported HKSAL version: ' . $hisals->getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function processResponse(Message $response)
|
||||
{
|
||||
parent::processResponse($response);
|
||||
|
||||
$responseSegments = $response->findSegments(HISAL::class);
|
||||
if (count($responseSegments) === 0) {
|
||||
throw new UnexpectedResponseException('No HISAL segments received!');
|
||||
}
|
||||
$this->response = array_merge($this->response, $responseSegments);
|
||||
}
|
||||
}
|
||||
167
vendor/nemiah/php-fints/lib/Fhp/Action/GetDepotAufstellung.php
vendored
Executable file
167
vendor/nemiah/php-fints/lib/Fhp/Action/GetDepotAufstellung.php
vendored
Executable file
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\Model\StatementOfHoldings\StatementOfHoldings;
|
||||
use Fhp\MT535\MT535;
|
||||
use Fhp\PaginateableAction;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\Message;
|
||||
use Fhp\Protocol\UnexpectedResponseException;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\Common\KtvV3;
|
||||
use Fhp\Segment\HIRMS\Rueckmeldungscode;
|
||||
use Fhp\Segment\WPD\HIWPD;
|
||||
use Fhp\Segment\WPD\HIWPDS;
|
||||
use Fhp\Segment\WPD\HIWPDv5;
|
||||
use Fhp\Segment\WPD\HKWPDv5;
|
||||
use Fhp\UnsupportedException;
|
||||
|
||||
/**
|
||||
* Depotaufstellung HKWPD
|
||||
* MT535
|
||||
*/
|
||||
class GetDepotAufstellung extends PaginateableAction
|
||||
{
|
||||
// Request (not available after serialization, i.e. not available in processResponse()).
|
||||
/** @var SEPAAccount */
|
||||
private $account;
|
||||
|
||||
// Response
|
||||
/** @var string */
|
||||
private $rawMT535 = '';
|
||||
|
||||
/** @var StatementOfHoldings */
|
||||
private $statement;
|
||||
|
||||
/** @var float */
|
||||
private $depotWert;
|
||||
|
||||
/**
|
||||
* @param SEPAAccount $account The account to get the statement for. This can be constructed based on information
|
||||
* that the user entered, or it can be {@link SEPAAccount} instance retrieved from {@link getAccounts()}.
|
||||
* @return GetDepotAufstellung A new action instance.
|
||||
*/
|
||||
public static function create(SEPAAccount $account): GetDepotAufstellung
|
||||
{
|
||||
$result = new GetDepotAufstellung();
|
||||
$result->account = $account;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*/
|
||||
public function serialize(): string
|
||||
{
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
parent::__serialize(),
|
||||
$this->account,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*
|
||||
* @param string $serialized
|
||||
* @return void
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
self::__unserialize(unserialize($serialized));
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void
|
||||
{
|
||||
list(
|
||||
$parentSerialized,
|
||||
$this->account
|
||||
) = $serialized;
|
||||
|
||||
is_array($parentSerialized) ?
|
||||
parent::__unserialize($parentSerialized) :
|
||||
parent::unserialize($parentSerialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The raw MT535 data received from the server.
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getRawMT535(): string
|
||||
{
|
||||
$this->ensureDone();
|
||||
return $this->rawMT535;
|
||||
}
|
||||
|
||||
public function getStatement(): StatementOfHoldings
|
||||
{
|
||||
$this->ensureDone();
|
||||
return $this->statement;
|
||||
}
|
||||
|
||||
public function getDepotWert(): float
|
||||
{
|
||||
$this->ensureDone();
|
||||
return $this->depotWert;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
/** @var HIWPDS $hiwpds */
|
||||
$hiwpds = $bpd->requireLatestSupportedParameters('HIWPDS');
|
||||
|
||||
switch ($hiwpds->getVersion()) {
|
||||
case 5:
|
||||
return HKWPDv5::create(KtvV3::fromAccount($this->account));
|
||||
default:
|
||||
throw new UnsupportedException('Unsupported HKWPD version: ' . $hiwpds->getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function processResponse(Message $response)
|
||||
{
|
||||
parent::processResponse($response);
|
||||
|
||||
$isUnavailable = $response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null;
|
||||
$responseHiwpd = $response->findSegments(HIWPDv5::class);
|
||||
|
||||
$numResponseSegments = count($responseHiwpd);
|
||||
if (!$isUnavailable && $numResponseSegments < count($this->getRequestSegmentNumbers())) {
|
||||
throw new UnexpectedResponseException("Only got $numResponseSegments HIWPD response segments!");
|
||||
}
|
||||
|
||||
/** @var HIWPD $hiwpd */
|
||||
foreach ($responseHiwpd as $hiwpd) {
|
||||
$this->rawMT535 .= $hiwpd->getDepotaufstellung()->getData();
|
||||
}
|
||||
|
||||
// Note: Pagination boundaries may cut in the middle of the MT535 data, so it is not possible to parse a partial
|
||||
// reponse before having received all pages.
|
||||
if (!$this->hasMorePages()) {
|
||||
$this->parseMt535();
|
||||
}
|
||||
}
|
||||
|
||||
private function parseMt535()
|
||||
{
|
||||
try {
|
||||
// Note: Some banks encode their MT 535 data as SWIFT/ISO-8859 like it should be according to the
|
||||
// specification, others just send UTF-8, so we try to detect it here.
|
||||
$rawMT535 = mb_detect_encoding($this->rawMT535, 'UTF-8', true) === false
|
||||
? mb_convert_encoding($this->rawMT535, 'UTF-8', 'ISO-8859-1') : $this->rawMT535;
|
||||
$parser = new MT535($rawMT535);
|
||||
$this->statement = $parser->parseHoldings();
|
||||
$this->depotWert = $parser->parseDepotWert();
|
||||
} catch (\Exception $e) {
|
||||
throw new \InvalidArgumentException('Invalid MT535 data', 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
92
vendor/nemiah/php-fints/lib/Fhp/Action/GetSEPAAccounts.php
vendored
Executable file
92
vendor/nemiah/php-fints/lib/Fhp/Action/GetSEPAAccounts.php
vendored
Executable file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\PaginateableAction;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\Message;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\BaseSegment;
|
||||
use Fhp\Segment\Common\Ktz;
|
||||
use Fhp\Segment\HIRMS\Rueckmeldungscode;
|
||||
use Fhp\Segment\SPA\HISPA;
|
||||
use Fhp\Segment\SPA\HKSPAv1;
|
||||
use Fhp\Segment\SPA\HKSPAv2;
|
||||
use Fhp\Segment\SPA\HKSPAv3;
|
||||
use Fhp\UnsupportedException;
|
||||
|
||||
/**
|
||||
* Runs an HKSPA request to retrieve account details about the accounts that the user can access through FinTs.
|
||||
*
|
||||
* TODO In future, once all banks populate the BIC in HIUPD.erweiterungKontobezogen, or if we force library users to
|
||||
* supply the BIC to us, we won't need to send an HKSPA anymore, but we can simply fulfil this action from the UPD.
|
||||
*/
|
||||
class GetSEPAAccounts extends PaginateableAction
|
||||
{
|
||||
// Empty request, in order to retrieve all accounts.
|
||||
|
||||
// Response
|
||||
/** @var SEPAAccount[] */
|
||||
private $accounts;
|
||||
|
||||
/**
|
||||
* @return GetSEPAAccounts A new action instance.
|
||||
*/
|
||||
public static function create(): GetSEPAAccounts
|
||||
{
|
||||
return new GetSEPAAccounts();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SEPAAccount[]
|
||||
*/
|
||||
public function getAccounts(): array
|
||||
{
|
||||
$this->ensureDone();
|
||||
return $this->accounts;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
/** @var BaseSegment $hispas */
|
||||
$hispas = $bpd->requireLatestSupportedParameters('HISPAS');
|
||||
switch ($hispas->getVersion()) {
|
||||
case 1:
|
||||
return HKSPAv1::createEmpty();
|
||||
case 2:
|
||||
return HKSPAv2::createEmpty();
|
||||
case 3:
|
||||
return HKSPAv3::createEmpty();
|
||||
default:
|
||||
throw new UnsupportedException('Unsupported HKSPA version: ' . $hispas->getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function processResponse(Message $response)
|
||||
{
|
||||
parent::processResponse($response);
|
||||
|
||||
// Banks send just 3010 and no HISPA in case there are no accounts (or at least none that the bank is able to
|
||||
// report through HISPA).
|
||||
if ($response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null) {
|
||||
$this->accounts = [];
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var HISPA $hispa */
|
||||
$hispa = $response->requireSegment(HISPA::class);
|
||||
$this->accounts = array_map(function ($ktz) {
|
||||
/** @var Ktz $ktz */
|
||||
$account = new SEPAAccount();
|
||||
$account->setIban($ktz->iban);
|
||||
$account->setBic($ktz->bic);
|
||||
$account->setAccountNumber($ktz->kontonummer);
|
||||
$account->setSubAccount($ktz->unterkontomerkmal);
|
||||
$account->setBlz($ktz->kreditinstitutskennung->kreditinstitutscode);
|
||||
return $account;
|
||||
}, $hispa->getSepaKontoverbindung());
|
||||
}
|
||||
}
|
||||
78
vendor/nemiah/php-fints/lib/Fhp/Action/GetSEPADirectDebitParameters.php
vendored
Executable file
78
vendor/nemiah/php-fints/lib/Fhp/Action/GetSEPADirectDebitParameters.php
vendored
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\BaseAction;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\DSE\HIDXES;
|
||||
use Fhp\Segment\DSE\MinimaleVorlaufzeitSEPALastschrift;
|
||||
|
||||
/**
|
||||
* Retrieves information about SEPA Direct Debit Requests
|
||||
*/
|
||||
class GetSEPADirectDebitParameters extends BaseAction
|
||||
{
|
||||
public const SEQUENCE_TYPES = ['FRST', 'OOFF', 'FNAL', 'RCUR'];
|
||||
public const DIRECT_DEBIT_TYPES = ['CORE', 'COR1', 'B2B'];
|
||||
|
||||
/** @var string */
|
||||
private $directDebitType;
|
||||
|
||||
/** @var string */
|
||||
private $seqType;
|
||||
|
||||
/** @var bool */
|
||||
private $singleDirectDebit;
|
||||
|
||||
/** @var HIDXES */
|
||||
private $hidxes;
|
||||
|
||||
public static function create(string $seqType, bool $singleDirectDebit, string $directDebitType = 'CORE')
|
||||
{
|
||||
if (!in_array($directDebitType, self::DIRECT_DEBIT_TYPES)) {
|
||||
throw new \InvalidArgumentException('Unknown CORE type, possible values are ' . implode(', ', self::DIRECT_DEBIT_TYPES));
|
||||
}
|
||||
if (!in_array($seqType, self::SEQUENCE_TYPES)) {
|
||||
throw new \InvalidArgumentException('Unknown SEPA sequence type, possible values are ' . implode(', ', self::SEQUENCE_TYPES));
|
||||
}
|
||||
$result = new GetSEPADirectDebitParameters();
|
||||
$result->directDebitType = $directDebitType;
|
||||
$result->seqType = $seqType;
|
||||
$result->singleDirectDebit = $singleDirectDebit;
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function getHixxesSegmentName(string $directDebitType, bool $singleDirectDebit): string
|
||||
{
|
||||
switch ($directDebitType) {
|
||||
case 'CORE':
|
||||
case 'COR1':
|
||||
return $singleDirectDebit ? 'HIDSES' : 'HIDMES';
|
||||
case 'B2B':
|
||||
return $singleDirectDebit ? 'HIBSES' : 'HIBMES';
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unknown DirectDebitTypes type, possible values are ' . implode(', ', self::DIRECT_DEBIT_TYPES));
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
$this->hidxes = $bpd->requireLatestSupportedParameters(static::getHixxesSegmentName($this->directDebitType, $this->singleDirectDebit));
|
||||
$this->isDone = true;
|
||||
return []; // No request to the bank required
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MinimaleVorlaufzeitSEPALastschrift|null The information about the lead time for the given Sequence Type and Direct Debit Type
|
||||
*/
|
||||
public function getMinimalLeadTime(): ?MinimaleVorlaufzeitSEPALastschrift
|
||||
{
|
||||
$parsed = $this->hidxes->getParameter()->getMinimalLeadTime($this->seqType);
|
||||
if ($parsed instanceof MinimaleVorlaufzeitSEPALastschrift) {
|
||||
return $parsed;
|
||||
}
|
||||
return $parsed[$this->directDebitType] ?? null;
|
||||
}
|
||||
}
|
||||
223
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccount.php
vendored
Executable file
223
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccount.php
vendored
Executable file
|
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\Model\StatementOfAccount\StatementOfAccount;
|
||||
use Fhp\MT940\Dialect\PostbankMT940;
|
||||
use Fhp\MT940\Dialect\SpardaMT940;
|
||||
use Fhp\MT940\MT940;
|
||||
use Fhp\MT940\MT940Exception;
|
||||
use Fhp\PaginateableAction;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\Message;
|
||||
use Fhp\Protocol\UnexpectedResponseException;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\Common\Kti;
|
||||
use Fhp\Segment\Common\Kto;
|
||||
use Fhp\Segment\Common\KtvV3;
|
||||
use Fhp\Segment\HIRMS\Rueckmeldungscode;
|
||||
use Fhp\Segment\KAZ\HIKAZ;
|
||||
use Fhp\Segment\KAZ\HIKAZS;
|
||||
use Fhp\Segment\KAZ\HKKAZv4;
|
||||
use Fhp\Segment\KAZ\HKKAZv5;
|
||||
use Fhp\Segment\KAZ\HKKAZv6;
|
||||
use Fhp\Segment\KAZ\HKKAZv7;
|
||||
use Fhp\UnsupportedException;
|
||||
|
||||
/**
|
||||
* Retrieves statements for one specific account or for all accounts that the user has access to. A statement is a
|
||||
* series of financial transactions that pertain to the account, grouped by day.
|
||||
*/
|
||||
class GetStatementOfAccount extends PaginateableAction
|
||||
{
|
||||
// Request (not available after serialization, i.e. not available in processResponse()).
|
||||
/** @var SEPAAccount */
|
||||
private $account;
|
||||
/** @var \DateTime */
|
||||
private $from;
|
||||
/** @var \DateTime */
|
||||
private $to;
|
||||
/** @var bool */
|
||||
private $allAccounts;
|
||||
/** @var bool */
|
||||
private $includeUnbooked;
|
||||
|
||||
// Information from the BPD needed to interpret the response.
|
||||
/** @var string */
|
||||
private $bankName;
|
||||
|
||||
// Response
|
||||
/** @var string */
|
||||
private $rawMT940 = '';
|
||||
|
||||
/** @var array */
|
||||
protected $parsedMT940 = [];
|
||||
|
||||
/** @var StatementOfAccount */
|
||||
private $statement;
|
||||
|
||||
/**
|
||||
* @param SEPAAccount $account The account to get the statement for. This can be constructed based on information
|
||||
* that the user entered, or it can be {@link SEPAAccount} instance retrieved from {@link getAccounts()}.
|
||||
* @param \DateTime|null $from If set, only transactions after this date (inclusive) are returned.
|
||||
* @param \DateTime|null $to If set, only transactions before this date (inclusive) are returned.
|
||||
* @param bool $allAccounts If set to true, will return statements for all accounts of the user. You still need to
|
||||
* pass one of the accounts into $account, though.
|
||||
* @return GetStatementOfAccount A new action instance.
|
||||
*/
|
||||
public static function create(SEPAAccount $account, ?\DateTime $from = null, ?\DateTime $to = null, bool $allAccounts = false, bool $includeUnbooked = false): GetStatementOfAccount
|
||||
{
|
||||
if ($from !== null && $to !== null && $from > $to) {
|
||||
throw new \InvalidArgumentException('From-date must be before to-date');
|
||||
}
|
||||
|
||||
$result = new GetStatementOfAccount();
|
||||
$result->account = $account;
|
||||
$result->from = $from;
|
||||
$result->to = $to;
|
||||
$result->allAccounts = $allAccounts;
|
||||
$result->includeUnbooked = $includeUnbooked;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*/
|
||||
public function serialize(): string
|
||||
{
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
parent::__serialize(),
|
||||
$this->account, $this->from, $this->to, $this->allAccounts,
|
||||
$this->bankName,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*
|
||||
* @param string $serialized
|
||||
* @return void
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
self::__unserialize(unserialize($serialized));
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void
|
||||
{
|
||||
list(
|
||||
$parentSerialized,
|
||||
$this->account, $this->from, $this->to, $this->allAccounts,
|
||||
$this->bankName
|
||||
) = $serialized;
|
||||
|
||||
is_array($parentSerialized) ?
|
||||
parent::__unserialize($parentSerialized) :
|
||||
parent::unserialize($parentSerialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The raw MT940 data received from the server.
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getRawMT940(): string
|
||||
{
|
||||
$this->ensureDone();
|
||||
return $this->rawMT940;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array The parsed MT940 data.
|
||||
*/
|
||||
public function getParsedMT940(): array
|
||||
{
|
||||
$this->ensureDone();
|
||||
return $this->parsedMT940;
|
||||
}
|
||||
|
||||
public function getStatement(): StatementOfAccount
|
||||
{
|
||||
$this->ensureDone();
|
||||
return $this->statement;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
$this->bankName = $bpd->getBankName();
|
||||
|
||||
/** @var HIKAZS $hikazs */
|
||||
$hikazs = $bpd->requireLatestSupportedParameters('HIKAZS');
|
||||
if ($this->allAccounts && !$hikazs->getParameter()->getAlleKontenErlaubt()) {
|
||||
throw new \InvalidArgumentException('The bank do not permit the use of allAccounts=true');
|
||||
}
|
||||
switch ($hikazs->getVersion()) {
|
||||
case 4:
|
||||
return HKKAZv4::create(Kto::fromAccount($this->account), $this->from, $this->to);
|
||||
case 5:
|
||||
return HKKAZv5::create(KtvV3::fromAccount($this->account), $this->allAccounts, $this->from, $this->to);
|
||||
case 6:
|
||||
return HKKAZv6::create(KtvV3::fromAccount($this->account), $this->allAccounts, $this->from, $this->to);
|
||||
case 7:
|
||||
return HKKAZv7::create(Kti::fromAccount($this->account), $this->allAccounts, $this->from, $this->to);
|
||||
default:
|
||||
throw new UnsupportedException('Unsupported HKKAZ version: ' . $hikazs->getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function processResponse(Message $response)
|
||||
{
|
||||
parent::processResponse($response);
|
||||
|
||||
// Banks send just 3010 and no HIKAZ in case there are no transactions.
|
||||
$isUnavailable = $response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null;
|
||||
$responseHikaz = $response->findSegments(HIKAZ::class);
|
||||
$numResponseSegments = count($responseHikaz);
|
||||
if (!$isUnavailable && $numResponseSegments < count($this->getRequestSegmentNumbers())) {
|
||||
throw new UnexpectedResponseException("Only got $numResponseSegments HIKAZ response segments!");
|
||||
}
|
||||
|
||||
/** @var HIKAZ $hikaz */
|
||||
foreach ($responseHikaz as $hikaz) {
|
||||
$this->rawMT940 .= $hikaz->getGebuchteUmsaetze()->getData();
|
||||
if ($this->includeUnbooked and $hikaz->getNichtGebuchteUmsaetze() !== null) {
|
||||
$this->rawMT940 .= $hikaz->getNichtGebuchteUmsaetze()->getData();
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Pagination boundaries may cut in the middle of the MT940 data, so it is not possible to parse a partial
|
||||
// reponse before having received all pages.
|
||||
if (!$this->hasMorePages()) {
|
||||
$this->parseMt940();
|
||||
}
|
||||
}
|
||||
|
||||
private function parseMt940()
|
||||
{
|
||||
if (str_contains(strtolower($this->bankName), 'sparda')) {
|
||||
$parser = new SpardaMT940();
|
||||
} elseif (str_contains(strtolower($this->bankName), 'postbank')) {
|
||||
$parser = new PostbankMT940();
|
||||
} else {
|
||||
$parser = new MT940();
|
||||
}
|
||||
|
||||
try {
|
||||
// Note: Some banks encode their MT 940 data as SWIFT/ISO-8859 like it should be according to the
|
||||
// specification (e.g. DKB), others just send UTF-8 (e.g. Consorsbank), so we try to detect it here.
|
||||
$rawMT940 = mb_detect_encoding($this->rawMT940, 'UTF-8', true) === false
|
||||
? mb_convert_encoding($this->rawMT940, 'UTF-8', 'ISO-8859-1') : $this->rawMT940;
|
||||
$this->parsedMT940 = $parser->parse($rawMT940);
|
||||
$this->statement = StatementOfAccount::fromMT940Array($this->parsedMT940);
|
||||
} catch (MT940Exception $e) {
|
||||
throw new \InvalidArgumentException('Invalid MT940 data', 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
176
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccountXML.php
vendored
Executable file
176
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccountXML.php
vendored
Executable file
|
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\PaginateableAction;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\Message;
|
||||
use Fhp\Protocol\UnexpectedResponseException;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\CAZ\HICAZSv1;
|
||||
use Fhp\Segment\CAZ\HICAZv1;
|
||||
use Fhp\Segment\CAZ\HKCAZv1;
|
||||
use Fhp\Segment\CAZ\UnterstuetzteCamtMessages;
|
||||
use Fhp\Segment\Common\Kti;
|
||||
use Fhp\Segment\HIRMS\Rueckmeldungscode;
|
||||
use Fhp\UnsupportedException;
|
||||
|
||||
/**
|
||||
* Retrieves statements for one specific account or for all accounts that the user has access to. A statement is a
|
||||
* series of financial transactions that pertain to the account, grouped by day.
|
||||
*/
|
||||
class GetStatementOfAccountXML extends PaginateableAction
|
||||
{
|
||||
// Request (not available after serialization, i.e. not available in processResponse()).
|
||||
/** @var SEPAAccount */
|
||||
private $account;
|
||||
/** @var \DateTime */
|
||||
private $from;
|
||||
/** @var \DateTime */
|
||||
private $to;
|
||||
/** @var string */
|
||||
private $camtURN;
|
||||
/** @var bool */
|
||||
private $allAccounts;
|
||||
|
||||
// Response
|
||||
/** @var string[] */
|
||||
protected $xml = [];
|
||||
|
||||
/**
|
||||
* @param SEPAAccount $account The account to get the statement for. This can be constructed based on information
|
||||
* that the user entered, or it can be {@link SEPAAccount} instance retrieved from {@link getAccounts()}.
|
||||
* @param \DateTime|null $from If set, only transactions after this date (inclusive) are returned.
|
||||
* @param \DateTime|null $to If set, only transactions before this date (inclusive) are returned.
|
||||
* @param string|null $camtURN The URN/descriptor of the CAMT XML format you want the bank to return.
|
||||
* Use null to just let the bank decide. Otherwise needs to be one of the reported URNs the bank supports.
|
||||
* For example urn:iso:std:iso:20022:tech:xsd:camt.052.001.02
|
||||
* @param bool $allAccounts If set to true, will return statements for all accounts of the user. You still need to
|
||||
* pass one of the accounts into $account, though.
|
||||
* @return GetStatementOfAccountXML A new action instance.
|
||||
*/
|
||||
public static function create(SEPAAccount $account, ?\DateTime $from = null, ?\DateTime $to = null, ?string $camtURN = null, bool $allAccounts = false): GetStatementOfAccountXML
|
||||
{
|
||||
if ($from !== null && $to !== null && $from > $to) {
|
||||
throw new \InvalidArgumentException('From-date must be before to-date');
|
||||
}
|
||||
|
||||
$result = new GetStatementOfAccountXML();
|
||||
$result->account = $account;
|
||||
$result->camtURN = $camtURN;
|
||||
$result->from = $from;
|
||||
$result->to = $to;
|
||||
$result->allAccounts = $allAccounts;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*/
|
||||
public function serialize(): string
|
||||
{
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
parent::__serialize(),
|
||||
$this->account, $this->camtURN, $this->from, $this->to, $this->allAccounts,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*
|
||||
* @param string $serialized
|
||||
* @return void
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
self::__unserialize(unserialize($serialized));
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void
|
||||
{
|
||||
list(
|
||||
$parentSerialized,
|
||||
$this->account, $this->camtURN, $this->from, $this->to, $this->allAccounts
|
||||
) = $serialized;
|
||||
|
||||
is_array($parentSerialized) ?
|
||||
parent::__unserialize($parentSerialized) :
|
||||
parent::unserialize($parentSerialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] The XML-Document(s) received from the bank, or empty array if the statement is unavailable/empty.
|
||||
*/
|
||||
public function getBookedXML(): array
|
||||
{
|
||||
$this->ensureDone();
|
||||
return $this->xml;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
if ($upd === null) {
|
||||
throw new UnsupportedException('The UPD is needed to be able to create a request for GetStatementOfAccountXML.');
|
||||
}
|
||||
|
||||
if (!$upd->isRequestSupportedForAccount($this->account, 'HKCAZ')) {
|
||||
throw new UnsupportedException('The bank (or the given account/user combination) does not support GetStatementOfAccountXML.');
|
||||
}
|
||||
|
||||
/** @var HICAZSv1 $hicazs */
|
||||
$hicazs = $bpd->requireLatestSupportedParameters('HICAZS');
|
||||
$supportedCamtURNs = $hicazs->getParameter()->getUnterstuetzteCamtMessages()->camtDescriptor;
|
||||
if (is_null($this->camtURN)) {
|
||||
$camtURNs = $supportedCamtURNs;
|
||||
} elseif (!in_array($this->camtURN, $supportedCamtURNs)) {
|
||||
throw new \InvalidArgumentException('The bank does not support the CAMT format' . $this->camtURN . '. The following formats are supported: ' . implode(', ', $supportedCamtURNs));
|
||||
} else {
|
||||
$camtURNs = [$this->camtURN];
|
||||
}
|
||||
|
||||
if ($this->allAccounts && !$hicazs->getParameter()->getAlleKontenErlaubt()) {
|
||||
throw new \InvalidArgumentException('The bank do not permit the use of allAccounts=true');
|
||||
}
|
||||
switch ($hicazs->getVersion()) {
|
||||
case 1:
|
||||
$unterstuetzteCamtMessages = UnterstuetzteCamtMessages::create($camtURNs);
|
||||
return HKCAZv1::create(Kti::fromAccount($this->account), $unterstuetzteCamtMessages, $this->allAccounts, $this->from, $this->to);
|
||||
default:
|
||||
throw new UnsupportedException('Unsupported HKCAZ version: ' . $hicazs->getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function processResponse(Message $response)
|
||||
{
|
||||
parent::processResponse($response);
|
||||
|
||||
// Banks send just 3010 and no HICAZ in case there are no transactions.
|
||||
if ($response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var HICAZv1[] $responseHicaz */
|
||||
$responseHicaz = $response->findSegments(HICAZv1::class);
|
||||
$numResponseSegments = count($responseHicaz);
|
||||
if ($numResponseSegments < count($this->getRequestSegmentNumbers())) {
|
||||
throw new UnexpectedResponseException("Only got $numResponseSegments HICAZ response segments!");
|
||||
}
|
||||
if ($numResponseSegments > 1) {
|
||||
throw new UnsupportedException('More than 1 HICAZ response segment is not supported at the moment!');
|
||||
}
|
||||
// It seems that paginated responses, always contain a whole XML Document
|
||||
foreach ($responseHicaz[0]->getGebuchteUmsaetze() as $xml_string) {
|
||||
$this->xml[] = $xml_string;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
vendor/nemiah/php-fints/lib/Fhp/Action/SendInternationalCreditTransfer.php
vendored
Executable file
50
vendor/nemiah/php-fints/lib/Fhp/Action/SendInternationalCreditTransfer.php
vendored
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\BaseAction;
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\AUB\HIAUBSv9;
|
||||
use Fhp\Segment\AUB\HKAUBv9;
|
||||
use Fhp\Segment\Common\Kti;
|
||||
use Fhp\Syntax\Bin;
|
||||
|
||||
class SendInternationalCreditTransfer extends BaseAction
|
||||
{
|
||||
/** @var SEPAAccount */
|
||||
protected $account;
|
||||
|
||||
/** @var string */
|
||||
protected $dtavzData;
|
||||
|
||||
/** @var string|null */
|
||||
protected $dtavzVersion;
|
||||
|
||||
/**
|
||||
* @param SEPAAccount $account The account of the creditor (the sender of the money)
|
||||
* @param string $dtavzData The details of the transfer(s) in DTAZV Format (Datenträgeraustauschverfahren Auslandszahlungsverkehr)
|
||||
* @param string|null $dtavzVersion If null the value the bank expects is used.
|
||||
*/
|
||||
public static function create(SEPAAccount $account, string $dtavzData, ?string $dtavzVersion = null): SendInternationalCreditTransfer
|
||||
{
|
||||
$result = new SendInternationalCreditTransfer();
|
||||
$result->account = $account;
|
||||
$result->dtavzVersion = $dtavzVersion;
|
||||
$result->dtavzData = $dtavzData;
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
/** @var HIAUBSv9 $hiaubs */
|
||||
$hiaubs = $bpd->requireLatestSupportedParameters('HIAUBS');
|
||||
|
||||
$hkaub = HKAUBv9::createEmpty();
|
||||
$hkaub->kontoverbindungInternational = Kti::fromAccount($this->account);
|
||||
$hkaub->DTAZVHandbuch = $this->dtavzVersion ?? $hiaubs->parameter->DTAZVHandbuch;
|
||||
$hkaub->DTAZVDatensatz = new Bin($this->dtavzData);
|
||||
return $hkaub;
|
||||
}
|
||||
}
|
||||
185
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPADirectDebit.php
vendored
Executable file
185
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPADirectDebit.php
vendored
Executable file
|
|
@ -0,0 +1,185 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\BaseAction;
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\BaseSegment;
|
||||
use Fhp\Segment\Common\Btg;
|
||||
use Fhp\Segment\Common\Kti;
|
||||
use Fhp\Segment\DME\HIDMESv1;
|
||||
use Fhp\Segment\DME\HIDMESv2;
|
||||
use Fhp\Segment\DSE\HIDSESv2;
|
||||
use Fhp\Segment\DSE\HIDXES;
|
||||
use Fhp\Segment\SPA\HISPAS;
|
||||
use Fhp\Syntax\Bin;
|
||||
use Fhp\UnsupportedException;
|
||||
|
||||
/**
|
||||
* Initiate one or multiple SEPA Direct Debits ("Lastschriften")
|
||||
*/
|
||||
class SendSEPADirectDebit extends BaseAction
|
||||
{
|
||||
/** @var SEPAAccount */
|
||||
protected $account;
|
||||
|
||||
/** @var string */
|
||||
protected $painMessage;
|
||||
|
||||
/** @var string */
|
||||
protected $painNamespace;
|
||||
|
||||
/** @var float */
|
||||
protected $ctrlSum;
|
||||
|
||||
/** @var bool */
|
||||
protected $singleDirectDebit = false;
|
||||
|
||||
/** @var bool */
|
||||
protected $tryToUseControlSumForSingleTransactions = false;
|
||||
|
||||
/** @var string */
|
||||
private $coreType;
|
||||
|
||||
public static function create(SEPAAccount $account, string $painMessage, bool $tryToUseControlSumForSingleTransactions = false): SendSEPADirectDebit
|
||||
{
|
||||
if (preg_match('/xmlns="(?<namespace>[^"]+)"/s', $painMessage, $matches) === 1) {
|
||||
$painNamespace = $matches['namespace'];
|
||||
} else {
|
||||
throw new \InvalidArgumentException('The namespace aka "xmlns" is missing in PAIN message');
|
||||
}
|
||||
|
||||
// Check whether the PAIN message contains multiple or only one Direct Debit, should match <NbOfTxs>xx</NbOfTxs> in the XML
|
||||
$nbOfTxs = substr_count($painMessage, '<DrctDbtTxInf>');
|
||||
$ctrlSum = null;
|
||||
|
||||
if (preg_match('@<GrpHdr>.*?<CtrlSum>(?<ctrlsum>[0-9.]+)</CtrlSum>.*?</GrpHdr>@s', $painMessage, $matches) === 1) {
|
||||
$ctrlSum = $matches['ctrlsum'];
|
||||
}
|
||||
|
||||
if (preg_match('@<PmtTpInf>.*?<LclInstrm>.*?<Cd>(?<coretype>CORE|COR1|B2B)</Cd>.*?</LclInstrm>.*?</PmtTpInf>@s', $painMessage, $matches) === 1) {
|
||||
$coreType = $matches['coretype'];
|
||||
} else {
|
||||
throw new \InvalidArgumentException('The type CORE/COR1/B2B is missing in PAIN message');
|
||||
}
|
||||
|
||||
if ($nbOfTxs > 1 && is_null($ctrlSum)) {
|
||||
throw new \InvalidArgumentException('The control sum aka "<GrpHdr><CtrlSum>xx</CtrlSum></GrpHdr>" is missing in PAIN message');
|
||||
}
|
||||
|
||||
$result = new SendSEPADirectDebit();
|
||||
$result->account = $account;
|
||||
$result->painMessage = $painMessage;
|
||||
$result->painNamespace = $painNamespace;
|
||||
$result->ctrlSum = $ctrlSum;
|
||||
$result->coreType = $coreType;
|
||||
|
||||
$result->singleDirectDebit = $nbOfTxs === 1;
|
||||
|
||||
$result->tryToUseControlSumForSingleTransactions = $tryToUseControlSumForSingleTransactions;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*/
|
||||
public function serialize(): string
|
||||
{
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
parent::__serialize(),
|
||||
$this->singleDirectDebit, $this->tryToUseControlSumForSingleTransactions, $this->ctrlSum, $this->coreType, $this->painMessage, $this->painNamespace, $this->account,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*
|
||||
* @param string $serialized
|
||||
* @return void
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
self::__unserialize(unserialize($serialized));
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void
|
||||
{
|
||||
list(
|
||||
$parentSerialized,
|
||||
$this->singleDirectDebit, $this->tryToUseControlSumForSingleTransactions, $this->ctrlSum, $this->coreType, $this->painMessage, $this->painNamespace, $this->account
|
||||
) = $serialized;
|
||||
|
||||
is_array($parentSerialized) ?
|
||||
parent::__unserialize($parentSerialized) :
|
||||
parent::unserialize($parentSerialized);
|
||||
}
|
||||
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
$useSingleDirectDebit = $this->singleDirectDebit;
|
||||
|
||||
// If the PAIN message contains a control sum, we should use it, if the bank also supports it
|
||||
if ($useSingleDirectDebit && $this->tryToUseControlSumForSingleTransactions && !is_null($this->ctrlSum) && !is_null($bpd->getLatestSupportedParameters('HIDMES'))) {
|
||||
$useSingleDirectDebit = false;
|
||||
}
|
||||
|
||||
/* @var HIDXES|BaseSegment $hidxes */
|
||||
$hidxes = $bpd->requireLatestSupportedParameters(GetSEPADirectDebitParameters::getHixxesSegmentName($this->coreType, $useSingleDirectDebit));
|
||||
|
||||
$supportedPainNamespaces = null;
|
||||
|
||||
if ($hidxes->getVersion() === 2) {
|
||||
/** @var HIDMESv2|HIDSESv2 $hidxes */
|
||||
$supportedPainNamespaces = $hidxes->getParameter()->getUnterstuetzteSEPADatenformate();
|
||||
}
|
||||
|
||||
// If there are no SEPA formats available in the HIDXES Parameters, we look to the general formats
|
||||
if (!is_array($supportedPainNamespaces) || count($supportedPainNamespaces) === 0) {
|
||||
/** @var HISPAS $hispas */
|
||||
$hispas = $bpd->requireLatestSupportedParameters('HISPAS');
|
||||
$supportedPainNamespaces = $hispas->getParameter()->getUnterstuetzteSEPADatenformate();
|
||||
}
|
||||
|
||||
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
|
||||
// GIBC_X stands for German Banking Industry Committee and a version counter.
|
||||
$xmlSchema = $this->painNamespace;
|
||||
$matchingSchemas = array_filter($supportedPainNamespaces, function($value) use ($xmlSchema) {
|
||||
// For example urn:iso:std:iso:20022:tech:xsd:pain.008.001.08 from the xml matches
|
||||
// urn:iso:std:iso:20022:tech:xsd:pain.008.001.08_GBIC_4
|
||||
return str_starts_with($value, $xmlSchema);
|
||||
});
|
||||
|
||||
if (count($matchingSchemas) === 0) {
|
||||
throw new UnsupportedException("The bank does not support the XML schema $this->painNamespace, but only "
|
||||
. implode(', ', $supportedPainNamespaces));
|
||||
}
|
||||
|
||||
/** @var mixed $hkdxe */ // TODO Put a new interface type here.
|
||||
$hkdxe = $hidxes->createRequestSegment();
|
||||
$hkdxe->kontoverbindungInternational = Kti::fromAccount($this->account);
|
||||
$hkdxe->sepaDescriptor = $this->painNamespace;
|
||||
$hkdxe->sepaPainMessage = new Bin($this->painMessage);
|
||||
|
||||
if (!$useSingleDirectDebit) {
|
||||
if ($hidxes->getParameter()->einzelbuchungErlaubt) {
|
||||
$hkdxe->einzelbuchungGewuenscht = false;
|
||||
}
|
||||
|
||||
/* @var HIDMESv1 $hidxes */
|
||||
// Just always send the control sum
|
||||
// if ($hidxes->getParameter()->summenfeldBenoetigt) {
|
||||
$hkdxe->summenfeld = Btg::create($this->ctrlSum);
|
||||
// }
|
||||
}
|
||||
|
||||
return $hkdxe;
|
||||
}
|
||||
}
|
||||
114
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPARealtimeTransfer.php
vendored
Executable file
114
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPARealtimeTransfer.php
vendored
Executable file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\BaseAction;
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\Message;
|
||||
use Fhp\Protocol\UnexpectedResponseException;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\Common\Kti;
|
||||
use Fhp\Segment\HIRMS\Rueckmeldung;
|
||||
use Fhp\Segment\HIRMS\Rueckmeldungscode;
|
||||
use Fhp\Segment\IPZ\HIIPZSv1;
|
||||
use Fhp\Segment\IPZ\HIIPZSv2;
|
||||
use Fhp\Segment\IPZ\HKIPZv2;
|
||||
use Fhp\Segment\SPA\HISPAS;
|
||||
use Fhp\Syntax\Bin;
|
||||
use Fhp\UnsupportedException;
|
||||
|
||||
/**
|
||||
* Initiates an outgoing realtime transfer in SEPA format (PAIN XML).
|
||||
*/
|
||||
class SendSEPARealtimeTransfer extends BaseAction
|
||||
{
|
||||
/** @var SEPAAccount */
|
||||
private $account;
|
||||
/** @var string */
|
||||
private $painMessage;
|
||||
/** @var string */
|
||||
private $xmlSchema;
|
||||
|
||||
private bool $allowConversionToSEPATransfer = true;
|
||||
|
||||
/**
|
||||
* @param SEPAAccount $account The account from which the transfer will be sent.
|
||||
* @param string $painMessage An XML-formatted ISO 20022 message. You may want to use github.com/nemiah/phpSepaXml
|
||||
* to create this.
|
||||
* @param bool $allowConversionToSEPATransfer If instant payment ist not possible, allow the bank to send as a regular transfer instead
|
||||
* @return SendSEPARealtimeTransfer A new action for executing this the given PAIN message.
|
||||
*/
|
||||
public static function create(SEPAAccount $account, string $painMessage, bool $allowConversionToSEPATransfer = true): SendSEPARealtimeTransfer
|
||||
{
|
||||
if (preg_match('/xmlns="(.*?)"/', $painMessage, $match) === false) {
|
||||
throw new \InvalidArgumentException('xmlns not found in the PAIN message');
|
||||
}
|
||||
$result = new SendSEPARealtimeTransfer();
|
||||
$result->account = $account;
|
||||
$result->painMessage = $painMessage;
|
||||
$result->xmlSchema = $match[1];
|
||||
$result->allowConversionToSEPATransfer = $allowConversionToSEPATransfer;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
/** @var HIIPZSv1|HIIPZSv2 $hiipzs */
|
||||
$hiipzs = $bpd->requireLatestSupportedParameters('HIIPZS');
|
||||
|
||||
$supportedSchemas = $hiipzs->parameter->getUnterstuetzteSEPADatenformate();
|
||||
|
||||
// If there are no SEPA formats available in the HIIPZS Parameters, we look to the general formats
|
||||
if (is_null($supportedSchemas)) {
|
||||
/** @var HISPAS $hispas */
|
||||
$hispas = $bpd->requireLatestSupportedParameters('HISPAS');
|
||||
$supportedSchemas = $hispas->getParameter()->getUnterstuetzteSEPADatenformate();
|
||||
}
|
||||
|
||||
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
|
||||
// GIBC_X stands for German Banking Industry Committee and a version counter.
|
||||
$xmlSchema = $this->xmlSchema;
|
||||
$matchingSchemas = array_filter($supportedSchemas, function($value) use ($xmlSchema) {
|
||||
// For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches
|
||||
// urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4
|
||||
return str_starts_with($value, $xmlSchema);
|
||||
});
|
||||
|
||||
if (count($matchingSchemas) === 0) {
|
||||
throw new UnsupportedException("The bank does not support the XML schema $this->xmlSchema, but only "
|
||||
. implode(', ', $supportedSchemas));
|
||||
}
|
||||
|
||||
/** @var HKIPZv1|HKIPZv2 $hkipz */
|
||||
$hkipz = $hiipzs->createRequestSegment();
|
||||
$hkipz->kontoverbindungInternational = Kti::fromAccount($this->account);
|
||||
$hkipz->sepaDescriptor = $this->xmlSchema;
|
||||
$hkipz->sepaPainMessage = new Bin($this->painMessage);
|
||||
if ($hiipzs instanceof HIIPZSv2) {
|
||||
$hkipz->umwandlungNachSEPAUeberweisungZulaessig = $hiipzs->parameter->umwandlungNachSEPAUeberweisungZulaessigErlaubt && $this->allowConversionToSEPATransfer;
|
||||
}
|
||||
return $hkipz;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function processResponse(Message $response)
|
||||
{
|
||||
parent::processResponse($response);
|
||||
|
||||
// Was the instant payment converted to a regular transfer?
|
||||
$info = $response->findRueckmeldungen(3270);
|
||||
if (count($info) > 0) {
|
||||
$this->successMessage = implode("\n", array_map(function (Rueckmeldung $rueckmeldung) {
|
||||
return $rueckmeldung->rueckmeldungstext;
|
||||
}, $info));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($response->findRueckmeldung(Rueckmeldungscode::ENTGEGENGENOMMEN) === null &&
|
||||
$response->findRueckmeldung(Rueckmeldungscode::AUSGEFUEHRT) === null) {
|
||||
throw new UnexpectedResponseException('Bank did not confirm SEPATransfer execution');
|
||||
}
|
||||
}
|
||||
}
|
||||
121
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPATransfer.php
vendored
Executable file
121
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPATransfer.php
vendored
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Action;
|
||||
|
||||
use Fhp\BaseAction;
|
||||
use Fhp\Model\SEPAAccount;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\Message;
|
||||
use Fhp\Protocol\UnexpectedResponseException;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\Common\Kti;
|
||||
use Fhp\Segment\HIRMS\Rueckmeldungscode;
|
||||
use Fhp\Segment\SPA\HISPAS;
|
||||
use Fhp\Syntax\Bin;
|
||||
use Fhp\UnsupportedException;
|
||||
|
||||
/**
|
||||
* Initiates an outgoing wire transfer in SEPA format (PAIN XML).
|
||||
*/
|
||||
class SendSEPATransfer extends BaseAction
|
||||
{
|
||||
/** @var SEPAAccount */
|
||||
private $account;
|
||||
/** @var string */
|
||||
private $painMessage;
|
||||
/** @var string */
|
||||
private $xmlSchema;
|
||||
|
||||
/**
|
||||
* @param SEPAAccount $account The account from which the transfer will be sent.
|
||||
* @param string $painMessage An XML-formatted ISO 20022 message. You may want to use github.com/nemiah/phpSepaXml
|
||||
* to create this.
|
||||
* @return SendSEPATransfer A new action for executing this the given PAIN message.
|
||||
*/
|
||||
public static function create(SEPAAccount $account, string $painMessage): SendSEPATransfer
|
||||
{
|
||||
if (preg_match('/xmlns="(.*?)"/', $painMessage, $match) === false) {
|
||||
throw new \InvalidArgumentException('xmlns not found in the PAIN message');
|
||||
}
|
||||
$result = new SendSEPATransfer();
|
||||
$result->account = $account;
|
||||
$result->painMessage = $painMessage;
|
||||
$result->xmlSchema = $match[1];
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
protected function createRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
//ANALYSE XML FOR RECEIPTS AND PAYMENT DATE
|
||||
$xmlAsObject = simplexml_load_string($this->painMessage, "SimpleXMLElement", LIBXML_NOCDATA);
|
||||
$numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs;
|
||||
$hasReqdExDates = false;
|
||||
foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) {
|
||||
// Checks for both, <ReqdExctnDt>1999-01-01</ReqdExctnDt> and <ReqdExctnDt><Dt>1999-01-01</Dt></ReqdExctnDt>
|
||||
if (isset($pmtInfo->ReqdExctnDt) && ($pmtInfo->ReqdExctnDt->Dt ?? $pmtInfo->ReqdExctnDt) != '1999-01-01') {
|
||||
$hasReqdExDates = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//NOW READ OUT, WICH SEGMENT SHOULD BE USED:
|
||||
if ($numberOfTransactions > 1 && $hasReqdExDates) {
|
||||
|
||||
// Terminierte SEPA-Sammelüberweisung (Segment HKCME / Kennung HICMES)
|
||||
$segmentID = 'HICMES';
|
||||
$segment = \Fhp\Segment\CME\HKCMEv1::createEmpty();
|
||||
} elseif ($numberOfTransactions == 1 && $hasReqdExDates) {
|
||||
|
||||
// Terminierte SEPA-Überweisung (Segment HKCSE / Kennung HICSES)
|
||||
$segmentID = 'HICSES';
|
||||
$segment = \Fhp\Segment\CSE\HKCSEv1::createEmpty();
|
||||
} elseif ($numberOfTransactions > 1 && !$hasReqdExDates) {
|
||||
|
||||
// SEPA-Sammelüberweisungen (Segment HKCCM / Kennung HICSES)
|
||||
$segmentID = 'HICSES';
|
||||
$segment = \Fhp\Segment\CCM\HKCCMv1::createEmpty();
|
||||
} else {
|
||||
|
||||
//SEPA Einzelüberweisung (Segment HKCCS / Kennung HICCSS).
|
||||
$segmentID = 'HICCSS';
|
||||
$segment = \Fhp\Segment\CCS\HKCCSv1::createEmpty();
|
||||
}
|
||||
|
||||
if (!$bpd->supportsParameters($segmentID, 1)) {
|
||||
throw new UnsupportedException('The bank does not support ' . $segmentID . 'v1');
|
||||
}
|
||||
|
||||
/** @var HISPAS $hispas */
|
||||
$hispas = $bpd->requireLatestSupportedParameters('HISPAS');
|
||||
$supportedSchemas = $hispas->getParameter()->getUnterstuetzteSEPADatenformate();
|
||||
|
||||
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
|
||||
// GIBC_X stands for German Banking Industry Committee and a version counter.
|
||||
$xmlSchema = $this->xmlSchema;
|
||||
$matchingSchemas = array_filter($supportedSchemas, function($value) use ($xmlSchema) {
|
||||
// For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches
|
||||
// urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4
|
||||
return str_starts_with($value, $xmlSchema);
|
||||
});
|
||||
|
||||
if (count($matchingSchemas) === 0) {
|
||||
throw new UnsupportedException("The bank does not support the XML schema $this->xmlSchema, but only "
|
||||
. implode(', ', $supportedSchemas));
|
||||
}
|
||||
|
||||
$segment->kontoverbindungInternational = Kti::fromAccount($this->account);
|
||||
$segment->sepaDescriptor = $this->xmlSchema;
|
||||
$segment->sepaPainMessage = new Bin($this->painMessage);
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function processResponse(Message $response)
|
||||
{
|
||||
parent::processResponse($response);
|
||||
if ($response->findRueckmeldung(Rueckmeldungscode::ENTGEGENGENOMMEN) === null && $response->findRueckmeldung(Rueckmeldungscode::AUSGEFUEHRT) === null) {
|
||||
throw new UnexpectedResponseException('Bank did not confirm SEPATransfer execution');
|
||||
}
|
||||
}
|
||||
}
|
||||
258
vendor/nemiah/php-fints/lib/Fhp/BaseAction.php
vendored
Executable file
258
vendor/nemiah/php-fints/lib/Fhp/BaseAction.php
vendored
Executable file
|
|
@ -0,0 +1,258 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
|
||||
namespace Fhp;
|
||||
|
||||
use Fhp\Model\TanRequest;
|
||||
use Fhp\Protocol\ActionIncompleteException;
|
||||
use Fhp\Protocol\BPD;
|
||||
use Fhp\Protocol\Message;
|
||||
use Fhp\Protocol\TanRequiredException;
|
||||
use Fhp\Protocol\UnexpectedResponseException;
|
||||
use Fhp\Protocol\UPD;
|
||||
use Fhp\Segment\BaseSegment;
|
||||
use Fhp\Segment\HIRMS\Rueckmeldung;
|
||||
use Fhp\Segment\HIRMS\Rueckmeldungscode;
|
||||
|
||||
/**
|
||||
* Base class for actions that can be performed against a bank server. On a high level, there are two kinds of actions:
|
||||
* - requests for information (e.g. an account statement), which the bank server will return
|
||||
* - transactions (e.g. a wire transfer to another account), which the bank server will execute.
|
||||
*
|
||||
* In part, this class is designed like futures/promises in concurrent programming. The outcome of the action (i.e. the
|
||||
* requested information or the execution confirmation of the transaction) becomes available in the future, possibly
|
||||
* much later than when the request was sent, in case the user needs to enter a TAN.
|
||||
* All action instances are serializable, so that the execution can be interrupted to ask the user for a TAN. Once the
|
||||
* TAN is available, the execution can resume either a couple seconds later in the same PHP process using the same
|
||||
* physical connection to the bank, or on the order of minutes later in a new PHP process with a newly established
|
||||
* connection to the bank. Note that the serialization only applies to selected relevant request parameters, and not to
|
||||
* the response. Thus it is only possible to serialize an action when its execution has been attempted but resulted in a
|
||||
* TAN request.
|
||||
* Actions that do not require a TAN will complete immediately.
|
||||
*
|
||||
* The implementation of an action consists of two parts: assembling the request to the bank, and processing the
|
||||
* response.
|
||||
*/
|
||||
abstract class BaseAction implements \Serializable
|
||||
{
|
||||
/** @var int[] Stores segment numbers that were assigned to the segments returned from {@link createRequest()}. */
|
||||
protected $requestSegmentNumbers;
|
||||
|
||||
/**
|
||||
* @var string|null Contains the name of the segment, that might need a tan, used by FinTs::execute to signal
|
||||
* to the bank that supplying a tan is supported.
|
||||
*/
|
||||
protected $needTanForSegment = null;
|
||||
|
||||
/**
|
||||
* If set, the last response from the server regarding this action asked for a TAN from the user.
|
||||
* @var TanRequest|null
|
||||
*/
|
||||
protected $tanRequest;
|
||||
|
||||
/** @var bool */
|
||||
protected $isDone = false;
|
||||
|
||||
/**
|
||||
* Will be populated with the message the bank sent along with the success indication, can be used to show to
|
||||
* the user.
|
||||
* @var string
|
||||
*/
|
||||
public $successMessage;
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*
|
||||
* NOTE: A common mistake is to call this function directly. Instead, you probably want `serialize($instance)`.
|
||||
*
|
||||
* An action can only be serialized *after* it has been executed in case it needs a TAN, i.e. when the result is not
|
||||
* present yet.
|
||||
* If a sub-class overrides this, it should call the parent function and include it in its result.
|
||||
* @return string The serialized action, e.g. for storage in a database. This will not contain sensitive user data.
|
||||
*/
|
||||
public function serialize(): string
|
||||
{
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
/**
|
||||
* An action can only be serialized *after* it has been executed in case it needs a TAN, i.e. when the result is not
|
||||
* present yet.
|
||||
* If a sub-class overrides this, it should call the parent function and include it in its result.
|
||||
*
|
||||
* @return array The serialized action, e.g. for storage in a database. This will not contain sensitive user data.
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
if (!$this->needsTan()) {
|
||||
throw new \RuntimeException('Cannot serialize this action, because it is not waiting for a TAN.');
|
||||
}
|
||||
return [
|
||||
$this->requestSegmentNumbers,
|
||||
$this->tanRequest,
|
||||
$this->needTanForSegment,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
||||
*
|
||||
* @param string $serialized
|
||||
* @return void
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
self::__unserialize(unserialize($serialized));
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void
|
||||
{
|
||||
list(
|
||||
$this->requestSegmentNumbers,
|
||||
$this->tanRequest,
|
||||
$this->needTanForSegment
|
||||
) = $serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the underlying operation has completed successfully and the result in this "future" is
|
||||
* available. Note: If this returns false, check {@link needsTan()}.
|
||||
*/
|
||||
public function isDone(): bool
|
||||
{
|
||||
return $this->isDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool If this returns true, the underlying operation has not completed because it is awaiting a TAN or a
|
||||
* "decoupled" confirmation. You should ask the user for this TAN/confirmation and pass it to
|
||||
* {@link FinTs::submitTan()} or call {@link FinTs::checkDecoupledSubmission()}, respectively.
|
||||
*/
|
||||
public function needsTan(): bool
|
||||
{
|
||||
return !$this->isDone() && $this->tanRequest !== null;
|
||||
}
|
||||
|
||||
public function getNeedTanForSegment(): ?string
|
||||
{
|
||||
return $this->needTanForSegment;
|
||||
}
|
||||
|
||||
public function getTanRequest(): ?TanRequest
|
||||
{
|
||||
return $this->tanRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception unless this action has been successfully executed, i.e. in the following cases:
|
||||
* - the action has not been {@link FinTs::execute()}-d at all or the {@link FinTs::execute()} call for it threw an
|
||||
* exception,
|
||||
* - the action is awaiting a TAN/confirmation (as per {@link BaseAction::needsTan()}.
|
||||
*
|
||||
* After executing an action, you can use this function to make sure that it succeeded. This is especially useful
|
||||
* for actions that don't have any results (as each result getter would call {@link ensureDone()} internally).
|
||||
* On the other hand, you do not need to call this function if you make sure that (1) you called
|
||||
* {@link FinTs::execute()} and (2) you checked {@link needsTan()} and, if it returned true, supplied a TAN by
|
||||
* calling {@ink FinTs::submitTan()}. Note that both exception types thrown from this method are sub-classes of
|
||||
* {@link \RuntimeException}, so you shouldn't need a try-catch block at the call site for this.
|
||||
* @throws ActionIncompleteException If the action hasn't even been executed.
|
||||
* @throws TanRequiredException If the action needs a TAN.
|
||||
*/
|
||||
public function ensureDone()
|
||||
{
|
||||
if ($this->tanRequest !== null) {
|
||||
throw new TanRequiredException($this->tanRequest);
|
||||
} elseif (!$this->isDone()) {
|
||||
throw new ActionIncompleteException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this action is about to be executed, in order to construct the request.
|
||||
* @param BPD $bpd See {@link BPD}.
|
||||
* @param UPD|null $upd See {@link UPD}. This is usually present (non-null), except for a few special login and TAN
|
||||
* management actions.
|
||||
* @return BaseSegment|BaseSegment[] A segment or a series of segments that should be sent to the bank server.
|
||||
* Note that an action can return an empty array to indicate that it does not need to make a request to the
|
||||
* server, but can instead compute the result just from the BPD/UPD, in which case it should set
|
||||
* `$this->isDone = true;` already in {@link createRequest()} and {@link processResponse()} will never
|
||||
* be executed.
|
||||
* @throws \InvalidArgumentException When the request cannot be built because the input data or BPD/UPD is invalid.
|
||||
*/
|
||||
abstract protected function createRequest(BPD $bpd, ?UPD $upd);
|
||||
|
||||
/**
|
||||
* Called by FinTs::execute when this action is about to be executed, in order to get a request. This function can
|
||||
* be called multiple times in case the response is paginated.
|
||||
* This method also tries to check if the segments might need a tan and stores this information for use in
|
||||
* FinTs::execute
|
||||
* @param BPD|null $bpd See {@link BPD}.
|
||||
* @param UPD|null $upd See {@link UPD}. This is usually present (non-null), except for a few special login and TAN
|
||||
* management actions.
|
||||
* @return BaseSegment[] A segment or a series of segments that should be sent to the bank server.
|
||||
* An empty array means that no request is necessary at all.
|
||||
* @throws \InvalidArgumentException When the request cannot be built because the input data or BPD/UPD is invalid.
|
||||
*/
|
||||
public function getNextRequest(BPD $bpd, ?UPD $upd)
|
||||
{
|
||||
$requestSegments = $this->createRequest($bpd, $upd);
|
||||
$requestSegments = is_array($requestSegments) ? $requestSegments : [$requestSegments];
|
||||
|
||||
$this->needTanForSegment = $bpd->tanRequiredForRequest($requestSegments);
|
||||
|
||||
return $requestSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this action was executed on the server (never if {@link createRequest()} returned an empty request),
|
||||
* to process the response. This function can be called multiple times in case the response is paginated.
|
||||
* In case the response indicates that this action failed, this function may throw an appropriate exception. Sub-classes should override this function
|
||||
* and call the parent/super function.
|
||||
* @param Message $response A fake message that contains the subset of segments received from the server that
|
||||
* were in response to the request segments that were created by {@link createRequest()}.
|
||||
* @throws UnexpectedResponseException When the response indicates failure.
|
||||
*/
|
||||
public function processResponse(Message $response)
|
||||
{
|
||||
$this->isDone = true;
|
||||
|
||||
$info = $response->findRueckmeldungen(Rueckmeldungscode::AUSGEFUEHRT);
|
||||
if (count($info) === 0) {
|
||||
$info = $response->findRueckmeldungen(Rueckmeldungscode::ENTGEGENGENOMMEN);
|
||||
}
|
||||
if (count($info) > 0) {
|
||||
$this->successMessage = implode("\n", array_map(function (Rueckmeldung $rueckmeldung) {
|
||||
return $rueckmeldung->rueckmeldungstext;
|
||||
}, $info));
|
||||
}
|
||||
}
|
||||
|
||||
/** @return int[] */
|
||||
public function getRequestSegmentNumbers(): array
|
||||
{
|
||||
return $this->requestSegmentNumbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called only by the FinTs instance that executes this action.
|
||||
* @param int[] $requestSegmentNumbers
|
||||
*/
|
||||
final public function setRequestSegmentNumbers(array $requestSegmentNumbers)
|
||||
{
|
||||
foreach ($requestSegmentNumbers as $segmentNumber) {
|
||||
if (!is_int($segmentNumber)) {
|
||||
throw new \InvalidArgumentException("Invalid segment number: $segmentNumber");
|
||||
}
|
||||
}
|
||||
$this->requestSegmentNumbers = $requestSegmentNumbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called only by the FinTs instance that executes this action.
|
||||
*/
|
||||
final public function setTanRequest(?TanRequest $tanRequest)
|
||||
{
|
||||
$this->tanRequest = $tanRequest;
|
||||
}
|
||||
}
|
||||
99
vendor/nemiah/php-fints/lib/Fhp/Connection.php
vendored
Executable file
99
vendor/nemiah/php-fints/lib/Fhp/Connection.php
vendored
Executable file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp;
|
||||
|
||||
/**
|
||||
* Thin wrapper around curl that does base64 encoding/decoding and converts errors to {@link CurlException}s.
|
||||
*/
|
||||
class Connection
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
protected $curlHandle;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $timeoutConnect = 15;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $timeoutResponse = 30;
|
||||
|
||||
public function __construct(string $url, int $timeoutConnect = 15, int $timeoutResponse = 30)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->timeoutConnect = $timeoutConnect;
|
||||
$this->timeoutResponse = $timeoutResponse;
|
||||
}
|
||||
|
||||
private function connect()
|
||||
{
|
||||
$this->curlHandle = curl_init();
|
||||
|
||||
curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($this->curlHandle, CURLOPT_USERAGENT, 'phpFinTS');
|
||||
curl_setopt($this->curlHandle, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($this->curlHandle, CURLOPT_URL, $this->url);
|
||||
curl_setopt($this->curlHandle, CURLOPT_CONNECTTIMEOUT, $this->timeoutConnect);
|
||||
curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
|
||||
curl_setopt($this->curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
|
||||
curl_setopt($this->curlHandle, CURLOPT_ENCODING, '');
|
||||
curl_setopt($this->curlHandle, CURLOPT_MAXREDIRS, 0);
|
||||
curl_setopt($this->curlHandle, CURLOPT_TIMEOUT, $this->timeoutResponse);
|
||||
curl_setopt($this->curlHandle, CURLOPT_HTTPHEADER, ['cache-control: no-cache', 'Content-Type: text/plain']);
|
||||
}
|
||||
|
||||
public function disconnect()
|
||||
{
|
||||
if ($this->curlHandle !== null) {
|
||||
curl_close($this->curlHandle);
|
||||
$this->curlHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message The message to be sent, in HBCI/FinTS wire format, ISO-8859-1 encoded.
|
||||
* @return string The response from the server, in HBCI/FinTS wire format, ISO-8859-1 encoded.
|
||||
* @throws CurlException When the request fails.
|
||||
*/
|
||||
public function send(string $message): string
|
||||
{
|
||||
if (!$this->curlHandle) {
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
curl_setopt($this->curlHandle, CURLOPT_POSTFIELDS, base64_encode($message));
|
||||
$response = curl_exec($this->curlHandle);
|
||||
|
||||
if (false === $response) {
|
||||
throw new CurlException(
|
||||
'Failed connection to ' . $this->url . ': ' . curl_error($this->curlHandle),
|
||||
null,
|
||||
curl_errno($this->curlHandle),
|
||||
curl_getinfo($this->curlHandle),
|
||||
curl_error($this->curlHandle)
|
||||
);
|
||||
}
|
||||
|
||||
$statusCode = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
|
||||
if ($statusCode < 200 || $statusCode > 299) {
|
||||
throw new CurlException(
|
||||
'Bad response with status code ' . $statusCode,
|
||||
$response,
|
||||
$statusCode,
|
||||
curl_getinfo($this->curlHandle)
|
||||
);
|
||||
}
|
||||
|
||||
return base64_decode($response);
|
||||
}
|
||||
}
|
||||
50
vendor/nemiah/php-fints/lib/Fhp/CurlException.php
vendored
Executable file
50
vendor/nemiah/php-fints/lib/Fhp/CurlException.php
vendored
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp;
|
||||
|
||||
class CurlException extends \Exception
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $curlInfo;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $curlMessage;
|
||||
|
||||
public function __construct(string $message, ?string $response, int $code = 0, array $curlInfo = [], ?string $curlMessage = null)
|
||||
{
|
||||
parent::__construct($message, $code, null);
|
||||
$this->response = $response;
|
||||
$this->curlInfo = $curlInfo;
|
||||
$this->curlMessage = $curlMessage;
|
||||
}
|
||||
|
||||
public function getResponse(): ?string
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the curl info from request / response.
|
||||
*/
|
||||
public function getCurlInfo(): array
|
||||
{
|
||||
return $this->curlInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the curl message from request / response.
|
||||
*/
|
||||
public function getCurlMessage(): ?string
|
||||
{
|
||||
return $this->curlMessage;
|
||||
}
|
||||
}
|
||||
1017
vendor/nemiah/php-fints/lib/Fhp/FinTs.php
vendored
Executable file
1017
vendor/nemiah/php-fints/lib/Fhp/FinTs.php
vendored
Executable file
File diff suppressed because it is too large
Load diff
125
vendor/nemiah/php-fints/lib/Fhp/MT535/MT535.php
vendored
Executable file
125
vendor/nemiah/php-fints/lib/Fhp/MT535/MT535.php
vendored
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\MT535;
|
||||
|
||||
use Fhp\Model\StatementOfHoldings\Holding;
|
||||
use Fhp\Model\StatementOfHoldings\StatementOfHoldings;
|
||||
|
||||
/**
|
||||
* Data format: MT 535 (Version SRG 1998)
|
||||
*
|
||||
* @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Finanzdatenformate_2010-08-06_final_version.pdf
|
||||
* Section: B.4
|
||||
*/
|
||||
class MT535
|
||||
{
|
||||
/** @var string */
|
||||
private $cleanedRawData;
|
||||
|
||||
public function __construct(string $rawData)
|
||||
{
|
||||
// The divider can be either \r\n or @@
|
||||
$divider = substr_count($rawData, "\r\n-") > substr_count($rawData, '@@-') ? "\r\n" : '@@';
|
||||
$this->cleanedRawData = preg_replace('#' . $divider . '([^:])#ms', '$1', $rawData);
|
||||
}
|
||||
|
||||
public function parseDepotWert(): float
|
||||
{
|
||||
preg_match('/:16R:ADDINFO(.*?):16S:ADDINFO/sm', $this->cleanedRawData, $block);
|
||||
preg_match('/EUR(.*)/sm', $block[1], $matches);
|
||||
return floatval(str_replace(',', '.', $matches[1]));
|
||||
}
|
||||
|
||||
public function parseHoldings(): StatementOfHoldings
|
||||
{
|
||||
$result = new StatementOfHoldings();
|
||||
preg_match_all('/:16R:FIN(.*?):16S:FIN/sm', $this->cleanedRawData, $blocks);
|
||||
foreach ($blocks[1] as $block) {
|
||||
$holding = new Holding();
|
||||
// handle ISIN, WKN & Name
|
||||
// :35B:ISIN DE0005190003/DE/519000BAY.MOTOREN WERKE AG ST
|
||||
if (preg_match('/^:35B:(.*?):/sm', $block, $iwn)) {
|
||||
preg_match('/^.{5}(.{12})/sm', $iwn[1], $r);
|
||||
$holding->setISIN($r[1]);
|
||||
preg_match('/^.{21}(.{6})/sm', $iwn[1], $r);
|
||||
$holding->setWKN($r[1]);
|
||||
preg_match('/^.{27}(.*)/sm', $iwn[1], $r);
|
||||
$holding->setName($r[1]);
|
||||
}
|
||||
|
||||
// handle acquisition price
|
||||
// e.g ':70E::HOLD//1STK23,968293+EUR'
|
||||
if (preg_match('/:70E::HOLD\/\/\d*STK2(\d*),(\d*)\+([A-Z]{3})/sm', $block, $iwn)) {
|
||||
$holding->setAcquisitionPrice((float) $iwn[1].'.'.$iwn[2]);
|
||||
if ($holding->getCurrency() === null) {
|
||||
$holding->setCurrency($iwn[3]);
|
||||
}
|
||||
}
|
||||
|
||||
// handle Price
|
||||
// :90B::MRKT//ACTU/EUR76,06
|
||||
// A1G1UF
|
||||
if (preg_match('/:90(.)::(.*?):/sm', $block, $iwn)) {
|
||||
if ($iwn[1] == 'B') {
|
||||
// Currency
|
||||
preg_match('/^.{11}(.{3})/sm', $iwn[2], $r);
|
||||
$holding->setCurrency($r[1]);
|
||||
// Price
|
||||
preg_match('/^.{14}(.*)/sm', $iwn[2], $r);
|
||||
$holding->setPrice(floatval(str_replace(',', '.', $r[1])));
|
||||
} elseif ($iwn[1] == 'A') {
|
||||
$holding->setCurrency('%');
|
||||
// Price
|
||||
preg_match('/^.{11}(.*)/sm', $iwn[2], $r);
|
||||
$holding->setPrice(floatval(str_replace(',', '.', $r[1])) / 100);
|
||||
}
|
||||
}
|
||||
|
||||
// handle Amount
|
||||
// :93B::AGGR//UNIT/2666,000
|
||||
if (preg_match('/:93B::(.*?):/sm', $block, $iwn)) {
|
||||
// Amount
|
||||
preg_match('/^.{11}(.*)/sm', $iwn[1], $r);
|
||||
$holding->setAmount(floatval(str_replace(',', '.', $r[1])));
|
||||
}
|
||||
|
||||
if ($holding->getAmount() !== null && $holding->getPrice() !== null) {
|
||||
if ($holding->getCurrency() === '%') {
|
||||
$holding->setValue($holding->getPrice() / 100);
|
||||
} else {
|
||||
$holding->setValue($holding->getPrice() * $holding->getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
// Bereitstellungsdatum
|
||||
// :98A::PRIC//20210304
|
||||
// :98C::STAT//20250104140541
|
||||
if (preg_match('/:98([AC])::(.*?):/sm', $block, $iwn)) {
|
||||
preg_match('/^.{6}(.{8})/sm', $iwn[2], $r);
|
||||
$holding->setDate($this->getDate($r[1]));
|
||||
$time = new \DateTime();
|
||||
if ($iwn[1] == 'C') {
|
||||
// 98C has a time component
|
||||
preg_match('/^.{14}(\d\d)(\d\d)(\d\d)/sm', $iwn[2], $r);
|
||||
$time->setTime($r[1], $r[2], $r[3]);
|
||||
} else {
|
||||
$time->setTime(0, 0);
|
||||
}
|
||||
$holding->setTime($time);
|
||||
}
|
||||
|
||||
$result->addHolding($holding);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getDate(string $val): \DateTime
|
||||
{
|
||||
preg_match('/(\d{4})(\d{2})(\d{2})/', $val, $m);
|
||||
try {
|
||||
return new \DateTime($m[1] . '-' . $m[2] . '-' . $m[3]);
|
||||
} catch (\Exception $e) {
|
||||
throw new \InvalidArgumentException("Invalid date: $val", 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/PostbankMT940.php
vendored
Executable file
39
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/PostbankMT940.php
vendored
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\MT940\Dialect;
|
||||
|
||||
use Fhp\MT940\MT940;
|
||||
|
||||
class PostbankMT940 extends MT940
|
||||
{
|
||||
public const DIALECT_ID = 'https://hbci.postbank.de/banking/hbci.do';
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function extractStructuredDataFromRemittanceLines($descriptionLines, string &$gvc, array &$rawLines, array $transaction): array
|
||||
{
|
||||
// z.B bei Zinsen o.ä. ist alles leer
|
||||
if (!isset($descriptionLines[0])) {
|
||||
return [];
|
||||
}
|
||||
$structuredStartFound = preg_match('/^[A-Z]{4}\+/', $descriptionLines[0]) === 1;
|
||||
|
||||
if ($structuredStartFound) {
|
||||
return parent::extractStructuredDataFromRemittanceLines($descriptionLines, $gvc, $rawLines, $transaction);
|
||||
}
|
||||
|
||||
// Bie Auslandsüberweisungen (=210)
|
||||
// Der Empfänger name steht als erstes im Verwendungszweck und teile des Verwendungszwecks stehen im Namen
|
||||
if ($gvc == '210') {
|
||||
$name = array_shift($descriptionLines);
|
||||
|
||||
array_unshift($descriptionLines, $rawLines[33]);
|
||||
array_unshift($descriptionLines, $rawLines[32]);
|
||||
$rawLines[32] = $name;
|
||||
$rawLines[33] = '';
|
||||
}
|
||||
|
||||
return [
|
||||
'SVWZ' => implode("\n", $descriptionLines),
|
||||
];
|
||||
}
|
||||
}
|
||||
87
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/SpardaMT940.php
vendored
Executable file
87
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/SpardaMT940.php
vendored
Executable file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\MT940\Dialect;
|
||||
|
||||
use Fhp\MT940\MT940;
|
||||
|
||||
class SpardaMT940 extends MT940
|
||||
{
|
||||
public const DIALECT_ID = 'https://fints.bankingonline.de/fints/FinTs30PinTanHttpGate';
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function extractStructuredDataFromRemittanceLines($descriptionLines, string &$gvc, array &$rawLines, array $transaction): array
|
||||
{
|
||||
$otherInfo = [];
|
||||
$structuredStartFound = false;
|
||||
$lines = [];
|
||||
foreach ($descriptionLines as $line) {
|
||||
if ($structuredStartFound || preg_match('/^[A-Z]{4}\+ /', $line) === 1) {
|
||||
$structuredStartFound = true;
|
||||
$lines[] = $line;
|
||||
} else {
|
||||
$otherInfo[] = $line;
|
||||
}
|
||||
}
|
||||
if (!$structuredStartFound) {
|
||||
return ['SVWZ' => implode("\n", $otherInfo)];
|
||||
}
|
||||
|
||||
// Beispiel
|
||||
/*
|
||||
0 => "SEPA-BASISLASTSCHRIFT"
|
||||
1 => "EREF+ xxxxxxxxxxxxxxxxxx MR"
|
||||
2 => "EF+ xxxxxxxxxxxxxxxx CRED+"
|
||||
3 => "XXxxxxxxxxxxxxxxxxx SVWZ+ A"
|
||||
4 => "bcdef ghijklmn opqr stuvwxy"
|
||||
5 => "z1 1234 678912345"
|
||||
6 => "ABWA+ Abcd Efghij"
|
||||
*/
|
||||
|
||||
$combined = '';
|
||||
foreach ($lines as $line) {
|
||||
// Sonderfall, für Zeile 2 aus dem Beispiel
|
||||
$combined .= preg_replace('/ ([A-Z]{4}\+)$/', ' $1 ', $line);
|
||||
}
|
||||
$combined = implode('', $lines);
|
||||
|
||||
// SEPA Bezeichner müssen in einer neuen Zeile Anfangen und kein Leerzeichen hinter dem + haben
|
||||
$fixed = preg_replace('/([A-Z]{4}\+) /', "\n$1", $combined);
|
||||
|
||||
$correctedLines = explode("\n", trim($fixed, "\n"));
|
||||
|
||||
// Buchungstext z.B. SEPA-ÜBERWEISUNG
|
||||
if (count($otherInfo) > 0) {
|
||||
$rawLines[0] = $bookingText = array_pop($otherInfo);
|
||||
|
||||
switch ($bookingText) {
|
||||
case 'SEPA-ÜBERWEISUNG':
|
||||
if ($transaction['credit_debit'] === static::CD_CREDIT) {
|
||||
$gvc = '166';
|
||||
}
|
||||
if ($transaction['credit_debit'] === static::CD_DEBIT) {
|
||||
$gvc = '177';
|
||||
}
|
||||
break;
|
||||
case 'SEPA-BASISLASTSCHRIFT':
|
||||
$gvc = '105';
|
||||
break;
|
||||
// case 'SEPA-RÜCKLASTSCHRIFT':
|
||||
// Hängt vom Betrag ab ?
|
||||
}
|
||||
}
|
||||
|
||||
// Rest vom Namen, wenn der > 27 Zeichen ist, ja ernsthaft
|
||||
if (count($otherInfo) > 0) {
|
||||
$rawLines[33] .= array_pop($otherInfo);
|
||||
}
|
||||
|
||||
$desc = parent::extractStructuredDataFromRemittanceLines($correctedLines, $gvc, $rawLines, $transaction);
|
||||
|
||||
if (isset($desc['SVWZ']) && str_starts_with($desc['SVWZ'], 'Dauerauftrag')) {
|
||||
$gvc = '152';
|
||||
$rawLines[0] = 'Dauerauftrag-Gutschrift';
|
||||
}
|
||||
|
||||
return $desc;
|
||||
}
|
||||
}
|
||||
258
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940.php
vendored
Executable file
258
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940.php
vendored
Executable file
|
|
@ -0,0 +1,258 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\MT940;
|
||||
|
||||
/**
|
||||
* Data format: MT 940 (Version SRG 2001)
|
||||
*
|
||||
* @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Messages_Finanzdatenformate_2010-08-06_final_version.pdf
|
||||
* Section: B.8
|
||||
*/
|
||||
class MT940
|
||||
{
|
||||
public const CD_CREDIT = 'credit';
|
||||
public const CD_DEBIT = 'debit';
|
||||
|
||||
/**
|
||||
* @throws MT940Exception
|
||||
*/
|
||||
public function parse(string $rawData): array
|
||||
{
|
||||
// The divider can be either \r\n or @@
|
||||
$divider = substr_count($rawData, "\r\n-") > substr_count($rawData, '@@-') ? "\r\n" : '@@';
|
||||
|
||||
$cleanedRawData = preg_replace('#' . $divider . '([^:])#ms', '$1', $rawData);
|
||||
|
||||
$booked = true;
|
||||
$result = [];
|
||||
$days = explode($divider . '-', $cleanedRawData);
|
||||
$soaDate = null;
|
||||
foreach ($days as &$day) {
|
||||
$day = explode($divider . ':', $day);
|
||||
|
||||
for ($i = 0, $cnt = count($day); $i < $cnt; ++$i) {
|
||||
if (preg_match("/\+\@[0-9]+\@$/", trim($day[$i]))) {
|
||||
$booked = false;
|
||||
}
|
||||
|
||||
// handle start balance
|
||||
// 60F:C160401EUR1234,56
|
||||
if (preg_match('/^60(F|M):/', $day[$i])) {
|
||||
// remove 60(F|M): for better parsing
|
||||
$day[$i] = substr($day[$i], 4);
|
||||
$soaDate = $this->getDate(substr($day[$i], 1, 6));
|
||||
|
||||
// if this statement date ist first seen set start_balance
|
||||
// Note: all further transactions in different statements with the same soaDate will be appended
|
||||
// there will be no new statement done for them. With bigger code changes this could be changed.
|
||||
// For now this is shortcutted like this for fixing https://github.com/nemiah/phpFinTS/issues/367
|
||||
if (!isset($result[$soaDate])) {
|
||||
$result[$soaDate] = ['start_balance' => []];
|
||||
$cdMark = substr($day[$i], 0, 1);
|
||||
if ($cdMark === 'C') {
|
||||
$result[$soaDate]['start_balance']['credit_debit'] = static::CD_CREDIT;
|
||||
} elseif ($cdMark === 'D') {
|
||||
$result[$soaDate]['start_balance']['credit_debit'] = static::CD_DEBIT;
|
||||
}
|
||||
|
||||
$amount = str_replace(',', '.', substr($day[$i], 10));
|
||||
$result[$soaDate]['start_balance']['amount'] = $amount;
|
||||
}
|
||||
} elseif (
|
||||
// found transaction
|
||||
// trx:61:1603310331DR637,39N033NONREF
|
||||
str_starts_with($day[$i], '61:')
|
||||
&& isset($day[$i + 1])
|
||||
&& str_starts_with($day[$i + 1], '86:')
|
||||
) {
|
||||
$transaction = substr($day[$i], 3);
|
||||
$description = substr($day[$i + 1], 3);
|
||||
|
||||
if (!isset($result[$soaDate]['transactions'])) {
|
||||
$result[$soaDate]['transactions'] = [];
|
||||
}
|
||||
|
||||
// short form for better handling
|
||||
$trx = &$result[$soaDate]['transactions'];
|
||||
|
||||
preg_match('/^\d{6}(\d{4})?(C|D|RC|RD)([A-Z]{1})?([^N]+)N/', $transaction, $trxMatch);
|
||||
if ($trxMatch[2] === 'C' || $trxMatch[2] === 'RC') {
|
||||
$trx[count($trx)]['credit_debit'] = static::CD_CREDIT;
|
||||
} elseif ($trxMatch[2] === 'D' || $trxMatch[2] === 'RD') {
|
||||
$trx[count($trx)]['credit_debit'] = static::CD_DEBIT;
|
||||
} else {
|
||||
throw new MT940Exception('cd mark not found in: ' . $transaction);
|
||||
}
|
||||
|
||||
$trx[count($trx) - 1]['is_storno'] = ($trxMatch[2] === 'RC' or $trxMatch[2] === 'RD');
|
||||
|
||||
$amount = $trxMatch[4];
|
||||
$amount = str_replace(',', '.', $amount);
|
||||
$trx[count($trx) - 1]['amount'] = $amount;
|
||||
|
||||
// :61:1605110509D198,02NMSCNONREF
|
||||
// 16 = year
|
||||
// 0511 = valuta date
|
||||
// 0509 = booking date
|
||||
|
||||
$year = substr($transaction, 0, 2);
|
||||
$valutaDate = $this->getDate($year . substr($transaction, 2, 4));
|
||||
$bookingDatePart = substr($transaction, 6, 4);
|
||||
|
||||
if (preg_match('/^\d{4}$/', $bookingDatePart) === 1) {
|
||||
// try to guess the correct year of the booking date
|
||||
|
||||
$valutaDateTime = new \DateTime($valutaDate);
|
||||
$bookingDateTime = new \DateTime($this->getDate($year . $bookingDatePart));
|
||||
|
||||
// the booking date can be before or after the valuata date
|
||||
// and one of them can be in another year for example 12-31 and 01-01
|
||||
|
||||
$diff = $valutaDateTime->diff($bookingDateTime);
|
||||
|
||||
// if diff is more than half a year
|
||||
if ($diff->days > 182) {
|
||||
// and positive
|
||||
if ($diff->invert === 0) {
|
||||
// its in the last year
|
||||
--$year;
|
||||
}
|
||||
// and negative
|
||||
else {
|
||||
// its in the next year
|
||||
++$year;
|
||||
}
|
||||
}
|
||||
$bookingDate = $this->getDate($year . $bookingDatePart);
|
||||
} else {
|
||||
// if booking date not set in :61, then we have to take it from :60F
|
||||
$bookingDate = $soaDate;
|
||||
}
|
||||
|
||||
$trx[count($trx) - 1]['booking_date'] = $bookingDate;
|
||||
$trx[count($trx) - 1]['valuta_date'] = $valutaDate;
|
||||
$trx[count($trx) - 1]['booked'] = $booked;
|
||||
|
||||
$trx[count($trx) - 1]['description'] = $this->parseDescription($description, $trx[count($trx) - 1]);
|
||||
} elseif (
|
||||
preg_match('/^62F:/', $day[$i]) // handle end balance
|
||||
) {
|
||||
// remove 62F: for better parsing
|
||||
$day[$i] = substr($day[$i], 4);
|
||||
$soaDate = $this->getDate(substr($day[$i], 1, 6));
|
||||
|
||||
if (isset($result[$soaDate])) {
|
||||
#$result[$soaDate] = ['end_balance' => []];
|
||||
|
||||
$amount = str_replace(',', '.', substr($day[$i], 10, -1));
|
||||
$cdMark = substr($day[$i], 0, 1);
|
||||
if ($cdMark == 'C') {
|
||||
$result[$soaDate]['end_balance']['credit_debit'] = static::CD_CREDIT;
|
||||
} elseif ($cdMark == 'D') {
|
||||
$result[$soaDate]['end_balance']['credit_debit'] = static::CD_DEBIT;
|
||||
$amount *= -1;
|
||||
}
|
||||
|
||||
$result[$soaDate]['end_balance']['amount'] = $amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function parseDescription($descr, $transaction): array
|
||||
{
|
||||
// Geschäftsvorfall-Code
|
||||
$gvc = substr($descr, 0, 3);
|
||||
|
||||
$prepared = [];
|
||||
$result = [];
|
||||
|
||||
// prefill with empty values
|
||||
for ($i = 0; $i <= 63; ++$i) {
|
||||
$prepared[$i] = null;
|
||||
}
|
||||
|
||||
$descr = str_replace('? ', '?', $descr);
|
||||
|
||||
preg_match_all('/\?(\d{2})([^\?]+)/', $descr, $matches, PREG_SET_ORDER);
|
||||
|
||||
$descriptionLines = [];
|
||||
$description1 = ''; // Legacy, could be removed.
|
||||
$description2 = ''; // Legacy, could be removed.
|
||||
foreach ($matches as $m) {
|
||||
$index = (int) $m[1];
|
||||
|
||||
if ((20 <= $index && $index <= 29) || (60 <= $index && $index <= 63)) {
|
||||
if (20 <= $index && $index <= 29) {
|
||||
$description1 .= $m[2];
|
||||
} else {
|
||||
$description2 .= $m[2];
|
||||
}
|
||||
$descriptionLines[] = $m[2];
|
||||
}
|
||||
$prepared[$index] = $m[2];
|
||||
}
|
||||
|
||||
$description = $this->extractStructuredDataFromRemittanceLines($descriptionLines, $gvc, $prepared, $transaction);
|
||||
|
||||
$result['booking_code'] = $gvc;
|
||||
$result['booking_text'] = trim($prepared[0] ?? '');
|
||||
$result['description'] = $description;
|
||||
$result['primanoten_nr'] = trim($prepared[10] ?? '');
|
||||
$result['description_1'] = trim($description1);
|
||||
$result['bank_code'] = trim($prepared[30] ?? '');
|
||||
$result['account_number'] = trim($prepared[31] ?? '');
|
||||
$result['name'] = trim(($prepared[32] ?? '') . ($prepared[33] ?? ''));
|
||||
$result['text_key_addition'] = trim($prepared[34] ?? '');
|
||||
$result['description_2'] = $description2;
|
||||
$result['desc_lines'] = $descriptionLines;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $descriptionLines that contain the remittance information
|
||||
* @param string $gvc Geschätsvorfallcode; Out-Parameter, might be changed from information in remittance info
|
||||
* @param string[] $rawLines All the lines in the Multi-Purpose-Field 86; Out-Parameter, might be changed from information in remittance info
|
||||
*/
|
||||
protected function extractStructuredDataFromRemittanceLines($descriptionLines, string &$gvc, array &$rawLines, array $transaction): array
|
||||
{
|
||||
$description = [];
|
||||
if (empty($descriptionLines) || strlen($descriptionLines[0]) < 5 || $descriptionLines[0][4] !== '+') {
|
||||
$description['SVWZ'] = implode('', $descriptionLines);
|
||||
} else {
|
||||
$lastType = null;
|
||||
foreach ($descriptionLines as $line) {
|
||||
if (strlen($line) >= 5 && $line[4] === '+') {
|
||||
if ($lastType != null) {
|
||||
$description[$lastType] = trim($description[$lastType]);
|
||||
}
|
||||
$lastType = substr($line, 0, 4);
|
||||
$description[$lastType] = substr($line, 5);
|
||||
} else {
|
||||
$description[$lastType] .= $line;
|
||||
}
|
||||
if (strlen($line) < 27) {
|
||||
// Usually, lines are 27 characters long. In case characters are missing, then it's either the end
|
||||
// of the current type or spaces have been trimmed from the end. We want to collapse multiple spaces
|
||||
// into one and we don't want to leave trailing spaces behind. So add a single space here to make up
|
||||
// for possibly missing spaces, and if it's the end of the type, it will be trimmed off later.
|
||||
$description[$lastType] .= ' ';
|
||||
}
|
||||
}
|
||||
$description[$lastType] = trim($description[$lastType]);
|
||||
}
|
||||
|
||||
return $description;
|
||||
}
|
||||
|
||||
protected function getDate(string $val): string
|
||||
{
|
||||
$val = '20' . $val;
|
||||
preg_match('/(\d{4})(\d{2})(\d{2})/', $val, $m);
|
||||
return $m[1] . '-' . $m[2] . '-' . $m[3];
|
||||
}
|
||||
}
|
||||
10
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940Exception.php
vendored
Executable file
10
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940Exception.php
vendored
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\MT940;
|
||||
|
||||
/**
|
||||
* Thrown for MT940-specific parsing errors.
|
||||
*/
|
||||
class MT940Exception extends \Exception
|
||||
{
|
||||
}
|
||||
147
vendor/nemiah/php-fints/lib/Fhp/Model/Account.php
vendored
Executable file
147
vendor/nemiah/php-fints/lib/Fhp/Model/Account.php
vendored
Executable file
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
/** @noinspection PhpUnused */
|
||||
|
||||
namespace Fhp\Model;
|
||||
|
||||
/**
|
||||
* Note: This account information is obtained from the HIUPD contained in the UPD data, but it lacks the BIC.
|
||||
*/
|
||||
class Account
|
||||
{
|
||||
/** @var string|null */
|
||||
protected $id;
|
||||
/** @var string|null */
|
||||
protected $accountNumber;
|
||||
/** @var string|null */
|
||||
protected $bankCode;
|
||||
/** @var string|null */
|
||||
protected $iban;
|
||||
/** @var string|null */
|
||||
protected $customerId;
|
||||
/** @var string|null */
|
||||
protected $currency;
|
||||
/** @var string|null */
|
||||
protected $accountOwnerName;
|
||||
/** @var string|null */
|
||||
protected $accountDescription;
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setId(?string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccountNumber(): ?string
|
||||
{
|
||||
return $this->accountNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setAccountNumber(?string $accountNumber)
|
||||
{
|
||||
$this->accountNumber = $accountNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBankCode(): ?string
|
||||
{
|
||||
return $this->bankCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setBankCode(?string $bankCode)
|
||||
{
|
||||
$this->bankCode = $bankCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIban(): ?string
|
||||
{
|
||||
return $this->iban;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setIban(?string $iban)
|
||||
{
|
||||
$this->iban = $iban;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomerId(): ?string
|
||||
{
|
||||
return $this->customerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustomerId(?string $customerId)
|
||||
{
|
||||
$this->customerId = $customerId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCurrency(): ?string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCurrency(?string $currency)
|
||||
{
|
||||
$this->currency = $currency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccountOwnerName(): ?string
|
||||
{
|
||||
return $this->accountOwnerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setAccountOwnerName(?string $accountOwnerName)
|
||||
{
|
||||
$this->accountOwnerName = $accountOwnerName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccountDescription(): ?string
|
||||
{
|
||||
return $this->accountDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setAccountDescription(?string $accountDescription)
|
||||
{
|
||||
$this->accountDescription = $accountDescription;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
181
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/DataElement.php
vendored
Executable file
181
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/DataElement.php
vendored
Executable file
|
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model\FlickerTan;
|
||||
|
||||
/**
|
||||
* Represents a Data Element which is part of the Flicker Tan Challenge. Shortens the whole challenge.
|
||||
* @see https://www.hbci-zka.de/dokumente/spezifikation_deutsch/hhd/Belegungsrichtlinien%20TANve1.5%20FV%20vom%202018-04-16.pdf
|
||||
*/
|
||||
class DataElement
|
||||
{
|
||||
public const ENC_ASCII = '1';
|
||||
public const ENC_ASC = self::ENC_ASCII;
|
||||
public const ENC_BCD = '0';
|
||||
|
||||
/**
|
||||
* @var string the encoding (either self::ENC_ASC or self::ENC_BCD)
|
||||
*/
|
||||
protected $enc;
|
||||
|
||||
/**
|
||||
* @var string the raw data string
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* @var string the highest bit of the generated header
|
||||
*/
|
||||
protected $headerHighBit;
|
||||
|
||||
/**
|
||||
* @param $challenge string raw challenge text
|
||||
* @return array [string $reducedChallenge, FlickerTanDataElement $dataElementObject]
|
||||
* @see https://www.hbci-zka.de/dokumente/spezifikation_deutsch/hhd/Belegungsrichtlinien%20TANve1.5%20FV%20vom%202018-04-16.pdf
|
||||
*/
|
||||
public static function parseNextBlock(string $challenge): array
|
||||
{
|
||||
if (empty($challenge)) {
|
||||
return [$challenge, new self('')];
|
||||
}
|
||||
$length = (int) substr($challenge, 0, 2);
|
||||
$data = substr($challenge, 2, $length);
|
||||
if (strlen($data) !== $length) {
|
||||
throw new \InvalidArgumentException('Parsing went wrong');
|
||||
}
|
||||
$rest = substr($challenge, 2 + $length);
|
||||
return [$rest, new self($data)];
|
||||
}
|
||||
|
||||
/**
|
||||
* The needed encoding will be automatically determined by the type of data
|
||||
* @param string $data the raw data
|
||||
*/
|
||||
protected function __construct(string $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->headerHighBit = 0;
|
||||
if (is_numeric($this->data) || empty($this->data)) {
|
||||
$this->enc = self::ENC_BCD;
|
||||
} else {
|
||||
$this->enc = self::ENC_ASC;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int amount of bytes in data
|
||||
*/
|
||||
protected function getLength(): int
|
||||
{
|
||||
if ($this->enc === self::ENC_BCD) {
|
||||
return ceil(strlen($this->data) / 2);
|
||||
}
|
||||
return strlen($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string returns the hex representation from the header of the data element as string with length 2
|
||||
*/
|
||||
public function getHeaderHex(): string
|
||||
{
|
||||
$lengthBin = str_pad(base_convert($this->getLength(), 10, 2), 6, '0', STR_PAD_LEFT);
|
||||
$headerHex = base_convert($this->headerHighBit . $this->enc . $lengthBin, 2, 16);
|
||||
return str_pad($headerHex, 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string returns the hex representation of the data, depending on the set encoding
|
||||
*/
|
||||
public function getDataHex(): string
|
||||
{
|
||||
if ($this->enc === self::ENC_BCD) {
|
||||
// base 10 and hex BCD encoded numbers are the same in range 0 to 9
|
||||
$hexData = $this->data;
|
||||
// Pad on Byte lenght
|
||||
if (strlen($hexData) % 2 === 1) {
|
||||
$hexData .= 'F';
|
||||
}
|
||||
return $hexData;
|
||||
}
|
||||
// ASCII encoding
|
||||
$hexData = '';
|
||||
foreach (str_split($this->data) as $char) {
|
||||
$hexData .= base_convert(ord($char), 10, 16);
|
||||
}
|
||||
return $hexData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string returns the hex representation of the Data Element incl header information
|
||||
*/
|
||||
public function toHex(): string
|
||||
{
|
||||
if (empty($this->data)) {
|
||||
return '';
|
||||
}
|
||||
return $this->getHeaderHex() . $this->getDataHex();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hex which will be converted
|
||||
* @param int $length to which the byte will be padded (default: 8)
|
||||
* @return string binary representation of hex value as string with length $length
|
||||
*/
|
||||
public static function hexToByte(string $hex, int $length = 8): string
|
||||
{
|
||||
$byte = base_convert($hex, 16, 2);
|
||||
return str_pad($byte, $length, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int calculates Luhn Checksum of this object
|
||||
*/
|
||||
public function getLuhnChecksum(): int
|
||||
{
|
||||
return self::calcLuhn($this->getDataHex());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int calculates the Luhn checksum of a given hex string
|
||||
*/
|
||||
protected static function calcLuhn(string $hex): int
|
||||
{
|
||||
$sum = 0;
|
||||
$doubleIt = false;
|
||||
foreach (str_split($hex) as $char) {
|
||||
$number = (int) base_convert($char, 16, 10);
|
||||
if ($doubleIt) {
|
||||
$number *= 2;
|
||||
$decRep = str_split($number);
|
||||
foreach ($decRep as $value) {
|
||||
$sum += (int) $value;
|
||||
}
|
||||
} else {
|
||||
$sum += $number;
|
||||
}
|
||||
$doubleIt = !$doubleIt;
|
||||
}
|
||||
return $sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some handy debug info
|
||||
*/
|
||||
public function __debugInfo(): ?array
|
||||
{
|
||||
return [
|
||||
'header' => $this->getHeaderHex(),
|
||||
'data' => $this->data,
|
||||
'hex-data' => $this->getDataHex(),
|
||||
'hex' => $this->toHex(),
|
||||
'luhn' => $this->getLuhnChecksum(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string hex representation of object
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->toHex();
|
||||
}
|
||||
}
|
||||
105
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/StartCode.php
vendored
Executable file
105
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/StartCode.php
vendored
Executable file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model\FlickerTan;
|
||||
|
||||
/**
|
||||
* Represents a startcode in the TAN Flicker Challenge. Shortens the given challenge
|
||||
* @see https://www.hbci-zka.de/dokumente/spezifikation_deutsch/hhd/Belegungsrichtlinien%20TANve1.5%20FV%20vom%202018-04-16.pdf
|
||||
*/
|
||||
class StartCode extends DataElement
|
||||
{
|
||||
/**
|
||||
* @var string[] of the control bytes in hex representation
|
||||
*/
|
||||
private $controlBytes;
|
||||
|
||||
/**
|
||||
* Parses Header information, control bytes and start code
|
||||
* @param string the rest of the given challenge from the bank to parse
|
||||
* @return array [string, FlickerTanStartCode]
|
||||
*/
|
||||
public static function parseNextBlock(string $challenge): array
|
||||
{
|
||||
$header = substr($challenge, 0, 2);
|
||||
$rest = substr($challenge, 2);
|
||||
$byte = self::hexToByte($header);
|
||||
/* LS encoded base 16, bit idx:
|
||||
* 0: 0=without ctrl byte 1=with ctrl byte
|
||||
* 1: 0=BCD 1=ASC // never set
|
||||
* 2 - 7: intval: start code length
|
||||
*/
|
||||
$hasControl = $byte[0] === '1';
|
||||
$length = (int) base_convert(substr($byte, 2, 6), 2, 10);
|
||||
[$ctrlBytes, $rest] = self::parseControlBytes($rest, $hasControl);
|
||||
$data = substr($rest, 0, $length);
|
||||
$rest = substr($rest, $length);
|
||||
return [$rest, new self($ctrlBytes, $data)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to parse the control bytes
|
||||
* @param string $challenge the unparsed rest of the challenge string
|
||||
* @param bool $hasControl is a controlbyte expected
|
||||
* @return array [string[] of controlbytes, string unparsed rest of the challenge]
|
||||
*/
|
||||
private static function parseControlBytes(string $challenge, bool $hasControl): array
|
||||
{
|
||||
$controlBytes = [];
|
||||
$rest = $challenge;
|
||||
while ($hasControl) {
|
||||
$ctrl = substr($challenge, 0, 2);
|
||||
$controlBytes[] = $ctrl;
|
||||
$rest = substr($challenge, 2);
|
||||
$hasControl = self::hexToByte($ctrl)[0] === '1';
|
||||
}
|
||||
return [$controlBytes, $rest];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException if $ctrlBytes are unequal to ['01'] -> old version not supported so far
|
||||
*/
|
||||
protected function __construct(array $ctrlBytes, string $data)
|
||||
{
|
||||
if ($ctrlBytes !== ['01']) {
|
||||
throw new \InvalidArgumentException('Other versions then 1.4 are not supported');
|
||||
}
|
||||
parent::__construct($data);
|
||||
$this->controlBytes = $ctrlBytes;
|
||||
$this->headerHighBit = '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toHex(): string
|
||||
{
|
||||
return $this->getHeaderHex() . implode('', $this->controlBytes) . $this->getDataHex();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getLuhnChecksum(): int
|
||||
{
|
||||
$luhn = 0;
|
||||
foreach ($this->controlBytes as $ctrl) {
|
||||
$luhn = self::calcLuhn($ctrl);
|
||||
}
|
||||
$luhn += parent::getLuhnChecksum(); // Luhn from the data (of the startcode)
|
||||
return $luhn;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function __debugInfo(): ?array
|
||||
{
|
||||
return [
|
||||
'header' => $this->getHeaderHex(),
|
||||
'ctrl' => $this->controlBytes,
|
||||
'data' => $this->data,
|
||||
'hex-data' => $this->getDataHex(),
|
||||
'luhn' => $this->getLuhnChecksum(),
|
||||
];
|
||||
}
|
||||
}
|
||||
167
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/SvgRenderer.php
vendored
Executable file
167
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/SvgRenderer.php
vendored
Executable file
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model\FlickerTan;
|
||||
|
||||
/**
|
||||
* inspired by @see https://github.com/willuhn/hbci4java/blob/master/src/org/kapott/hbci/manager/FlickerCode.java
|
||||
* documentation @see tan_hhd_uc_v14.pdf e.g. https://github.com/willuhn/hbci4java/blob/master/doc/tan_hhd_uc_v14.pdf
|
||||
*/
|
||||
class SvgRenderer
|
||||
{
|
||||
private $svg;
|
||||
|
||||
/**
|
||||
* @var string[] the code in half-bit representation (string has length 4)
|
||||
*/
|
||||
private $bitPattern;
|
||||
|
||||
/**
|
||||
* @var int blink frequency in Hz [1/s] should be between 2 and 20 Hz by documentation, but many TAN Generators are able to fetch 40 Hz as well
|
||||
*/
|
||||
private $frequency;
|
||||
|
||||
/**
|
||||
* @param string[] $bitPattern a bit pattern in the format from {@see TanRequestChallengeFlicker::getFlickerPattern()}
|
||||
* @param int $flickerFrequenz in Hz [1/s] between 2 and 40 Hz allowed
|
||||
* @param int $width width of the svg, aspect ratio around 2:1 is recommended, but not enforced, default 210
|
||||
* @param int $height height of the svg (will not adapt to width automatic), default 130
|
||||
* @param string $id DOM id of the svg, which can be used as a selector via JS, e.g. to change height or width on clientside
|
||||
* @throws \InvalidArgumentException thrown if $flickerFrequenz or $bitPattern are faulty formed
|
||||
*/
|
||||
public function __construct(array $bitPattern, int $flickerFrequenz = 10, int $width = 210, int $height = 130, string $id = 'flickerTanSVG')
|
||||
{
|
||||
$this->validate($bitPattern, $flickerFrequenz);
|
||||
$this->frequency = $flickerFrequenz;
|
||||
// prefix sync identifier
|
||||
$this->bitPattern = $bitPattern;
|
||||
// do the svg
|
||||
$doc = [];
|
||||
// init black background rect with rounded corners
|
||||
$doc[] = $this->buildNode('rect', [
|
||||
'width' => 210,
|
||||
'height' => 105,
|
||||
'rx' => 7.5,
|
||||
'ry' => 7.5,
|
||||
'fill' => 'black',
|
||||
]);
|
||||
// triangle for aiming help for hardware tan generator
|
||||
$doc[] = $this->buildNode('polygon', [
|
||||
'points' => [
|
||||
'25,18', // middle bottom
|
||||
'32,5', // top right
|
||||
'18,5', // top left
|
||||
],
|
||||
'fill' => 'grey',
|
||||
]);
|
||||
$doc[] = $this->buildNode('polygon', [
|
||||
'points' => [ // x shifted by 160
|
||||
'185,18', // middle bottom
|
||||
'192,5', // top right
|
||||
'178,5', // top left
|
||||
],
|
||||
'fill' => 'grey',
|
||||
]);
|
||||
|
||||
// init flicker rectangles
|
||||
for ($i = 0; $i < 5; ++$i) {
|
||||
$doc[] = $this->buildNode('rect', [
|
||||
'x' => 40 * $i + 10,
|
||||
'y' => 20,
|
||||
'width' => 30,
|
||||
'height' => 75,
|
||||
], [$this->getAnimation($i)]);
|
||||
}
|
||||
$docAttr = [
|
||||
'xmlns' => 'http://www.w3.org/2000/svg',
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'viewBox' => '0 0 210 105',
|
||||
'preserveAspectRatio' => 'none',
|
||||
'id' => $id,
|
||||
];
|
||||
$this->svg = $this->buildNode('svg', $docAttr, $doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $channelNumber flickerRectangles numbered from left to right, 0 is clock, 1 is 2^0, ..., 4 is 2^3 half byte representation
|
||||
*/
|
||||
private function getAnimation(int $channelNumber): string
|
||||
{
|
||||
$timePerHalfByte = 1 / $this->frequency * 2;
|
||||
$attr = [
|
||||
'attributeName' => 'fill',
|
||||
'calcMode' => 'discrete',
|
||||
'repeatCount' => 'indefinite',
|
||||
];
|
||||
if ($channelNumber === 0) {
|
||||
// first rectangle is the clock
|
||||
$attr['values'] = 'white;black;white';
|
||||
$attr['keyFrames'] = '0;0.5;1';
|
||||
$attr['dur'] = $timePerHalfByte . 's';
|
||||
} else {
|
||||
// arrange keyframes and colors
|
||||
$colors = array_map(static function (string $pattern) use ($channelNumber) {
|
||||
return $pattern[$channelNumber - 1] === '1' ? 'white' : 'black';
|
||||
}, $this->bitPattern);
|
||||
$keyFrames = range(0, 1, 1.0 / count($this->bitPattern));
|
||||
|
||||
$attr['values'] = implode(';', $colors);
|
||||
$attr['keyFrames'] = implode(';', $keyFrames);
|
||||
$attr['dur'] = ($timePerHalfByte * count($this->bitPattern)) . 's';
|
||||
}
|
||||
return $this->buildNode('animate', $attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tag the tag of the node
|
||||
* @param array $attributes name-value pairs of the node attributes
|
||||
* @param string[] $childs of the node
|
||||
* @return string the string representation of the whole node with cilds
|
||||
*/
|
||||
private function buildNode(string $tag, array $attributes = [], array $childs = []): string
|
||||
{
|
||||
$attr = [];
|
||||
foreach ($attributes as $name => $value) {
|
||||
switch ($name) {
|
||||
case 'fill':
|
||||
$attr[] = "style='fill: $value'";
|
||||
break;
|
||||
case 'points':
|
||||
$attr[] = "points='" . implode(' ', $value) . "'";
|
||||
break;
|
||||
default:
|
||||
$attr[] = "$name='$value'";
|
||||
}
|
||||
}
|
||||
$attrStr = implode(' ', $attr);
|
||||
$childStr = implode(PHP_EOL, $childs);
|
||||
return "<$tag $attrStr>$childStr</$tag>";
|
||||
}
|
||||
|
||||
public function getImage(): string
|
||||
{
|
||||
return $this->svg;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input for frequency and bit pattern
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
private function validate(array $bitPattern, int $frequency): void
|
||||
{
|
||||
if ($frequency < 2 || $frequency > 40) {
|
||||
throw new \InvalidArgumentException('Frequency is not between 2 and 40 Hz');
|
||||
}
|
||||
foreach ($bitPattern as $idx => $pattern) {
|
||||
// detect if a string is not length 4 with only 0 and 1 chars
|
||||
if (!preg_match('/^[01]{4}$/', $pattern)) {
|
||||
throw new \InvalidArgumentException("Bit Pattern at index $idx is faulty, only 0 and 1 are allowed with length 4");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/TanRequestChallengeFlicker.php
vendored
Executable file
153
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/TanRequestChallengeFlicker.php
vendored
Executable file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model\FlickerTan;
|
||||
|
||||
use Fhp\Syntax\Bin;
|
||||
|
||||
/**
|
||||
* Parses the HHDUC Flicker Tan Challenge to a Flicker pattern with suffixed control sequence
|
||||
* @see https://www.hbci-zka.de/dokumente/spezifikation_deutsch/hhd/Belegungsrichtlinien%20TANve1.5%20FV%20vom%202018-04-16.pdf
|
||||
*/
|
||||
class TanRequestChallengeFlicker
|
||||
{
|
||||
/**
|
||||
* @var string original challenge data
|
||||
*/
|
||||
private $challenge;
|
||||
|
||||
/**
|
||||
* @var StartCode holds and parses the startcode block of the challenge
|
||||
*/
|
||||
private $startCode;
|
||||
|
||||
/**
|
||||
* @var DataElement[] Holds and parses the first DataElement of the challenge, 3 max
|
||||
*/
|
||||
private $dataElements;
|
||||
|
||||
public function __construct(Bin $challengeBin)
|
||||
{
|
||||
$this->challenge = $challengeBin->getData();
|
||||
$this->parseChallenge();
|
||||
}
|
||||
|
||||
private function parseChallenge(): void
|
||||
{
|
||||
$reducedChallenge = trim(str_replace(' ', '', $this->challenge));
|
||||
// length of whole challenge (without lc) max 255 | encoding: base 10
|
||||
$lc = (int) substr($reducedChallenge, 0, 3);
|
||||
$reducedChallenge = substr($reducedChallenge, 3);
|
||||
if (strlen($reducedChallenge) !== $lc) {
|
||||
throw new \InvalidArgumentException("Wrong length of TAN Challenge expected: $lc - found: ". strlen($reducedChallenge). ' - only Version 1.4 supported');
|
||||
}
|
||||
|
||||
[$reducedChallenge, $this->startCode] = StartCode::parseNextBlock($reducedChallenge);
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
[$reducedChallenge, $de] = DataElement::parseNextBlock($reducedChallenge);
|
||||
$this->dataElements[$i] = $de;
|
||||
}
|
||||
if (!empty($reducedChallenge)) {
|
||||
throw new \InvalidArgumentException("Challenge has unexpected ending $reducedChallenge");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string the xor checksum string in hex base
|
||||
*/
|
||||
private function calcXorChecksum(): string
|
||||
{
|
||||
$xor = 0b0000; // bin Representation of 0
|
||||
$hex = str_split($this->getHexPayload());
|
||||
foreach ($hex as $hexChar) {
|
||||
$intVal = (int) base_convert($hexChar, 16, 10);
|
||||
$xor ^= $intVal; // xor operator
|
||||
}
|
||||
return base_convert($xor, 10, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string returns hex representation of the flicker code
|
||||
*/
|
||||
private function getHexPayload(): string
|
||||
{
|
||||
$hex = $this->startCode->toHex();
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
$hex .= $this->dataElements[$i]->toHex();
|
||||
}
|
||||
$lc = strlen($hex) / 2 + 1;
|
||||
$lc = str_pad(base_convert($lc, 10, 16), 2, '0', STR_PAD_LEFT);
|
||||
return $lc . $hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* calculates Luhn Checksum over the whole code
|
||||
*/
|
||||
private function calcLuhnChecksum(): int
|
||||
{
|
||||
$luhn = $this->startCode->getLuhnChecksum();
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
$luhn += $this->dataElements[$i]->getLuhnChecksum();
|
||||
}
|
||||
return (10 - ($luhn % 10)) % 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string hex representation of challenge
|
||||
*/
|
||||
public function getHex(): string
|
||||
{
|
||||
$payload = $this->getHexPayload();
|
||||
$luhn = $this->calcLuhnChecksum();
|
||||
$xor = $this->calcXorChecksum();
|
||||
|
||||
return $payload . $luhn . $xor;
|
||||
}
|
||||
|
||||
/**
|
||||
* takes Hex Representation and builds bit patterns from it. Notable differences to the hex code:
|
||||
* - prefixes 0FFF to the hex code (F0FF after swap)
|
||||
* - swaps half bytes e.g. 0F FF ... -> F0 FF ...
|
||||
* Hints for rendering:
|
||||
* - 1 equals white, 0 equals black rectangle (other colors are possible, as long contrast is high enough, but unadvised)
|
||||
* - The Tan Generator expects the following onscreen pattern: | clock | 2^0 | 2^1 | 2^2 | 2^3 |
|
||||
* - Tan Generators read all 4 values on white to black flank, it is suggested to change the pattern on the black to white flank
|
||||
* - each entry in the returned array will be hold for the whole clock cycle (both colors)
|
||||
* @return string[] integer indexed array with strings, each 4 chars long with 0 or 1, which represent the expected flicker patterns
|
||||
*/
|
||||
public function getFlickerPattern(): array
|
||||
{
|
||||
$hexCode = $this->getHex();
|
||||
$bitPattern = [];
|
||||
// starting pattern - beginning of the pattern 0xFOFF
|
||||
$bitPattern[] = '1111';
|
||||
$bitPattern[] = '0000';
|
||||
$bitPattern[] = '1111';
|
||||
$bitPattern[] = '1111';
|
||||
// convert hex code to flicker pattern
|
||||
$len = strlen($hexCode);
|
||||
for ($i = 0; $i < $len; $i += 2) {
|
||||
// convert hex to bin representation of 1 byte at a time
|
||||
$byte = base_convert(substr($hexCode, $i, 2), 16, 2);
|
||||
// add missing zeros to the left
|
||||
$byte = str_pad($byte, 8, '0', STR_PAD_LEFT);
|
||||
// reverse order of half-bytes; flicker pattern is | clock | 2^0 | 2^1 | 2^2 | 2^3 |
|
||||
$firstHalfByte = strrev(substr($byte, 0, 4));
|
||||
$secondHalfByte = strrev(substr($byte, 4, 4));
|
||||
// change order from first and second half byte (@see C.2)
|
||||
$bitPattern[] = $secondHalfByte;
|
||||
$bitPattern[] = $firstHalfByte;
|
||||
}
|
||||
return $bitPattern;
|
||||
}
|
||||
|
||||
public function __debugInfo(): ?array
|
||||
{
|
||||
return [
|
||||
'startcode' => $this->startCode,
|
||||
'dataElements' => $this->dataElements,
|
||||
'payload' => $this->getHexPayload(),
|
||||
'luhn' => $this->calcLuhnChecksum(),
|
||||
'xor' => $this->calcXorChecksum(),
|
||||
];
|
||||
}
|
||||
}
|
||||
130
vendor/nemiah/php-fints/lib/Fhp/Model/NoPsd2TanMode.php
vendored
Executable file
130
vendor/nemiah/php-fints/lib/Fhp/Model/NoPsd2TanMode.php
vendored
Executable file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model;
|
||||
|
||||
use Fhp\Segment\TAN\HKTAN;
|
||||
|
||||
/**
|
||||
* This is a placeholder used instead of a real {@link TanMode} in order to signal that the bank's HBCI interface
|
||||
* supports no strong authentication whatsoever and thus also no TAN modes. While it should still support the
|
||||
* PIN/TAN authentication scheme (that's the only one that this library implements), not supporting the TAN part of
|
||||
* means, in times of PSD2 regulations, that the HBCI interface is limited to read-only operations (like reading
|
||||
* accounts and statements) and a separate login (through an app or web UI) is required regularly for the HBCI
|
||||
* access to keep working.
|
||||
*/
|
||||
final class NoPsd2TanMode implements TanMode
|
||||
{
|
||||
public const ID = -1;
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getId(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getName(): string
|
||||
{
|
||||
return 'No PSD2/TANs supported';
|
||||
}
|
||||
|
||||
public function isProzessvariante2(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function isDecoupled(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getChallengeLabel(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getMaxChallengeLength(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getMaxTanLength(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getTanFormat(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function needsTanMedium(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getSmsAbbuchungskontoErforderlich(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getAuftraggeberkontoErforderlich(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getChallengeKlasseErforderlich(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getAntwortHhdUcErforderlich(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getMaxDecoupledChecks(): int
|
||||
{
|
||||
throw new \RuntimeException('Only allowed for decoupled TAN modes');
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getFirstDecoupledCheckDelaySeconds(): int
|
||||
{
|
||||
throw new \RuntimeException('Only allowed for decoupled TAN modes');
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getPeriodicDecoupledCheckDelaySeconds(): int
|
||||
{
|
||||
throw new \RuntimeException('Only allowed for decoupled TAN modes');
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function allowsManualConfirmation(): bool
|
||||
{
|
||||
throw new \RuntimeException('Only allowed for decoupled TAN modes');
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function allowsAutomatedPolling(): bool
|
||||
{
|
||||
throw new \RuntimeException('Only allowed for decoupled TAN modes');
|
||||
}
|
||||
|
||||
public function createHKTAN(): HKTAN
|
||||
{
|
||||
throw new \AssertionError('HKTAN should not be needed when the bank does not support PSD2');
|
||||
}
|
||||
}
|
||||
97
vendor/nemiah/php-fints/lib/Fhp/Model/SEPAAccount.php
vendored
Executable file
97
vendor/nemiah/php-fints/lib/Fhp/Model/SEPAAccount.php
vendored
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model;
|
||||
|
||||
/**
|
||||
* Note: This account information is obtained from the HISPA response to a HKSPA request.
|
||||
*/
|
||||
class SEPAAccount
|
||||
{
|
||||
// All fields are nullable, but the overall SEPAAccount is only valid if at least {IBAN,BIC} or {accountNumber,blz} are present.
|
||||
|
||||
/** @var string|null */
|
||||
protected $iban;
|
||||
/** @var string|null */
|
||||
protected $bic;
|
||||
/** @var string|null */
|
||||
protected $accountNumber;
|
||||
/** @var string|null */
|
||||
protected $subAccount;
|
||||
/** @var string|null */
|
||||
protected $blz;
|
||||
|
||||
public function getIban(): ?string
|
||||
{
|
||||
return $this->iban;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setIban(?string $iban)
|
||||
{
|
||||
$this->iban = $iban;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBic(): ?string
|
||||
{
|
||||
return $this->bic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setBic(?string $bic)
|
||||
{
|
||||
$this->bic = $bic;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccountNumber(): ?string
|
||||
{
|
||||
return $this->accountNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setAccountNumber(?string $accountNumber)
|
||||
{
|
||||
$this->accountNumber = $accountNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSubAccount(): ?string
|
||||
{
|
||||
return $this->subAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSubAccount(?string $subAccount)
|
||||
{
|
||||
$this->subAccount = $subAccount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBlz(): ?string
|
||||
{
|
||||
return $this->blz;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setBlz(?string $blz)
|
||||
{
|
||||
$this->blz = $blz;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
130
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/Statement.php
vendored
Executable file
130
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/Statement.php
vendored
Executable file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model\StatementOfAccount;
|
||||
|
||||
class Statement
|
||||
{
|
||||
public const CD_CREDIT = 'credit';
|
||||
public const CD_DEBIT = 'debit';
|
||||
|
||||
/**
|
||||
* @var array of Transaction
|
||||
*/
|
||||
protected $transactions = [];
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected $startBalance = 0.0;
|
||||
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
protected $endBalance = null;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $creditDebit = null;
|
||||
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
protected $date;
|
||||
|
||||
/**
|
||||
* Get transactions
|
||||
*
|
||||
* @return Transaction[]
|
||||
*/
|
||||
public function getTransactions(): array
|
||||
{
|
||||
return $this->transactions;
|
||||
}
|
||||
|
||||
public function addTransaction(Transaction $transaction)
|
||||
{
|
||||
$this->transactions[] = $transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get startBalance
|
||||
*/
|
||||
public function getStartBalance(): float
|
||||
{
|
||||
return $this->startBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set startBalance
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setStartBalance(float $startBalance)
|
||||
{
|
||||
$this->startBalance = (float) $startBalance;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get endBalance
|
||||
* @return ?float returns the value, if given by the bank or null if unknown
|
||||
*/
|
||||
public function getEndBalance(): ?float
|
||||
{
|
||||
return $this->endBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set endBalance
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setEndBalance(float $endBalance)
|
||||
{
|
||||
$this->endBalance = (float) $endBalance;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get creditDebit
|
||||
*/
|
||||
public function getCreditDebit(): ?string
|
||||
{
|
||||
return $this->creditDebit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set creditDebit
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCreditDebit(?string $creditDebit)
|
||||
{
|
||||
$this->creditDebit = $creditDebit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date
|
||||
*/
|
||||
public function getDate(): \DateTime
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set date
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDate(\DateTime $date)
|
||||
{
|
||||
$this->date = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
126
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/StatementOfAccount.php
vendored
Executable file
126
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/StatementOfAccount.php
vendored
Executable file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model\StatementOfAccount;
|
||||
|
||||
use Fhp\MT940\MT940;
|
||||
|
||||
class StatementOfAccount
|
||||
{
|
||||
/**
|
||||
* @var Statement[]
|
||||
*/
|
||||
protected $statements = [];
|
||||
|
||||
/**
|
||||
* Get statements
|
||||
*
|
||||
* @return Statement[]
|
||||
*/
|
||||
public function getStatements(): array
|
||||
{
|
||||
return $this->statements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets statement for given date.
|
||||
*
|
||||
* @param string|\DateTime $date
|
||||
*/
|
||||
public function getStatementForDate($date): ?Statement
|
||||
{
|
||||
if (is_string($date)) {
|
||||
$date = static::parseDate($date);
|
||||
}
|
||||
|
||||
foreach ($this->statements as $stmt) {
|
||||
if ($stmt->getDate() == $date) {
|
||||
return $stmt;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a statement with given date exists.
|
||||
*
|
||||
* @param string|\DateTime $date
|
||||
*/
|
||||
public function hasStatementForDate($date): bool
|
||||
{
|
||||
return null !== $this->getStatementForDate($date);
|
||||
}
|
||||
|
||||
private static function parseDate(string $date): \DateTime
|
||||
{
|
||||
try {
|
||||
return new \DateTime($date);
|
||||
} catch (\Exception $e) {
|
||||
throw new \InvalidArgumentException("Invalid date: $date", 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array A parsed MT940 dataset, as returned from {@link MT940::parse()}.
|
||||
* @return StatementOfAccount A new instance that contains the given data.
|
||||
*/
|
||||
public static function fromMT940Array(array $array): StatementOfAccount
|
||||
{
|
||||
$result = new StatementOfAccount();
|
||||
foreach ($array as $date => $statement) {
|
||||
if ($result->hasStatementForDate($date)) {
|
||||
$statementModel = $result->getStatementForDate($date);
|
||||
} else {
|
||||
$statementModel = new Statement();
|
||||
$statementModel->setDate(static::parseDate($date));
|
||||
if (isset($statement['start_balance']['amount'])) {
|
||||
$statementModel->setStartBalance((float) $statement['start_balance']['amount']);
|
||||
}
|
||||
if (isset($statement['end_balance'])) {
|
||||
$statementModel->setEndBalance((float) $statement['end_balance']['amount'] * ($statement["end_balance"]['credit_debit'] == MT940::CD_CREDIT ? 1 : -1));
|
||||
}
|
||||
if (isset($statement['start_balance']['credit_debit'])) {
|
||||
$statementModel->setCreditDebit($statement['start_balance']['credit_debit']);
|
||||
}
|
||||
$result->statements[] = $statementModel;
|
||||
}
|
||||
|
||||
if (isset($statement['transactions'])) {
|
||||
foreach ($statement['transactions'] as $trx) {
|
||||
$replaceIn = [
|
||||
'booking_text',
|
||||
'description_1',
|
||||
'description_2',
|
||||
'description',
|
||||
'name',
|
||||
];
|
||||
foreach ($replaceIn as $k) {
|
||||
if (isset($trx['description'][$k])) {
|
||||
$trx['description'][$k] = str_replace('@@', '', $trx['description'][$k]);
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = new Transaction();
|
||||
$transaction->setBookingDate(static::parseDate($trx['booking_date']));
|
||||
$transaction->setValutaDate(static::parseDate($trx['valuta_date']));
|
||||
$transaction->setCreditDebit($trx['credit_debit']);
|
||||
$transaction->setIsStorno($trx['is_storno']);
|
||||
$transaction->setAmount($trx['amount']);
|
||||
$transaction->setBookingCode($trx['description']['booking_code']);
|
||||
$transaction->setBookingText($trx['description']['booking_text']);
|
||||
$transaction->setDescription1($trx['description']['description_1']);
|
||||
$transaction->setDescription2($trx['description']['description_2']);
|
||||
$transaction->setStructuredDescription($trx['description']['description']);
|
||||
$transaction->setBankCode($trx['description']['bank_code']);
|
||||
$transaction->setAccountNumber($trx['description']['account_number']);
|
||||
$transaction->setName($trx['description']['name']);
|
||||
$transaction->setBooked($trx['booked']);
|
||||
$transaction->setPN($trx['description']['primanoten_nr']);
|
||||
$transaction->setTextKeyAddition($trx['description']['text_key_addition']);
|
||||
$statementModel->addTransaction($transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
450
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/Transaction.php
vendored
Executable file
450
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/Transaction.php
vendored
Executable file
|
|
@ -0,0 +1,450 @@
|
|||
<?php
|
||||
/** @noinspection PhpUnused */
|
||||
|
||||
namespace Fhp\Model\StatementOfAccount;
|
||||
|
||||
class Transaction
|
||||
{
|
||||
public const CD_CREDIT = 'credit';
|
||||
public const CD_DEBIT = 'debit';
|
||||
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
protected $bookingDate;
|
||||
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
protected $valutaDate;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected $amount;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $creditDebit;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $isStorno;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $bookingCode;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $bookingText;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description1;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description2;
|
||||
|
||||
/**
|
||||
* Array keys are identifiers like "SVWZ" for the main description.
|
||||
* @var string[]
|
||||
*/
|
||||
protected $structuredDescription;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $bankCode;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $accountNumber;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $booked;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $pn;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $textKeyAddition;
|
||||
|
||||
/**
|
||||
* Get booking date.
|
||||
*
|
||||
* @deprecated Use getBookingDate() instead
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getDate(): ?\DateTime
|
||||
{
|
||||
return $this->getBookingDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get booking date
|
||||
*/
|
||||
public function getBookingDate(): ?\DateTime
|
||||
{
|
||||
return $this->bookingDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date
|
||||
*/
|
||||
public function getValutaDate(): ?\DateTime
|
||||
{
|
||||
return $this->valutaDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set booking date
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBookingDate(?\DateTime $date = null)
|
||||
{
|
||||
$this->bookingDate = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set valuta date
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setValutaDate(?\DateTime $date = null)
|
||||
{
|
||||
$this->valutaDate = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get amount
|
||||
*/
|
||||
public function getAmount(): float
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set booked status
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBooked(bool $booked)
|
||||
{
|
||||
$this->booked = $booked;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set amount
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setAmount(float $amount)
|
||||
{
|
||||
$this->amount = (float) $amount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get creditDebit
|
||||
*/
|
||||
public function getCreditDebit(): string
|
||||
{
|
||||
return $this->creditDebit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set creditDebit
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCreditDebit(string $creditDebit)
|
||||
{
|
||||
$this->creditDebit = $creditDebit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isStorno
|
||||
*/
|
||||
public function isStorno(): bool
|
||||
{
|
||||
return $this->isStorno;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isStorno
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIsStorno(bool $isStorno)
|
||||
{
|
||||
$this->isStorno = $isStorno;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookingCode
|
||||
*/
|
||||
public function getBookingCode(): string
|
||||
{
|
||||
return $this->bookingCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bookingCode
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBookingCode(string $bookingCode)
|
||||
{
|
||||
$this->bookingCode = (string) $bookingCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookingText
|
||||
*/
|
||||
public function getBookingText(): string
|
||||
{
|
||||
return $this->bookingText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bookingText
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBookingText(string $bookingText)
|
||||
{
|
||||
$this->bookingText = (string) $bookingText;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description1
|
||||
*/
|
||||
public function getDescription1(): string
|
||||
{
|
||||
return $this->description1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description1
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDescription1(string $description1)
|
||||
{
|
||||
$this->description1 = (string) $description1;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description2
|
||||
*/
|
||||
public function getDescription2(): string
|
||||
{
|
||||
return $this->description2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description2
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDescription2(string $description2)
|
||||
{
|
||||
$this->description2 = (string) $description2;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get structuredDescription
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getStructuredDescription(): array
|
||||
{
|
||||
return $this->structuredDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set structuredDescription
|
||||
*
|
||||
* @param string[] $structuredDescription
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setStructuredDescription(array $structuredDescription)
|
||||
{
|
||||
$this->structuredDescription = $structuredDescription;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main description (SVWZ)
|
||||
*/
|
||||
public function getMainDescription(): string
|
||||
{
|
||||
if (array_key_exists('SVWZ', $this->structuredDescription)) {
|
||||
return $this->structuredDescription['SVWZ'];
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the end to end id (EREF)
|
||||
*/
|
||||
public function getEndToEndID(): string
|
||||
{
|
||||
if (array_key_exists('EREF', $this->structuredDescription)) {
|
||||
return $this->structuredDescription['EREF'];
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bankCode
|
||||
*/
|
||||
public function getBankCode(): string
|
||||
{
|
||||
return $this->bankCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bankCode
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBankCode(string $bankCode)
|
||||
{
|
||||
$this->bankCode = (string) $bankCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accountNumber
|
||||
*/
|
||||
public function getAccountNumber(): string
|
||||
{
|
||||
return $this->accountNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set accountNumber
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setAccountNumber(string $accountNumber)
|
||||
{
|
||||
$this->accountNumber = (string) $accountNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get booked status
|
||||
*/
|
||||
public function getBooked(): bool
|
||||
{
|
||||
return $this->booked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = (string) $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get primanota number
|
||||
*/
|
||||
public function getPN(): int
|
||||
{
|
||||
return $this->pn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set primanota number
|
||||
*
|
||||
* @param int|mixed $nr Will be parsed to an int.
|
||||
* @return $this
|
||||
*/
|
||||
public function setPN($nr)
|
||||
{
|
||||
$this->pn = intval($nr);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text key addition
|
||||
*/
|
||||
public function getTextKeyAddition(): int
|
||||
{
|
||||
return $this->textKeyAddition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set text key addition
|
||||
*
|
||||
* @param int|mixed $textKeyAddition Will be parsed to an int.
|
||||
* @return $this
|
||||
*/
|
||||
public function setTextKeyAddition($textKeyAddition)
|
||||
{
|
||||
$this->textKeyAddition = intval($textKeyAddition);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
256
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfHoldings/Holding.php
vendored
Executable file
256
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfHoldings/Holding.php
vendored
Executable file
|
|
@ -0,0 +1,256 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model\StatementOfHoldings;
|
||||
|
||||
class Holding
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $isin;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $wkn;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
protected $acquisitionPrice;
|
||||
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
protected $price;
|
||||
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
protected $amount;
|
||||
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
protected $date;
|
||||
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
protected $time;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $currency;
|
||||
|
||||
/**
|
||||
* Set ISIN
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setISIN(?string $isin)
|
||||
{
|
||||
$this->isin = $isin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set WKN
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setWKN(?string $wkn)
|
||||
{
|
||||
$this->wkn = $wkn;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Name
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setName(?string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setValue(?float $value)
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set price
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPrice(?float $price)
|
||||
{
|
||||
$this->price = $price;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set acquisition price
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setAcquisitionPrice(?float $price)
|
||||
{
|
||||
$this->acquisitionPrice = $price;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set amount
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setAmount(?float $amount)
|
||||
{
|
||||
$this->amount = $amount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set currency
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCurrency(?string $currency)
|
||||
{
|
||||
$this->currency = $currency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set date
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDate(\DateTime $date)
|
||||
{
|
||||
$this->date = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set time
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setTime(\DateTime $time)
|
||||
{
|
||||
$this->time = $time;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISIN
|
||||
*/
|
||||
public function getISIN(): ?string
|
||||
{
|
||||
return $this->isin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WKN
|
||||
*/
|
||||
public function getWKN(): ?string
|
||||
{
|
||||
return $this->wkn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value
|
||||
*/
|
||||
public function getValue(): ?float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get acquisition price
|
||||
*/
|
||||
public function getAcquisitionPrice(): ?float
|
||||
{
|
||||
return $this->acquisitionPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price
|
||||
*/
|
||||
public function getPrice(): ?float
|
||||
{
|
||||
return $this->price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get amount
|
||||
*/
|
||||
public function getAmount(): ?float
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currency
|
||||
*/
|
||||
public function getCurrency(): ?string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time
|
||||
*/
|
||||
public function getTime(): ?\DateTime
|
||||
{
|
||||
return $this->time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date
|
||||
*/
|
||||
public function getDate(): ?\DateTime
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
}
|
||||
26
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfHoldings/StatementOfHoldings.php
vendored
Executable file
26
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfHoldings/StatementOfHoldings.php
vendored
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace Fhp\Model\StatementOfHoldings;
|
||||
|
||||
class StatementOfHoldings
|
||||
{
|
||||
/**
|
||||
* @var Holding[]
|
||||
*/
|
||||
protected $holdings = [];
|
||||
|
||||
/**
|
||||
* Get statements
|
||||
*
|
||||
* @return Holding[]
|
||||
*/
|
||||
public function getHoldings(): array
|
||||
{
|
||||
return $this->holdings;
|
||||
}
|
||||
|
||||
public function addHolding(Holding $holding)
|
||||
{
|
||||
$this->holdings[] = $holding;
|
||||
}
|
||||
}
|
||||
27
vendor/nemiah/php-fints/lib/Fhp/Model/TanMedium.php
vendored
Executable file
27
vendor/nemiah/php-fints/lib/Fhp/Model/TanMedium.php
vendored
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
/** @noinspection PhpUnused */
|
||||
|
||||
namespace Fhp\Model;
|
||||
|
||||
/**
|
||||
* For two-step authentication, users need to enter a TAN, which can be obtained in various ways. After choosing one of
|
||||
* these ways, i.e. choosing a a {@link TanMode} (SMS, TAN generator device, and so on), the user might have to choose
|
||||
* which of their TAN media they want to use within this mode, in case they have multiple. For instance, a user might
|
||||
* have multiple mobile phone numbers configured for smsTAN, might have multiple TAN generators, or multiple iTAN lists.
|
||||
* Each {@link TanMedium} instance describes one of these options.
|
||||
*/
|
||||
interface TanMedium
|
||||
{
|
||||
/**
|
||||
* @return string A user-readable name for this TAN medium, which serves as its identifier at the same time. This is
|
||||
* what the application needs to persist when it wants to remember the users decision for future transactions.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* @return string|null In case this is a mobileTAN/smsTAN medium, this is its (possibly obfuscated) phone number.
|
||||
*/
|
||||
public function getPhoneNumber(): ?string;
|
||||
|
||||
// TODO Consider making more information from TanMediumListeV4 available here.
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue