From 4b7046132cc20a2dee11ab07506cc3d719500134 Mon Sep 17 00:00:00 2001 From: data Date: Wed, 11 Mar 2026 09:33:47 +0100 Subject: [PATCH] feat: Initiales Produktverwaltung-Modul (v1.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kategorie-Baumansicht mit Inline-Editing fΓΌr Dolibarr. - Hierarchischer Kategoriebaum mit Auf-/Zuklappen - Inline-Editing: Ref, Label, Beschreibung per Doppelklick - Best EK mit 3-Zeichen Lieferanten-Badge - Marge-Berechnung mit Farbmarkierung - Kategorie-CRUD mit 20 Farb-Swatches - Produkte ohne Kategorie Sektion - Admin: Ref-Schema, Standard-Aufklapp-Verhalten - Export: CSV und PDF - Berechtigungen: read, write, delete, export Co-Authored-By: Claude Opus 4.6 --- .gitignore | 5 + COPYING | 621 ++++++++++++++++ ChangeLog.md | 5 + PRODUKT-UMBENENNUNG.md | 755 +++++++++++++++++++ README.md | 136 ++++ REFERENZ-SCHEMA.md | 346 +++++++++ admin/about.php | 118 +++ admin/setup.php | 171 +++++ ajax/product_actions.php | 524 ++++++++++++++ build/buildzip.php | 316 ++++++++ build/makepack-produktverwaltung.conf | 11 + core/modules/modProduktVerwaltung.class.php | 484 +++++++++++++ css/produktverwaltung.css | 490 +++++++++++++ export.php | 303 ++++++++ img/README.md | 14 + js/produktverwaltung.js | 759 ++++++++++++++++++++ langs/de_DE/produktverwaltung.lang | 85 +++ langs/en_US/produktverwaltung.lang | 84 +++ lib/produktverwaltung.lib.php | 85 +++ modulebuilder.txt | 3 + produktverwaltungindex.php | 575 +++++++++++++++ sql/dolibarr_allversions.sql | 3 + 22 files changed, 5893 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 ChangeLog.md create mode 100644 PRODUKT-UMBENENNUNG.md create mode 100644 README.md create mode 100644 REFERENZ-SCHEMA.md create mode 100644 admin/about.php create mode 100644 admin/setup.php create mode 100644 ajax/product_actions.php create mode 100644 build/buildzip.php create mode 100644 build/makepack-produktverwaltung.conf create mode 100644 core/modules/modProduktVerwaltung.class.php create mode 100644 css/produktverwaltung.css create mode 100644 export.php create mode 100644 img/README.md create mode 100644 js/produktverwaltung.js create mode 100644 langs/de_DE/produktverwaltung.lang create mode 100644 langs/en_US/produktverwaltung.lang create mode 100644 lib/produktverwaltung.lib.php create mode 100644 modulebuilder.txt create mode 100644 produktverwaltungindex.php create mode 100644 sql/dolibarr_allversions.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e85b72b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.claude/ +*.swp +*.swo +*~ +.DS_Store diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a0453 --- /dev/null +++ b/COPYING @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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 diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..13b5952 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,5 @@ +# CHANGELOG MODULE PRODUKTVERWALTUNG FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) + +## 1.0 + +Initial version diff --git a/PRODUKT-UMBENENNUNG.md b/PRODUKT-UMBENENNUNG.md new file mode 100644 index 0000000..cd0b9eb --- /dev/null +++ b/PRODUKT-UMBENENNUNG.md @@ -0,0 +1,755 @@ +# Produkt-Umbenennungsliste + +Stand: 2026-03-05 + +## Legende +- βœ… = Bereits korrekt oder nur kleine Anpassung +- πŸ”„ = Umbenennung erforderlich +- ⏸️ = Import-Produkt (spΓ€ter pflegen) +- ❌ = LΓΆschen (Test-Produkt) + +--- + +## Verteilung / Sicherungen + +### Leitungsschutzschalter +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 267 | `LSS-EAT-PXLB161-B16` | `LSS-EAT-B16-1P` | πŸ”„ | +| 268 | `LSS-EAT-PXLB163-B16-3P` | `LSS-EAT-B16-3P` | πŸ”„ | +| 304 | `LSS-EAT-PXLC163-C16-3P` | `LSS-EAT-C16-3P` | πŸ”„ | +| 280 | `LSS-EAT-PXLC203-C20-3P` | `LSS-EAT-C20-3P` | πŸ”„ | +| 264 | `LSS-HAG-MBS113-B13-6kA` | `LSS-HAG-B13-1P` | πŸ”„ | +| 132 | `LSS-HAG-MBS116-B16-6kA` | `LSS-HAG-B16-1P` | πŸ”„ | +| 131 | `LSS-HAG-MBS316-B16-3P-6kA` | `LSS-HAG-B16-3P` | πŸ”„ | + +### FI-Schutzschalter +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 279 | `FIS-EAT-PXF404003-40A-4P-6kA` | `FI-EAT-40A-4P-30` | πŸ”„ | +| 133 | `FIS-HAG-CDA44D-40A-4P-6kA` | `FI-HAG-40A-4P-30` | πŸ”„ | +| 265 | `FIS-HAG-CDA663D-63A-4P-30MA-6kA` | `FI-HAG-63A-4P-30` | πŸ”„ | + +### FI/LS-Kombischalter +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 134 | `FILS-HAG-ADS916D-B16-1P+N-30mA-6kA` | `FILS-HAG-B16-30` | πŸ”„ | +| 297 | `FLLS-HAG-ARC910D-B10-1P+N-6kA` | `AFDD-HAG-B10` | πŸ”„ | +| 98 | `VT-EAT-PXKFILS-B16-30MA` | `FILS-EAT-B16-30` | πŸ”„ | + +### SLS-Schalter +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 120 | `SLS-HAG-HTS340E-40A-E-3P` | `SLS-HAG-40A-3P` | πŸ”„ | +| 309 | `SLS-HAG-HTS350E-50A-E-3P` | `SLS-HAG-50A-3P` | πŸ”„ | +| 213 | `SLS-HAG-HTS363E-63A-E-3P` | `SLS-HAG-63A-3P` | πŸ”„ | + +### Hauptschalter +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 117 | `AUS-HAG-SBN363-63A-3P` | `HSC-HAG-63A-3P` | πŸ”„ | + +### Überspannungsableiter +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 15 | `UA-DEHN-2POL-TYP2` | `UAS-DEH-TYP2-2P` | πŸ”„ | +| 18 | `UA-VOSS-DUO-Z` | `UAS-VOS-DUO` | πŸ”„ | + +### Stromstoßschalter / Relais +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 64 | `VT-ELT-S12-100-230V-SSS` | `SSS-ELT-16A` | πŸ”„ | +| 65 | `REL-ELT-R12-100-230V-SSS` | `REL-ELT-16A-1S` | πŸ”„ | +| 310 | `REL-ELT-R12-110-230V-SSS` | `REL-ELT-16A-1S1O` | πŸ”„ | + +### Zeitschaltuhren +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 97 | `VT-HAG-UHR-EHN-111` | `ZSU-HAG-EHN111` | πŸ”„ | +| 60 | `VT-THEBEN-UHR-SUL-181d` | `ZSU-THE-SUL181D` | πŸ”„ | + +### Klingeltrafos +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 119 | `KLT-HAG-ST303N-8V-S` | `KLT-HAG-8V` | βœ… | + +### Taster / Leuchtmelder +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 17 | `ABB-WECHSELS-E213-16-001` | `TAS-ABB-WEC-16A` | πŸ”„ | +| 299 | `ASC-HAG-SBT116-16A` | `TAS-HAG-AUS-16A` | πŸ”„ | + +### Schmelzsicherungen +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 142 | `DIA-SIE-5SB4111-SIE-35A-DIII-gG` | `DIA-SIE-DIII-35A` | πŸ”„ | +| 141 | `DIA-SIE-5SB4211-SIE-50A-DIII-gG` | `DIA-SIE-DIII-50A` | πŸ”„ | +| 140 | `DIA-SIE-5SB4311-SIE-63A-DIII-gG` | `DIA-SIE-DIII-63A` | πŸ”„ | +| 144 | `DIA-SIE-5SH113-SCK-DII` | `DIA-SIE-DII-KAP` | πŸ”„ | +| 143 | `DIA-SIE-5SH113-SCK-DIII` | `DIA-SIE-DIII-KAP` | πŸ”„ | +| 33 | `NEO-SIE-KAP-D02` | `NEO-SIE-D02-KAP` | πŸ”„ | +| 34 | `NEO-SIE-SIC-35A` | `NEO-SIE-D02-35A` | πŸ”„ | +| 295 | `HFE-MER-NEO-D01-D02` | `NEO-MER-FEDER` | πŸ”„ | +| 293 | `PEI-MER-NEO-D02-20A` | `NEO-MER-D02-PE20` | πŸ”„ | +| 290 | `PEI-MER-NEO-D02-25A` | `NEO-MER-D02-PE25` | πŸ”„ | +| 291 | `PEI-MER-NEO-D02-35A` | `NEO-MER-D02-PE35` | πŸ”„ | +| 292 | `PEI-MER-NEO-D02-50A` | `NEO-MER-D02-PE50` | πŸ”„ | +| 135 | `SIS-MER-NEO-D02-3P` | `NEO-MER-D02-SOC-3P` | πŸ”„ | + +--- + +## Steckklemmen / Reihenklemmen + +### Wago Steckklemmen (Flex 221) +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 189 | `CFK-WAG-FLEX-221-412-2F` | `STK-WAG-FLX-2F-221` | πŸ”„ | +| 73 | `CFK-WAG-FLEX-221-413-3F` | `STK-WAG-FLX-3F-221` | πŸ”„ | +| 72 | `CFK-WAG-FLEX-221-415-5F` | `STK-WAG-FLX-5F-221` | πŸ”„ | +| 75 | `CFV-WAG-FLEX-221-2411-F` | `STK-WAG-FLX-1F-221` | πŸ”„ | + +### Wago Steckklemmen (Compact 2273) +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 94 | `COK-WAG-2273-202-2F` | `STK-WAG-STD-2F-2273` | πŸ”„ | +| 93 | `COK-WAG-2273-203-3F` | `STK-WAG-STD-3F-2273` | πŸ”„ | +| 95 | `COK-WAG-2273-204-4F` | `STK-WAG-STD-4F-2273` | πŸ”„ | +| 92 | `COK-WAG-2273-205-5F` | `STK-WAG-STD-5F-2273` | πŸ”„ | +| 74 | `COK-WAG-2273-208-8F` | `STK-WAG-STD-8F-2273` | πŸ”„ | + +### Wago TopJob S +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 167 | `ABS-WAG-TOPJOBS-2003-7692-ORANGE` | `RKL-WAG-S-ABS` | πŸ”„ | +| 252 | `EKL-WAG-TOPJOBS-249-117-TS35` | `RKL-WAG-S-EKL` | πŸ”„ | +| 169 | `IEK-WAG-TOPJOBS-2003-7641-NTLPE-4MM` | `RKL-WAG-S-NLPE-4MM` | πŸ”„ | +| 168 | `IEK-WAG-TOPJOBS-2003-7692-LL-4MM` | `RKL-WAG-S-LL-4MM` | πŸ”„ | +| 177 | `KAB-WAG-TOPJOBS-2002-402-2F` | `RKL-WAG-S-KBR-2F` | πŸ”„ | +| 178 | `KAB-WAG-TOPJOBS-2002-403-3F` | `RKL-WAG-S-KBR-3F` | πŸ”„ | +| 179 | `KAB-WAG-TOPJOBS-2002-405-5F` | `RKL-WAG-S-KBR-5F` | πŸ”„ | +| 170 | `SAS-WAG-TOPJOBS-210-133-10x3MM` | `RKL-WAG-S-SAS` | πŸ”„ | +| 175 | `SCB-WAG-TOPJOBS-2002-472-2F` | `RKL-WAG-S-SCB-2F` | πŸ”„ | +| 174 | `SCB-WAG-TOPJOBS-2002-473-3F` | `RKL-WAG-S-SCB-3F` | πŸ”„ | +| 173 | `SCB-WAG-TOPJOBS-2002-475-5F` | `RKL-WAG-S-SCB-5F` | πŸ”„ | +| 176 | `SCB-WAG-TOPJOBS-2002-477-7F` | `RKL-WAG-S-SCB-7F` | πŸ”„ | +| 195 | `SLK-WAG-TOBJOBS-2016-7607-16MM-GRGE` | `RKL-WAG-S-PE-16MM` | πŸ”„ | +| 253 | `SSA-WAG-TOPJOBS-777-303` | `RKL-WAG-S-SSA` | πŸ”„ | +| 172 | `SST-WAG-TOPJOBS-2009-305-TS35` | `RKL-WAG-S-SST` | πŸ”„ | +| 171 | `TRK-WAG-TOPJOBS-2016-7714-N` | `RKL-WAG-S-TRN-N` | πŸ”„ | +| 139 | `REK-WAG-821-122-TJS-HVS` | `RKL-WAG-S-SET` | πŸ”„ | + +### Wago TopJob SD +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 247 | `ABS-WAG-TOPJOBSD-2203-7692-ORANGE` | `RKL-WAG-SD-ABS` | πŸ”„ | +| 250 | `ABS-WAG-TOPJOBSD-2216-7792-ORANGE` | `RKL-WAG-SD-ABS-1MM` | πŸ”„ | +| 245 | `IEK-WAG-TOPJOBSD-2203-7641-NTLPE-4MM` | `RKL-WAG-SD-NLPE-4MM` | πŸ”„ | +| 246 | `IEK-WAG-TOPJOBSD-2203-7692-LL-4MM` | `RKL-WAG-SD-LL-4MM` | πŸ”„ | +| 316 | `SLK-WAG-TOBJOBSD-2216-7601-16MM-GRAU` | `RKL-WAG-SD-L-16MM` | πŸ”„ | +| 315 | `SLK-WAG-TOBJOBSD-2216-7604-16MM-BLAU` | `RKL-WAG-SD-N-16MM` | πŸ”„ | +| 248 | `SLK-WAG-TOBJOBSD-2216-7607-16MM-GRGE` | `RKL-WAG-SD-PE-16MM` | πŸ”„ | +| 249 | `TRK-WAG-TOPJOBSD-2216-7714-N` | `RKL-WAG-SD-TRN-N` | πŸ”„ | + +### Phasenschienen +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 124 | `BSA-HAG-KZN059` | `PHS-HAG-BSA` | πŸ”„ | +| 125 | `ENK-HAG-KZN023-2P3P` | `PHS-HAG-ENK` | πŸ”„ | +| 129 | `PAS-HAG-KDN363A-3P-10MM-12FA` | `PHS-HAG-3P-10MM-12F` | πŸ”„ | +| 130 | `PAS-HAG-KDN363F-3P+N-10MM-12FA` | `PHS-HAG-3PN-10MM-12F` | πŸ”„ | +| 128 | `PAS-HAG-KDN363M-3P-10MM-6FA` | `PHS-HAG-3P-10MM-6F` | πŸ”„ | +| 127 | `PAS-HAG-KDN363N-3P-10MM-9FA` | `PHS-HAG-3P-10MM-9F` | πŸ”„ | +| 269 | `PHS-EAT-EVG-3P-10MM-12FA` | `PHS-EAT-3P-10MM-12F` | πŸ”„ | +| 281 | `PHS-EAT-EVG-3P-10MM-9FA` | `PHS-EAT-3P-10MM-9F` | πŸ”„ | +| 270 | `PHS-EAT-EVG16-3P-16MM-12FA` | `PHS-EAT-3P-16MM-12F` | πŸ”„ | +| 126 | `PHS-HAG-KCF663L-3+N-2RE-LIN` | `PHS-HAG-3PN-2R-L` | πŸ”„ | + +### Abzweigklemmen +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 313 | `AKL-POL-A14S-25MM-12FA` | `AKL-POL-L-25MM-12F` | πŸ”„ | +| 306 | `AKL-POL-A14S-25MM-12FA-BL` | `AKL-POL-N-25MM-12F` | πŸ”„ | +| 307 | `AKL-POL-PE14S-25MM-12FA-GRUN` | `AKL-POL-PE-25MM-12F` | πŸ”„ | +| 32 | `HLAK-POL-HLAK_25-5_10-S` | `HLAK-POL-25MM` | πŸ”„ | + +### VerbindungsbrΓΌcken +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 122 | `KAB-HAG-K67C-10q-110` | `VEB-HAG-10MM-110` | πŸ”„ | +| 121 | `KAB-HAG-K67M-10q-250` | `VEB-HAG-10MM-250` | πŸ”„ | +| 123 | `VEB-HAG-KC325-3P` | `VEB-HAG-3P` | βœ… | + +--- + +## Kabel / Leitungen + +### NYM-J Mantelleitung +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 14 | `KAB-NYM-J-1x16` | `NYM-1x16` | πŸ”„ | +| 45 | `KAB-NYM-J-3x1_5` | `NYM-3x1_5` | πŸ”„ | +| 283 | `KAB-NYM-J-3x2_5` | `NYM-3x2_5` | πŸ”„ | +| 46 | `KAB-NYM-J-5x1_5` | `NYM-5x1_5` | πŸ”„ | +| 285 | `KAB-NYM-J-5x10` | `NYM-5x10` | πŸ”„ | +| 151 | `KAB-NYM-J-5x10-SCHNITTWARE` | `NYM-5x10-SCH` | πŸ”„ | +| 284 | `KAB-NYM-J-5x2_5` | `NYM-5x2_5` | πŸ”„ | +| 244 | `KAB-NYM-J-5x6` | `NYM-5x6` | πŸ”„ | +| 44 | `KAB-NYY-J-5x1x5` | `NYY-5x1_5` | πŸ”„ | +| 13 | `KAB-NYY-J-3x1x5` | `NYY-3x1_5` | πŸ”„ | + +### H07V-K Aderleitung (ohne Kategorie!) +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 31 | `KAB-H07V-K-10MM-GN-F` | `H07VK-10MM-GN` | πŸ”„ | +| 41 | `KAB-H07V-K-10MM-GN-S` | `H07VU-10MM-GN` | πŸ”„ | +| 30 | `KAB-H07V-K-10MM-HBL-F` | `H07VK-10MM-BL` | πŸ”„ | +| 42 | `KAB-H07V-K-10MM-HBL-S` | `H07VU-10MM-BL` | πŸ”„ | +| 29 | `KAB-H07V-K-10MM-SW-F` | `H07VK-10MM-SW` | πŸ”„ | +| 43 | `KAB-H07V-K-10MM-SW-S` | `H07VU-10MM-SW` | πŸ”„ | +| 71 | `KAB-H07V-K-2_5MM-SW-F` | `H07VK-2_5MM-SW` | πŸ”„ | + +### PVC-Schlauchleitung +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 157 | `SCL-H05VVF-4x0_75-WEISS` | `H05VV-4x0_75-WS` | πŸ”„ | + +### Steuerleitung +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 302 | `KAB-YSLYJZ-7x1_5-STEU` | `STEU-YSLY-7x1_5` | πŸ”„ | + +### Fernmeldekabel +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 277 | `KAB-DIV-JE-Y(ST)Y-2x2x0_8` | `FERN-2x2x0_8` | πŸ”„ | +| 271 | `KAB-DIV-JE-Y(ST)Y-4x2x0_8` | `FERN-4x2x0_8` | πŸ”„ | +| 197 | `KAB-DIV-JY(ST)Y-4x2x0_6` | `FERN-4x2x0_6` | πŸ”„ | + +### Datenkabel +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 263 | `KAB-ELT-NET-WORKS-1000SX-GEL` | `DAT-ELT-1000SX-GEL` | πŸ”„ | +| 278 | `KAB-KAT-CAT7-SFTP-CCA` | `DAT-KAT-CAT7-SFTP` | πŸ”„ | + +--- + +## Installationsmaterial + +### GerΓ€tedosen +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 154 | `GED-KAI-105504-46MM` | `GED-KAI-46MM` | πŸ”„ | +| 153 | `GED-KAI-155504-66MM` | `GED-KAI-66MM` | πŸ”„ | +| 347 | `HOW-KAI-9063-02` | `HOW-KAI-FLACH` | πŸ”„ | +| 234 | `ADD-KAI-118490` | `GED-KAI-DECK-UNI` | πŸ”„ | +| 232 | `FED-KAI-72MM` | `GED-KAI-FED-72MM` | πŸ”„ | +| 233 | `FED-KAI-85MM` | `GED-KAI-FED-85MM` | πŸ”„ | +| 378 | `PAR-KAI-D60-12MM` | `GED-KAI-PAR-12MM` | πŸ”„ | +| 377 | `PAR-KAI-D60-24MM` | `GED-KAI-PAR-24MM` | πŸ”„ | + +### AbzweigkΓ€sten +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 25 | `ABZ-HEN-9320-001` | `ABZ-HEN-9320` | πŸ”„ | +| 242 | `ABZ-HEN-9340-6MM` | `ABZ-HEN-9340` | πŸ”„ | +| 69 | `INM-SPE-M25-LEER` | `ABZ-SPE-M25` | πŸ”„ | +| 70 | `INM-SPE-SD7L-LEER` | `ABZ-SPE-SD7L` | πŸ”„ | + +### Kabelkanal +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 12 | `KAK-OBO-WDK15015RW-15x15-Weiß-PVC` | `KAK-OBO-15x15-WS` | πŸ”„ | +| 9 | `KAK-OBO-WDK15030RW-15x30-WEISS-PVC` | `KAK-OBO-15x30-WS` | πŸ”„ | +| 59 | `KAK-OBO-WDK15030BR-15x30-BRAUN-PVC` | `KAK-OBO-15x30-BR` | πŸ”„ | +| 321 | `KAK-OBO-WDK15030GR-15x30-WEISS-PVC` | `KAK-OBO-15x30-GR` | πŸ”„ | +| 156 | `KAK-OBO-WDK20020RW-20x20-Weiß-PVC` | `KAK-OBO-20x20-WS` | πŸ”„ | +| 335 | `KAK-OBO-WDK40040RW-40x40-Weiß-PVC` | `KAK-OBO-40x40-WS` | πŸ”„ | +| 336 | `KAK-OBO-WDK40060RW-40x60-Weiß-PVC` | `KAK-OBO-40x60-WS` | πŸ”„ | +| 136 | `KAK-OBO-WDK60110RW-60x110-WEISS-PVC` | `KAK-OBO-60x110-WS` | πŸ”„ | +| 314 | `KAK-OBO-WDK80170LGR-60x110-LGRAU-PVC` | `KAK-OBO-80x170-GR` | πŸ”„ | +| 319 | `KAK-HAG-LF6011009016-60x110-WEISS-PVC` | `KAK-HAG-60x110-WS` | πŸ”„ | +| 138 | `FWH-OBO-HF60110RW-60x110-WEISS-PVC` | `KKW-OBO-60x110-FW` | πŸ”„ | +| 20 | `KKE-OBO-HE15030RW-15x30-END-WEISS-PVC` | `KKE-OBO-15x30` | πŸ”„ | +| 137 | `KKE-OBO-HE60110RW-60x110-END-WEISS-PVC` | `KKE-OBO-60x110` | πŸ”„ | +| 320 | `KKE-HAG-LF601106-60x110-END-WEISS-PVC` | `KKE-HAG-60x110` | πŸ”„ | +| 365 | `KAK-OBO-GRIP10` | `KKL-OBO-GRIP10` | πŸ”„ | + +### Kabeltrassen +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 286 | `KRI-OBO-RKSM610FS-60x100` | `KTR-OBO-RIN-60x100` | πŸ”„ | +| 287 | `FRS-OBO-FRSB-6x12` | `KTR-OBO-SCR-6x12` | πŸ”„ | +| 288 | `WAL-OBO-21` | `KTR-OBO-WAL-21` | πŸ”„ | +| 289 | `WAL-OBO-31` | `KTR-OBO-WAL-31` | πŸ”„ | + +### PG-Verschraubung +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 49 | `PGV-OBO-VTE-16` | `PGV-OBO-PG16` | πŸ”„ | +| 79 | `PGV-OBO-VTE-16-MUT` | `PGV-OBO-PG16-MUT` | πŸ”„ | +| 36 | `PGV-OBO-VTE-21` | `PGV-OBO-PG21` | πŸ”„ | +| 37 | `PGV-OBO-VTE-36` | `PGV-OBO-PG36` | πŸ”„ | + +### Kabelbefestigung +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 67 | `NAS-OBO-10x14-30MM` | `NAS-OBO-10x14` | πŸ”„ | +| 222 | `NAS-OBO-7x12-25mm` | `NAS-OBO-7x12-25` | πŸ”„ | +| 66 | `NAS-OBO-7x12-35mm` | `NAS-OBO-7x12-35` | πŸ”„ | +| 152 | `NAF-OBO-3_5x50` | `NAF-OBO-3_5x50` | βœ… | +| 80 | `KB-CEL-7_5-320-SW` | `KBI-CEL-320x7_5` | πŸ”„ | + +### AderendhΓΌlsen / Kabelschuhe +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 35 | `ADE-KLA-10MM-12MM` | `ADE-KLA-10MM-12` | βœ… | +| 78 | `ADE-KLA-2_5MM-12MM` | `ADE-KLA-2_5MM-12` | βœ… | +| 188 | `ADE-KLA-2_5MM-8MM` | `ADE-KLA-2_5MM-8` | βœ… | +| 254 | `ADE-KLA-2X10MM-12MM` | `ADE-KLA-2x10MM-12` | πŸ”„ | +| 305 | `ADE-KLA-2X2_5MM-10MM` | `ADE-KLA-2x2_5MM-10` | πŸ”„ | +| 187 | `ADE-KLA-47318-2_5MM-18MM` | `ADE-KLA-2_5MM-18` | πŸ”„ | +| 184 | `ADE-KLA-47618-10MM-18MM` | `ADE-KLA-10MM-18` | πŸ”„ | +| 352 | `ADE-KLA-6MM-12MM` | `ADE-KLA-6MM-12` | βœ… | +| 193 | `RKS-KLA-16QMM-M12` | `RKS-KLA-16MM-M12` | πŸ”„ | +| 255 | `STV-KLA-21R-6MM` | `STV-KLA-6MM` | πŸ”„ | +| 380 | `STV-KLA-22R-10MM` | `STV-KLA-10MM` | πŸ”„ | +| 190 | `STV-KLA-SV1525-1_5MM-M` | `STV-KLA-1_5MM` | πŸ”„ | + +### Schrumpfschlauch / Muffen +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 99 | `SCS-CEL-3_2MM-SW` | `SCS-CEL-3_2MM-SW` | βœ… | +| 100 | `SCS-CEL-4_8MM-SW` | `SCS-CEL-4_8MM-SW` | βœ… | +| 256 | `SCS-CEL-6_4MM-SW` | `SCS-CEL-6_4MM-SW` | βœ… | +| 257 | `SCS-CEL-12_7MM-SW` | `SCS-CEL-12_7MM-SW` | βœ… | +| 258 | `SCS-CEL-19_1MM-SW` | `SCS-CEL-19_1MM-SW` | βœ… | +| 47 | `MUF-CEL-Y00-GIE` | `MUF-CEL-Y00` | πŸ”„ | + +### LΓΌsterklemmen +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 101 | `KLM-OBO-LUS-4mm` | `LKL-OBO-4MM` | πŸ”„ | +| 102 | `KLM-OBO-LUS-10mm` | `LKL-OBO-10MM` | πŸ”„ | +| 103 | `KLM-OBO-LUS-16mm` | `LKL-OBO-16MM` | πŸ”„ | + +### DΓΌbel / Schrauben +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 19 | `DU-FIS-6x30mm` | `DUE-FIS-6x30` | πŸ”„ | +| 27 | `DU-BAR-SPR-6x30-001` | `DUE-BAR-6x30` | πŸ”„ | +| 26 | `SCHR-SPA-A2J-4_5x50-001` | `SCR-SPA-4_5x50` | πŸ”„ | +| 28 | `SCHR-DRE-SPAN-4_5x70-001` | `SCR-DRE-4_5x70` | πŸ”„ | +| 21 | `Schr-Torx-Senk-4_5x50` | `SCR-GEN-TORX-4_5x50` | πŸ”„ | + +### Erdungsmaterial +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 149 | `ANK-DEH-AK810AQ-V4A` | `ERD-DEH-ANK-V4A` | πŸ”„ | +| 145 | `POS-POL-POT1625-2REI` | `ERD-POL-PAS-16x25` | πŸ”„ | +| 194 | `RUD-DEH-RD10-10MM-V4A` | `ERD-DEH-RD-10MM` | πŸ”„ | +| 148 | `RUD-DEH-RD8-D8-NIRO` | `ERD-DEH-RD-8MM` | πŸ”„ | +| 147 | `SCS-DEH-620001-D20-NIRO` | `ERD-DEH-SCS-D20` | πŸ”„ | +| 146 | `TIE-DEH-TE201500AZV4A-D20-L1500-NIRO` | `ERD-DEH-TIE-1500` | πŸ”„ | +| 150 | `WDF-DEH-EWDMVK810M10-300x500MM-NIRO` | `ERD-DEH-WDF` | πŸ”„ | + +--- + +## Schalterprogramme + +### Busch-Jaeger Balance SI (Serie 914) +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 6 | `RAH-BJ-914-1FA` | `RAH-BJ-1F-914` | πŸ”„ | +| 158 | `RAH-BJ-914-2F` | `RAH-BJ-2F-914` | πŸ”„ | +| 218 | `RAH-BJ-914-3FA` | `RAH-BJ-3F-914` | πŸ”„ | +| 163 | `RAH-BJ-914-4F` | `RAH-BJ-4F-914` | πŸ”„ | +| 8 | `BJ-914-AP-RAH-1F` | `APR-BJ-1F-914` | πŸ”„ | +| 217 | `BJ-914-AP-RAH-3F` | `APR-BJ-3F-914` | πŸ”„ | +| 366 | `APR-BJ-1702-914-2F` | `APR-BJ-2F-914` | πŸ”„ | +| 7 | `STD-BJ-914-EUC` | `STD-BJ-EUC-914` | πŸ”„ | +| 164 | `WIP-BJ-914-2506` | `WIP-BJ-STD-914` | πŸ”„ | +| 165 | `WIP-BJ-914-1789` | `WIP-BJ-KTR-914` | πŸ”„ | +| 166 | `WIP-BJ-914-2504` | `WIP-BJ-JAL-914` | πŸ”„ | +| 346 | `BZS-BJ-2527-914-BALANCE` | `BZS-BJ-LAU-914` | πŸ”„ | + +### Busch-Jaeger Reflex SI (Serie 214) +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 348 | `RAH-BJ-214-3FA` | `RAH-BJ-3F-214` | πŸ”„ | +| 323 | `BZS-BJ-2538-214-REFLEX` | `BZS-BJ-214` | πŸ”„ | +| 324 | `TAE-BJ-2539-214-Reflex` | `TAE-BJ-214` | πŸ”„ | +| 325 | `UAE-BJ-1803-02-214-2F` | `UAE-BJ-2F-214` | πŸ”„ | +| 326 | `UAE-BJ-1803-214-Reflex-1F` | `UAE-BJ-1F-214` | πŸ”„ | + +### Busch-Jaeger UP-EinsΓ€tze +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 159 | `WSE-BJ-2000_6US-WECHSEL-10A` | `WSE-BJ-WEC` | πŸ”„ | +| 160 | `KSE-BJ-2000_7-KREUZ-10A` | `WSE-BJ-KRZ` | πŸ”„ | +| 161 | `WSE-BJ-2000_5US-SERIE-10A` | `WSE-BJ-SER` | πŸ”„ | +| 162 | `WST-BJ-2020US206-WTaster-10A` | `WSE-BJ-WTA` | πŸ”„ | + +### Presto-Vedder +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 51 | `STA-VED-ST-1FA-AP` | `STD-VED-AP-1F` | πŸ”„ | +| 50 | `STA-VED-ST-2FA-W-AP` | `STD-VED-AP-2F` | πŸ”„ | +| 52 | `WEA-VED-SA-WEC-AP` | `WEC-VED-AP` | πŸ”„ | +| 311 | `WEA-VED-PRE-9396_6-AP` | `WEC-VED-AP-KTR` | πŸ”„ | +| 243 | `WEC-VED-SA-AP-2F-SENK` | `WEC-VED-AP-2F-S` | πŸ”„ | +| 227 | `WST-VED-SA-AP-2F-SENK` | `KOM-VED-AP-2F-S` | πŸ”„ | + +--- + +## Leuchtmittel / Leuchten + +### LED E27 +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 104 | `LEU-PHI-Bulb-MATT-E27-927-5_9W` | `LED-PHI-E27-6W` | πŸ”„ | +| 231 | `LEU-PHI-Bulb-MATT-E27-927-7_8W` | `LED-PHI-E27-8W` | πŸ”„ | +| 262 | `LEU-PHI-Bulb-MATT-E27-927-11_2W` | `LED-PHI-E27-11W` | πŸ”„ | +| 376 | `BAF-BACH-E27` | `BAF-BAC-E27` | πŸ”„ | + +### LED E14 +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 106 | `LEU-PHI-CAND-E14-927-3_4W-CLG` | `LED-PHI-E14-3_4W` | πŸ”„ | +| 322 | `LEU-PHI-MASLEDLUST-E14-927-2_5W-CLG` | `LED-PHI-E14-2_5W` | πŸ”„ | + +### LED GU10 / G9 +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 105 | `LEU-PHI-Spot-GU10-927-3_7W-60_` | `LED-PHI-GU10-3_7W` | πŸ”„ | +| 266 | `LEU-SLV-GU9-927-3_6W` | `LED-SLV-G9-3_6W` | πŸ”„ | +| 185 | `HVF-BRUM-GU10-14CM` | `FAS-BRU-GU10-14CM` | πŸ”„ | +| 186 | `HVF-HOU-GU10-M3-LOCHER` | `FAS-HOU-GU10-M3` | πŸ”„ | + +### Halogen R7s +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 259 | `HST-SHA-R7S-118MM-200W-2000LM` | `HAL-SHA-R7S-200W` | πŸ”„ | + +### T8 RΓΆhren +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 62 | `T8-1500-PHI-LED-CorePro-840-KVG_VVG-27866000` | `T8-PHI-1500-840` | πŸ”„ | +| 39 | `T8-PHI-LED-MAS-830-KVG_VVG-1500-31662100` | `T8-PHI-1500-830` | πŸ”„ | +| 63 | `T8-PHI-LED-MAS-865-KVG_VVG-1500-31690400` | `T8-PHI-1500-865` | πŸ”„ | + +### Leuchten +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 38 | `FWL-PHI-WT050C-1500-1` | `DEC-PHI-WT050C-1X` | πŸ”„ | +| 11 | `FWL-PHI-WT050C-1500-2` | `DEC-PHI-WT050C-2X` | πŸ”„ | +| 228 | `WAL-SLV-GRAFIT-E27-ANTHRAZIT` | `WAN-SLV-GRAFT-ANT` | πŸ”„ | +| 219 | `AUS-PHI-BVP167-70W` | `STR-PHI-70W` | πŸ”„ | +| 241 | `LEU-NIE-65025-E27-1F` | `DEC-NIE-E27` | πŸ”„ | + +### Sonderposten +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 111 | `LEU-SON` | `SON-LEU` | πŸ”„ | +| 112 | `LAM-SON` | `SON-LAM` | πŸ”„ | +| 110 | `ERS-SON` | `SON-ERS` | πŸ”„ | +| 229 | `MAT-SON` | `SON-MAT` | πŸ”„ | +| 363 | `ANK-SON` | `SON-ANK` | πŸ”„ | + +--- + +## IT / Netzwerk + +### Patchkabel +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 274 | `PAK-DIG-0_25M-SW` | `PAK-DIG-025M-6A` | πŸ”„ | +| 203 | `PAK-DIG-0_25M-SW-IEC` | `PAK-DIG-025M-6A-IEC` | πŸ”„ | +| 275 | `PAK-DIG-0_5M-SW` | `PAK-DIG-050M-6A` | πŸ”„ | +| 204 | `PAK-DIG-0_5M-SW-IEC` | `PAK-DIG-050M-6A-IEC` | πŸ”„ | +| 276 | `PAK-DIG-1M-SW` | `PAK-DIG-1M-6A` | πŸ”„ | +| 273 | `PAK-DIG-2M-SW` | `PAK-DIG-2M-6A` | πŸ”„ | +| 272 | `PAK-DIG-5M-SW` | `PAK-DIG-5M-6A` | πŸ”„ | +| 192 | `PAK-EFB-EC6000A-15M` | `PAK-EFB-15M-6A` | πŸ”„ | + +### Netzwerkschrank +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 199 | `NES-DIG-UNS-600X600-19Z-SW` | `NES-DIG-600x600-19Z` | πŸ”„ | +| 200 | `FAB-DIG-19Z-1HE-SW` | `FAB-DIG-1HE` | πŸ”„ | +| 201 | `PPM-DIG-19Z-24F-1HE-SW` | `PPM-DIG-24F-1HE` | πŸ”„ | +| 207 | `STL-DIG-19Z-8F-ALU` | `STL-DIG-8F` | πŸ”„ | + +### Verbinder / Module +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 202 | `KEM-DIG-CAT6` | `KSM-DIG-6A` | πŸ”„ | +| 205 | `KAV-MET-CAT7-METALL` | `KAV-MET-CAT7` | πŸ”„ | +| 206 | `KAV-TEL-VM88-CAT7A` | `KAV-TEL-CAT7A` | πŸ”„ | + +### Netzwerkdosen +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 327 | `NWD-RUT-UP-1F` | `NWD-RUT-UP-1F` | βœ… | +| 328 | `NWDRUT-UP-2F` | `NWD-RUT-UP-2F` | πŸ”„ | +| 329 | `UAE-RUT-UP-2F` | `UAE-RUT-UP-2F` | βœ… | +| 196 | `AND-RUT-NFF` | `NWD-RUT-AP` | πŸ”„ | + +### Switch / Repeater +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 337 | `SWI-DLI-DGS108E-8P-SW` | `SWI-DLI-8P` | πŸ”„ | +| 343 | `REP-FRI-1200AX` | `REP-FRI-1200AX` | βœ… | +| 10 | `FB-REPEATER-2400` | `REP-FRI-2400` | πŸ”„ | + +### NAS / HDD +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 237 | `NAS-SYN-DS725+-SW-2FA` | `NAS-SYN-DS725+` | πŸ”„ | +| 239 | `NAS-SYN-DS225+-SW-2FA` | `NAS-SYN-DS225+` | πŸ”„ | +| 238 | `HDD-SYN-HAT3300-4TB-3_5` | `HDD-SYN-4TB` | πŸ”„ | + +### Powerline +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 226 | `PLA-FRI-1260E-SET` | `PLA-FRI-1260E` | πŸ”„ | + +--- + +## ZΓ€hlerschrΓ€nke + +### ZΓ€hlerschrΓ€nke +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 113 | `ZAS-HAG-ZB533M28C` | `ZAS-HAG-SET` | πŸ”„ | +| 308 | `ZAS-HAG-ZB52S-LEER-2F` | `ZAS-HAG-2F` | πŸ”„ | +| 208 | `ZAS-HAG-ZB54S-LEER-4F` | `ZAS-HAG-4F` | πŸ”„ | + +### Felder +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 209 | `ZAF-HAG-ZH53X3-3PU-2F-1350MM` | `ZAF-HAG-3PU-2F` | πŸ”„ | +| 210 | `ZAF-HAG-ZH53X1-3PUTSG-1F-1350MM` | `ZAF-HAG-3PU-TSG-1F` | πŸ”„ | +| 211 | `VEF-HAG-ZU59VT7APZ2-1350MM` | `VEF-HAG-APZ-1350` | πŸ”„ | +| 212 | `VEF-HAG-ZU59KS-1350MM` | `VEF-HAG-1350` | πŸ”„ | + +### Kabelrangierkanal +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 116 | `KRK-HAG-FZ443N-B800` | `KRK-HAG-B800` | πŸ”„ | +| 214 | `KRK-HAG-FZ444N-B1050` | `KRK-HAG-B1050` | πŸ”„ | + +### Kombiableiter +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 118 | `KOA-HAG-SPA701Z-4P` | `KOM-HAG-4P` | πŸ”„ | + +### ZubehΓΆr +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 215 | `ESA-HAG-K96ESA5-25MM-5P` | `ESA-HAG-25MM-5P` | πŸ”„ | +| 216 | `SSV-HAG-ZZ55SAVE-5P` | `SSV-HAG-5P` | πŸ”„ | +| 181 | `ZVH-HAG-T95L` | `ZUB-HAG-ZVH` | πŸ”„ | +| 182 | `PLT-HAG-FZ794-A4-KLAR` | `ZUB-HAG-PLT-A4` | πŸ”„ | +| 183 | `ADS-HAG-S35S-WEISS-219MM` | `ZUB-HAG-ABS` | πŸ”„ | +| 300 | `TSG-EAT-ZSD-31` | `TSG-EAT-ZSD31` | πŸ”„ | +| 301 | `VLE-EAT-ZSD-APZ-RFZ` | `ZUB-EAT-VLE` | πŸ”„ | +| 303 | `RJ45-EAT-ZSD-ISO-CAT6` | `ZUB-EAT-RJ45-6` | πŸ”„ | +| 282 | `MUT-ENO-NET-KM06` | `ZUB-ENO-MUT` | πŸ”„ | +| 381 | `SCT-HAG-VA48T-VOLTA-4F` | `SCT-HAG-4F` | πŸ”„ | + +### Unterverteiler +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 312 | `UV-HAG-VA48CN-AP-4F` | `UV-HAG-AP-4F` | πŸ”„ | +| 296 | `UV-HAG-VE412DN-AP-4F-IP65` | `UV-HAG-AP-4F-IP65` | πŸ”„ | +| 364 | `STE-HAG-SNS016-16A` | `STD-HAG-HS-16A` | πŸ”„ | +| 16 | `AG-HENSEL-KV9106-6PLE-IP65` | `UV-HEN-6PLE-IP65` | πŸ”„ | + +--- + +## HIFI / Audio + +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 349 | `LAU-MON-ESP315-WS` | `LAU-MON-ESP315` | πŸ”„ | +| 350 | `LAU-MON-EDL62HQ-WS` | `LAU-MON-EDL62HQ` | πŸ”„ | +| 367 | `DLS-BOS-FS2C-WEISS` | `LAU-BOS-FS2C` | πŸ”„ | +| 369 | `WLS-BOS-FS2SE-WEISS` | `LAU-BOS-FS2SE-SET` | πŸ”„ | +| 368 | `VES-BOS-VE2160BH-ELA` | `VES-BOS-VE2160-ELA` | πŸ”„ | +| 370 | `VES-BOS-VE2160BL` | `VES-BOS-VE2160` | πŸ”„ | +| 382 | `CCS-BOS-CC2-DEU` | `VEZ-BOS-CC2` | πŸ”„ | +| 383 | `RCD-TEC-DIGITRADIO143-CD` | `RCD-TEC-143CD` | πŸ”„ | + +--- + +## TV + +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 355 | `WAH-VOG-TVM3415-TILT-MED` | `WAH-VOG-TILT-MED` | πŸ”„ | + +--- + +## Sonstiges + +### Batterien +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 24 | `BAT-VAT-CR2430-V` | `BAT-VAR-CR2430` | πŸ”„ | +| 338 | `BAT-VAR-9V-BLI-BLO` | `BAT-VAR-9V` | πŸ”„ | +| 339 | `BAT-VAR-1_5-BABY` | `BAT-VAR-BABY` | πŸ”„ | + +### Bewegungsmelder +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 225 | `BWM-BJ-BWP-685122135-220BT-ANT` | `BWM-BJ-220BT-ANT` | πŸ”„ | +| 180 | `BWM-BJ-BWP-68519135-90BT-ANT` | `BWM-BJ-90BT-ANT` | πŸ”„ | + +### Rauchmelder +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 155 | `RWM-HAG-TG600AL-WEISS` | `RWM-HAG-STD` | πŸ”„ | + +### Klingelgong +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 298 | `KGO-GRO-ZWE-230V` | `KGO-GRO-230V` | πŸ”„ | + +### Wallbox +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 260 | `WBO-KOS-ENE-11KW` | `WBX-KOS-11KW` | πŸ”„ | + +### Anschlusskabel +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 331 | `EUR-BAC-VER-2M-SW` | `ANK-BAC-EUR-2M` | πŸ”„ | +| 332 | `EUR-BAC-VER-3M-SW` | `ANK-BAC-EUR-3M` | πŸ”„ | +| 330 | `EUR-BAC-VER-5M-SW` | `ANK-BAC-EUR-5M` | πŸ”„ | +| 333 | `HDMI-RUT-3M-SW` | `HDMI-RUT-3M` | πŸ”„ | +| 334 | `HDMI-RUT-5M-SW` | `HDMI-RUT-5M` | πŸ”„ | +| 342 | `HDMI-SCONN-2M-SW` | `HDMI-SCO-2M` | πŸ”„ | +| 345 | `HDMI-SCONN-3M-SW` | `HDMI-SCO-3M` | πŸ”„ | +| 344 | `HDMI-SCONN-5M-SW` | `HDMI-SCO-5M` | πŸ”„ | +| 341 | `NETZ-HAMA-UNI-1500mA` | `ANK-HAM-UNI-1500mA` | πŸ”„ | + +### Trafos +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 220 | `TRA-EGS-N1hKSW3-12V-18W-1_5A` | `TRA-EGS-12V-18W` | πŸ”„ | +| 223 | `TRA-ELT-WNT61-12VDC10W` | `TRA-ELT-12V-10W` | πŸ”„ | +| 224 | `TRA-ELT-WNT61-24VDC10W` | `TRA-ELT-24V-10W` | πŸ”„ | + +### Kleinspeicher +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 240 | `KSU-BOS-TR1500-5L` | `KSU-BOS-5L` | πŸ”„ | + +### Werkzeug +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 107 | `WER-MAK-AKK-18V-6AH` | `WER-MAK-AKK-18V-6AH` | βœ… | +| 109 | `WER-MAK-AKK-18V-6AH-SET` | `WER-MAK-AKK-18V-SET` | πŸ”„ | +| 108 | `WER-MAK-AKK-18V-LAD` | `WER-MAK-LAD-18V` | πŸ”„ | +| 294 | `MER-PES` | `WER-MER-PES` | πŸ”„ | + +### Kamera +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 3 | `KAM-REO-RLC843WA` | `KAM-REO-843WA` | πŸ”„ | +| 4 | `KAM-REO-JUNCTIONBOXD20` | `KAM-REO-JBOX` | πŸ”„ | +| 61 | `NET-REO-SWI-RLAPSI-8F` | `SWI-REO-8P-POE` | πŸ”„ | + +### Sonstiges ohne Kategorie +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 5 | `SDCARD-64GB` | `SD-SAN-64GB` | πŸ”„ | +| 2 | `KFZ-Stecker-13Pol` | `KFZ-HEL-13P` | πŸ”„ | +| 48 | `FB-TRU-IND-4KA-FUN` | `FER-TRU-4KA` | πŸ”„ | +| 68 | `BEL-EBE-DA-565-08` | `DAM-EBE-565` | πŸ”„ | +| 40 | `VER` | `VER` | βœ… | +| 96 | `FUH-SPR` | `KON-FUH-SPR` | πŸ”„ | +| 115 | `MAV-ANR` | `ARB-MAV-ANR` | πŸ”„ | + +### Lightpro Gartenbeleuchtung (ohne Kategorie) +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 54 | `BEL-LPR-12V-TRA-24W` | `TRA-LPR-12V-24W` | πŸ”„ | +| 53 | `BEL-LPR-12V-VER-3M` | `ANK-LPR-VER-3M` | πŸ”„ | +| 55 | `BEL-LPR-12V-ONY-60RS` | `LEU-LPR-ONY60` | πŸ”„ | +| 56 | `BEL-LPR-12V-CON-F` | `ZUB-LPR-CON-F` | πŸ”„ | +| 58 | `BEL-LPR-12V-CON-M` | `ZUB-LPR-CON-M` | πŸ”„ | +| 57 | `BEL-LPR-12V-CAB-CAP` | `ZUB-LPR-END` | πŸ”„ | + +### Eaton M22 (ohne Kategorie) +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 84 | `SSI-EAT-M22-KNG-2ST-WRLK-W` | `TAS-EAT-M22-KNG2` | πŸ”„ | +| 83 | `SSI-EAT-M22-KNG-3ST-WRK3` | `TAS-EAT-M22-KNG3` | πŸ”„ | +| 85 | `SSI-EAT-M22-DS` | `TAS-EAT-M22-DRK` | πŸ”„ | +| 86 | `SSI-EAT-M22-DDL-S-X7_X7-T` | `TAS-EAT-M22-DDL-PF` | πŸ”„ | +| 87 | `SSI-EAT-M22-DDL-GR-X1_X0-T` | `TAS-EAT-M22-DDL-AO` | πŸ”„ | +| 88 | `SSI-EAT-M22-FRB` | `TAS-EAT-M22-BEF` | πŸ”„ | +| 89 | `SSI-EAT-M22-LED-WEISS` | `TAS-EAT-M22-LED-WS` | πŸ”„ | +| 90 | `SSI-EAT-M22-1O-CK01` | `TAS-EAT-M22-1O` | πŸ”„ | +| 91 | `SSI-EAT-M22-1S` | `TAS-EAT-M22-1S` | πŸ”„ | + +### Kontierung +| ID | Alt | Neu | Status | +|----|-----|-----|--------| +| 114 | `GWG-KON-250` | `KON-GWG-250` | πŸ”„ | +| 221 | `GWG` | `KON-GWG` | πŸ”„ | +| 235 | `BUM-KON` | `KON-BUM` | πŸ”„ | +| 236 | `FAL-KON` | `KON-FAL` | πŸ”„ | +| 82 | `KFZ-Reparatur` | `KON-KFZ` | πŸ”„ | +| 81 | `Roh-__Hilfs-_und_Betriebsstoff` | `KON-RHB` | πŸ”„ | +| 317 | `Rabatt` | `KON-RAB` | πŸ”„ | +| 340 | `RUND-KON` | `KON-RUND` | πŸ”„ | +| 230 | `SKONTO` | `KON-SKO` | πŸ”„ | +| 261 | `SON-BET` | `KON-SON` | πŸ”„ | +| 22 | `Werkzeug` | `KON-WER` | πŸ”„ | + +--- + +## Import-Produkte (spΓ€ter pflegen) + +| ID | Alt | Beschreibung | Status | +|----|-----|--------------|--------| +| 371 | `NEW-SON-0102162` | H07V-K 6 gn/ge | ⏸️ | +| 374 | `NEW-SON-0419068` | OBO Goldenschraube | ⏸️ | +| 372 | `NEW-SON-0430104` | OBO Banderdungsschelle | ⏸️ | +| 373 | `NEW-SON-0430106` | OBO Banderdungsschelle | ⏸️ | +| 375 | `NEW-SON-0452210` | Wago 243-204 | ⏸️ | +| 353 | `NEW-SON-4540890` | Rutenbeck HDMI 2m | ⏸️ | +| 384 | `NEW-SON-4541306` | Rutenbeck RJ45 Cat6A | ⏸️ | +| 361 | `NEW-WAL-01500127` | Mennekes CEE Dose | ⏸️ | +| 359 | `NEW-WAL-01504212` | Mennekes Phasenwender | ⏸️ | +| 360 | `NEW-WAL-01570088` | Mennekes Wanddose | ⏸️ | +| 354 | `NEW-WAL-02002148` | BJ Steckdose | ⏸️ | +| 356 | `NEW-WAL-03589567` | Mennekes CEE Stecker | ⏸️ | +| 357 | `NEW-WAL-03589569` | Mennekes CEE Kupplung | ⏸️ | +| 358 | `NEW-WAL-03589570` | Mennekes CEE Kupplung | ⏸️ | +| 362 | `NEW-WAL-11002686` | H07RN-F 5G2,5 | ⏸️ | + +--- + +## Test-Produkt (lΓΆschen) + +| ID | Alt | Beschreibung | Status | +|----|-----|--------------|--------| +| 198 | `KOK-KAT-LCD90-90dB` | Label = "KAT-HERST-MERKMAL-Nr" | ❌ | + +--- + +## Statistik + +| Status | Anzahl | +|--------|--------| +| βœ… Bereits korrekt | ~20 | +| πŸ”„ Umbenennen | ~250 | +| ⏸️ Import (spΓ€ter) | 15 | +| ❌ LΓΆschen | 1 | + +--- + +*Erstellt: 2026-03-05* diff --git a/README.md b/README.md new file mode 100644 index 0000000..78d97b3 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# Produktverwaltung - Dolibarr Modul + +Kategorie-Baumansicht mit Inline-Editing und Produktverwaltung fΓΌr [Dolibarr ERP & CRM](https://www.dolibarr.org). + +## Features + +- **Kategorie-Baumansicht** - Hierarchische Darstellung aller Produktkategorien mit Auf-/Zuklappen +- **Inline-Editing** - Doppelklick auf Ref, Label oder Beschreibung zum direkten Bearbeiten +- **Best EK-Anzeige** - GΓΌnstigster Lieferantenpreis mit 3-Zeichen Lieferanten-Badge +- **Marge-Berechnung** - Automatische Berechnung des Aufschlags (VK-EK)/EK mit Farbmarkierung +- **Kategorie-Management** - Erstellen, Bearbeiten, LΓΆschen von Kategorien mit 20 Farb-Swatches +- **Produkte ohne Kategorie** - Eigene Sektion mit visueller Warnung +- **Ref-Schema-Hinweis** - Konfigurierbarer Infoblock mit Referenz-Schema +- **Status-Anzeige** - Verkaufbar/Beziehbar Icons (nur aktive werden angezeigt) +- **Produkt-Dialog** - Schnellbearbeitung von Ref, Label, Beschreibung, VK netto +- **Export** - CSV und PDF Export des Produktkatalogs + +## Screenshots + +``` ++----------------------------------------------------------+ +| Produktverwaltung - Kategorie-Übersicht [CSV] [PDF] | ++----------------------------------------------------------+ +| [i] Ref-Schema: KAT-HER-[TYP-]SPEC[-SERIE] | ++----------------------------------------------------------+ +| [Alle aufklappen] [Alle zuklappen] [+Kat] Suche:[____] | ++----------------------------------------------------------+ +| v Elektro (148) [edit][+][x] | +| v Leitungsschutzschalter (7) | +| | St | Ref | Label | EK | VK | +| | πŸ›’ | LSS-HAG-B16-1P | Hager B16 1p | 3.20 SON| 5.40| ++----------------------------------------------------------+ +| ⚠ Produkte ohne Kategorie (92) | ++----------------------------------------------------------+ +``` + +## Installation + +### Voraussetzungen +- Dolibarr ERP & CRM (getestet mit v18+) +- PHP 7.4+ +- MySQL/MariaDB + +### Installation + +```bash +cd /pfad/zu/dolibarr/custom/ +git clone https://git.data-it-solution.de/data/dolibarr.produktverwaltung.git produktverwaltung +``` + +### Aktivierung +1. Dolibarr als Admin einloggen +2. Einstellungen > Module > "Produktverwaltung" suchen +3. Modul aktivieren +4. MenΓΌpunkt erscheint unter **Produkte** im SeitenmenΓΌ + +## Einstellungen + +Unter **Einstellungen > Module > Produktverwaltung**: + +| Einstellung | Beschreibung | Standard | +|-------------|-------------|----------| +| Ref-Schema anzeigen | Info-Block mit Referenz-Schema auf der Hauptseite | Ja | +| Kategorien standardmÀßig aufklappen | Alle Kategorien beim Seitenaufruf aufgeklappt | Nein | +| Schema-Muster | Freier Text/HTML fΓΌr das Referenz-Schema | KAT-HER-[TYP-]SPEC[-SERIE] | +| Beispiele | Beispiel-Referenzen zur Veranschaulichung | - | + +## Berechtigungen + +| Berechtigung | Beschreibung | +|-------------|-------------| +| Anzeigen | Produktverwaltung anzeigen | +| Bearbeiten | Produkte bearbeiten (Ref/Label) | +| LΓΆschen | Produkte lΓΆschen | +| Exportieren | Produktverwaltung exportieren | + +## Technische Details + +### Modul-ID +`500032` + +### Dateistruktur +``` +produktverwaltung/ +β”œβ”€β”€ admin/ +β”‚ └── setup.php # Einstellungen +β”œβ”€β”€ ajax/ +β”‚ └── product_actions.php # AJAX-Handler +β”œβ”€β”€ core/ +β”‚ └── modules/ +β”‚ └── modProduktVerwaltung.class.php # Modul-Descriptor +β”œβ”€β”€ css/ +β”‚ └── produktverwaltung.css # Styles +β”œβ”€β”€ js/ +β”‚ └── produktverwaltung.js # Frontend-Logik +β”œβ”€β”€ langs/ +β”‚ β”œβ”€β”€ de_DE/produktverwaltung.lang # Deutsch +β”‚ └── en_US/produktverwaltung.lang # Englisch +β”œβ”€β”€ lib/ +β”‚ └── produktverwaltung.lib.php # Hilfsfunktionen +β”œβ”€β”€ export.php # CSV/PDF Export +└── produktverwaltungindex.php # Hauptseite +``` + +### AJAX-Endpunkte + +| Action | Beschreibung | +|--------|-------------| +| `update_ref` | Produkt-Referenz aktualisieren | +| `update_label` | Produkt-Bezeichnung aktualisieren | +| `update_description` | Produkt-Beschreibung aktualisieren | +| `update_product` | Produkt komplett aktualisieren (Dialog) | +| `delete_product` | Produkt lΓΆschen | +| `add_to_category` | Produkt einer Kategorie zuordnen | +| `remove_from_category` | Produkt aus Kategorie entfernen | +| `get_categories` | Kategorieliste laden | +| `get_product` | Produktdaten laden | +| `get_category_data` | Kategorie-Daten laden | +| `create_category` | Neue Kategorie erstellen | +| `update_category` | Kategorie bearbeiten | +| `delete_category` | Kategorie lΓΆschen | + +### Best EK-Logik + +Der gΓΌnstigste Einkaufspreis wird per Subquery aus `llx_product_fournisseur_price` ermittelt: +- `MIN(unitprice)` = gΓΌnstigster StΓΌckpreis +- LieferantenkΓΌrzel: `LEFT(COALESCE(NULLIF(name_alias,''), nom), 3)` +- Wird als Badge neben dem Preis angezeigt + +## Lizenz + +GPLv3 oder (nach Wahl) eine spΓ€tere Version. Siehe [COPYING](COPYING). + +## Autor + +Eduard Wisch - [data IT solution](https://data-it-solution.de) diff --git a/REFERENZ-SCHEMA.md b/REFERENZ-SCHEMA.md new file mode 100644 index 0000000..8929d08 --- /dev/null +++ b/REFERENZ-SCHEMA.md @@ -0,0 +1,346 @@ +# Produkt-Referenz Schema + +## Aufbau: `KAT-HER-[TYP-]SPEC[-SERIE]` + +| Position | Feld | Pflicht | Beschreibung | Beispiele | +|----------|------|---------|--------------|-----------| +| 1 | **KAT** | Ja | Produktkategorie (2-4 Zeichen) | LSS, FI, STK, KAB, PAK | +| 2 | **HER** | Ja | Hersteller (2-4 Zeichen) | HAG, EAT, WAG, BJ, OBO | +| 3 | **TYP** | Nein | Variante/Typ (nur wenn nΓΆtig) | FLX, STD, AP, UP | +| 4 | **SPEC** | Ja | Spezifikation/Grâße | B16, 3F, 5x1.5, 60x110 | +| 5 | **SERIE** | Nein | Produktserie (nur wenn nΓΆtig) | 221, 2273, 914 | + +### Regeln + +- **Trennzeichen:** Bindestrich `-` zwischen allen Feldern +- **Keine Sonderzeichen** außer Bindestrich +- **Großbuchstaben** durchgehend +- **Zahlen mit Einheit** ohne Leerzeichen: `10MM`, `3F`, `B16` +- **Unterstriche** fΓΌr Dezimaltrennzeichen: `2_5MM` statt `2,5MM` + +--- + +## Kategorie-KΓΌrzel (KAT) + +### Verteilung / Sicherungen + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `LSS` | Leitungsschutzschalter | `LSS-HER-CHAR-AMP[-POL]` | `LSS-HAG-B16-1P` | +| `FI` | FI-Schutzschalter | `FI-HER-AMP-POL-MA` | `FI-EAT-40A-4P-30` | +| `FILS` | FI/LS-Kombischalter | `FILS-HER-CHAR-AMP-MA` | `FILS-HAG-B16-30` | +| `AFDD` | Fehlerlichtbogen | `AFDD-HER-CHAR-AMP` | `AFDD-HAG-B10` | +| `SLS` | SLS-Schalter | `SLS-HER-AMP-POL` | `SLS-HAG-63A-3P` | +| `HSC` | Hauptschalter | `HSC-HER-AMP-POL` | `HSC-HAG-63A-3P` | +| `NEO` | Neozed | `NEO-HER-TYP-AMP` | `NEO-SIE-D02-35A` | +| `DIA` | Diazed | `DIA-HER-TYP-AMP` | `DIA-SIE-DIII-63A` | +| `UAS` | Überspannungsableiter | `UAS-HER-TYP` | `UAS-DEH-TYP2` | +| `SSS` | Stromstoßschalter | `SSS-HER-AMP` | `SSS-ELT-16A` | +| `ZSU` | Zeitschaltuhr | `ZSU-HER-MODELL` | `ZSU-HAG-EHN111` | +| `TLA` | Treppenlichtautomat | `TLA-HER-MODELL` | `TLA-THE-SUL181D` | +| `KLT` | Klingeltrafo | `KLT-HER-VOLT` | `KLT-HAG-8V` | +| `REL` | Relais | `REL-HER-AMP` | `REL-ELT-16A` | +| `TAS` | Taster/Leuchtmelder | `TAS-HER-TYP` | `TAS-ABB-WEC-16A` | + +### Reihenklemmen / Verbindungstechnik + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `STK` | Steckklemmen | `STK-HER-[TYP-]POL[-SERIE]` | `STK-WAG-FLX-3F-221` | +| `RKL` | Reihenklemmen | `RKL-HER-SERIE-TYP-MM` | `RKL-WAG-S-PE-4MM` | +| `PHS` | Phasenschienen | `PHS-HER-POL-MM-FA` | `PHS-HAG-3P-10MM-12F` | +| `VEB` | VerbindungsbrΓΌcken | `VEB-HER-POL` | `VEB-HAG-3P` | +| `AKL` | Abzweigklemmen | `AKL-HER-MM-FA` | `AKL-POL-25MM-12F` | +| `HLAK` | Hauptleitungsabzweigkl. | `HLAK-HER-MM` | `HLAK-POL-25MM` | + +### Kabel / Leitungen + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `NYM` | NYM-J Mantelleitung | `NYM-QUER` | `NYM-5x1_5` | +| `NYY` | NYY-J Erdkabel | `NYY-QUER` | `NYY-5x1_5` | +| `H07VK` | H07V-K FeindrΓ€htig | `H07VK-MM-FARBE` | `H07VK-10MM-GN` | +| `H07VU` | H07V-U Starr | `H07VU-MM-FARBE` | `H07VU-10MM-SW` | +| `H05VV` | H05VV-F Schlauchltg. | `H05VV-QUER-FARBE` | `H05VV-4x0_75-WS` | +| `H07RN` | H07RN-F Gummi | `H07RN-QUER` | `H07RN-5x2_5` | +| `STEU` | Steuerleitung | `STEU-TYP-QUER` | `STEU-YSLY-7x1_5` | +| `FERN` | Fernmeldekabel | `FERN-QUER` | `FERN-4x2x0_8` | +| `KOA` | Koaxialkabel | `KOA-TYP` | `KOA-LCD90` | +| `DAT` | Datenkabel | `DAT-CAT-TYP` | `DAT-CAT7-SFTP` | + +### Installationsmaterial + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `GED` | GerΓ€tedosen | `GED-HER-TIEFE` | `GED-KAI-46MM` | +| `HOW` | Hohlwanddosen | `HOW-HER-TYP` | `HOW-KAI-FLACH` | +| `ABZ` | AbzweigkΓ€sten | `ABZ-HER-MODELL` | `ABZ-HEN-9320` | +| `KAK` | Kabelkanal | `KAK-HER-BxH-FARBE` | `KAK-OBO-60x110-WS` | +| `KKE` | Kabelkanal-Endkappe | `KKE-HER-BxH` | `KKE-OBO-60x110` | +| `KKW` | Kabelkanal-Winkel | `KKW-HER-BxH-TYP` | `KKW-OBO-60x110-FW` | +| `KTR` | Kabeltrasse | `KTR-HER-TYP-BxH` | `KTR-OBO-RIN-60x100` | +| `PGV` | PG-Verschraubung | `PGV-HER-PG` | `PGV-OBO-PG16` | +| `NAS` | Nagelschellen | `NAS-HER-MM` | `NAS-OBO-7x12` | +| `NAF` | Nagelfix | `NAF-HER-MM` | `NAF-OBO-3_5x50` | +| `KBI` | Kabelbinder | `KBI-HER-LxB` | `KBI-CEL-320x7_5` | +| `ISO` | Isolierband | `ISO-HER-FARBE` | `ISO-CEL-SW` | +| `ADE` | AderendhΓΌlsen | `ADE-HER-MM-LAENGE` | `ADE-KLA-10MM-12MM` | +| `RKS` | Rohrkabelschuhe | `RKS-HER-MM` | `RKS-KLA-16MM` | +| `STV` | Stoßverbinder | `STV-HER-MM` | `STV-KLA-6MM` | +| `SCS` | Schrumpfschlauch | `SCS-HER-MM-FARBE` | `SCS-CEL-6_4MM-SW` | +| `DUE` | DΓΌbel | `DUE-HER-MM` | `DUE-FIS-6x30` | +| `SCR` | Schrauben | `SCR-HER-GxL` | `SCR-SPA-4_5x50` | +| `ERD` | Erdungsmaterial | `ERD-HER-TYP` | `ERD-DEH-TIE-D20` | + +### Schalterprogramme + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `RAH` | Rahmen | `RAH-HER-FA[-SERIE]` | `RAH-BJ-1F-914` | +| `WIP` | Wippe | `WIP-HER-TYP[-SERIE]` | `WIP-BJ-KTR-914` | +| `STD` | Steckdose | `STD-HER-TYP[-SERIE]` | `STD-BJ-EUC-914` | +| `WEC` | Wechselschalter | `WEC-HER-TYP[-SERIE]` | `WEC-VED-AP-2F` | +| `SER` | Serienschalter | `SER-HER-TYP[-SERIE]` | `SER-BJ-UP-914` | +| `KRZ` | Kreuzschalter | `KRZ-HER-TYP[-SERIE]` | `KRZ-BJ-UP-914` | +| `WSE` | Schaltereinsatz UP | `WSE-HER-TYP` | `WSE-BJ-WEC` | +| `APR` | AP-Rahmen | `APR-HER-FA-SERIE` | `APR-BJ-1F-914` | +| `BZS` | Blindzentralscheibe | `BZS-HER-SERIE` | `BZS-BJ-914` | + +### Leuchtmittel / Leuchten + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `LED` | LED-Leuchtmittel | `LED-HER-SOCKEL-WATT` | `LED-PHI-E27-5_9W` | +| `HAL` | Halogen | `HAL-HER-SOCKEL-WATT` | `HAL-SHA-R7S-200W` | +| `T8` | LeuchtstoffrΓΆhre | `T8-HER-LAENGE-KELVIN` | `T8-PHI-1500-840` | +| `LEU` | Leuchte | `LEU-HER-TYP` | `LEU-NIE-E27` | +| `STR` | Strahler | `STR-HER-WATT` | `STR-PHI-70W` | +| `WAN` | Wandleuchte | `WAN-HER-TYP` | `WAN-SLV-GRAFT` | +| `DEC` | Deckenleuchte | `DEC-HER-TYP` | `DEC-PHI-WT050C` | +| `FAS` | Fassung | `FAS-HER-SOCKEL` | `FAS-BRU-GU10` | +| `BAF` | Baufassung | `BAF-HER-SOCKEL` | `BAF-BAC-E27` | + +### IT / Netzwerk + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `PAK` | Patchkabel | `PAK-HER-LAENGE-CAT` | `PAK-DIG-025M-6A` | +| `PPM` | Patchpanel | `PPM-HER-FA-HE` | `PPM-DIG-24F-1HE` | +| `NES` | Netzwerkschrank | `NES-HER-BxT-ZOLL` | `NES-DIG-600x600-19Z` | +| `FAB` | Fachboden | `FAB-HER-HE` | `FAB-DIG-1HE` | +| `STL` | Steckdosenleiste | `STL-HER-FA` | `STL-DIG-8F` | +| `SWI` | Switch | `SWI-HER-PORT` | `SWI-DLI-8P` | +| `REP` | Repeater | `REP-HER-MODELL` | `REP-FRI-1200AX` | +| `NAS` | NAS | `NAS-HER-MODELL` | `NAS-SYN-DS225+` | +| `HDD` | Festplatte | `HDD-HER-TB` | `HDD-SYN-4TB` | +| `KSM` | Keystone-Modul | `KSM-HER-CAT` | `KSM-DIG-6A` | +| `NWD` | Netzwerkdose | `NWD-HER-TYP-FA` | `NWD-RUT-UP-2F` | +| `KAV` | Kabelverbinder | `KAV-HER-CAT` | `KAV-TEL-CAT7A` | + +### ZΓ€hlerschrΓ€nke + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `ZAS` | ZΓ€hlerschrank | `ZAS-HER-REIH` | `ZAS-HAG-4F` | +| `ZAF` | ZΓ€hlerfeld | `ZAF-HER-TYP` | `ZAF-HAG-3PU-2F` | +| `VEF` | Verteilerfeld | `VEF-HER-TYP` | `VEF-HAG-1350MM` | +| `KRK` | Kabelrangierkanal | `KRK-HER-BREITE` | `KRK-HAG-B800` | +| `KOM` | Kombiableiter | `KOM-HER-POL` | `KOM-HAG-4P` | +| `TSG` | BestΓΌckungspaket | `TSG-HER-TYP` | `TSG-EAT-ZSD31` | +| `ZUB` | ZubehΓΆr | `ZUB-HER-TYP` | `ZUB-HAG-PLT-A4` | +| `SSV` | Sammelschienenverbinder | `SSV-HER-POL` | `SSV-HAG-5P` | +| `ESA` | Einspeiseadapter | `ESA-HER-MM-POL` | `ESA-HAG-25MM-5P` | + +### HIFI / Audio + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `LAU` | Lautsprecher | `LAU-HER-MODELL` | `LAU-MON-ESP315` | +| `VES` | VerstΓ€rker | `VES-HER-MODELL` | `VES-BOS-VE2160` | +| `MIK` | Mikrofon | `MIK-HER-MODELL` | `MIK-BOS-CC2` | +| `RCD` | Radio/CD | `RCD-HER-MODELL` | `RCD-TEC-143CD` | + +### TV + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `WAH` | Wandhalterung | `WAH-HER-TYP-GROESSE` | `WAH-VOG-TILT-MED` | + +### Sonstiges + +| KΓΌrzel | Kategorie | Muster | Beispiel | +|--------|-----------|--------|----------| +| `BAT` | Batterien | `BAT-HER-TYP` | `BAT-VAR-9V` | +| `WBX` | Wallbox | `WBX-HER-KW` | `WBX-KOS-11KW` | +| `RWM` | Rauchmelder | `RWM-HER-TYP` | `RWM-HAG-STD` | +| `BWM` | Bewegungsmelder | `BWM-HER-GRAD` | `BWM-BJ-220BT` | +| `KGO` | Klingelgong | `KGO-HER-VOLT` | `KGO-GRO-230V` | +| `UV` | Unterverteiler | `UV-HER-TYP-REIH` | `UV-HAG-AP-4F` | +| `SCT` | SchranktΓΌr | `SCT-HER-REIH` | `SCT-HAG-4F` | +| `WER` | Werkzeug | `WER-HER-TYP` | `WER-MAK-AKK-18V` | +| `VER` | Versand | `VER` | `VER` | +| `ANK` | Anschlusskabel | `ANK-HER-TYP-LAENGE` | `ANK-BAC-EUR-2M` | +| `HDMI` | HDMI-Kabel | `HDMI-HER-LAENGE` | `HDMI-RUT-3M` | +| `TRA` | Trafo | `TRA-HER-VOLT-WATT` | `TRA-ELT-12V-10W` | +| `PLA` | Powerline | `PLA-HER-MODELL` | `PLA-FRI-1260E` | +| `KSU` | Kleinspeicher | `KSU-HER-LITER` | `KSU-BOS-5L` | + +--- + +## Hersteller-KΓΌrzel (HER) + +| KΓΌrzel | Hersteller | | KΓΌrzel | Hersteller | +|--------|------------|---|--------|------------| +| `ABB` | ABB | | `MER` | Mersen | +| `BAC` | Bachmann | | `MET` | Metz Connect | +| `BAR` | Barracuda | | `MON` | Monacor | +| `BOS` | Bose | | `NIE` | Niermann | +| `BJ` | Busch-Jaeger | | `OBO` | OBO Bettermann | +| `BRU` | Brumberg | | `PHI` | Philips | +| `CEL` | Cellpack | | `POL` | Pollmann | +| `DEH` | DEHN | | `RUT` | Rutenbeck | +| `DIG` | Digitus | | `SCO` | S-CONN | +| `DLI` | D-Link | | `SHA` | Scharnberger | +| `DRE` | Dresselhaus | | `SIE` | Siemens | +| `EAT` | Eaton | | `SLV` | SLV | +| `EBE` | Eberle | | `SON` | Sonepar (Import) | +| `EFB` | EFB Elektronik | | `SPA` | Spax | +| `EGS` | Egston | | `SPE` | Spelsberg | +| `ELT` | Eltako | | `SYN` | Synology | +| `FIS` | Fischer | | `TEC` | Technisat | +| `FRI` | Fritz/AVM | | `TEL` | TelegΓ€rtner | +| `GRO` | Grothe | | `THE` | Theben | +| `HAG` | Hager | | `TRU` | Tru Components | +| `HEN` | Hensel | | `VAR` | Varta | +| `HOU` | Houben | | `VED` | Presto-Vedder | +| `KAI` | Kaiser | | `VOG` | Vogels | +| `KAT` | Kathrein | | `WAG` | Wago | +| `KLA` | Klauke | | `WAL` | Waldmann (Import) | +| `KOS` | Kostal | | | | +| `LPR` | Lightpro | | | | +| `MAK` | Makita | | | | +| `MEN` | Mennekes | | | | + +--- + +## Varianten-KΓΌrzel (TYP) + +| KΓΌrzel | Bedeutung | | KΓΌrzel | Bedeutung | +|--------|-----------|---|--------|-----------| +| `AP` | Aufputz | | `FW` | Flachwinkel | +| `UP` | Unterputz | | `IW` | Innenwinkel | +| `FLX` | Flex-Variante | | `AW` | Außenwinkel | +| `STD` | Standard | | `END` | Endkappe | +| `S` | TopJob S | | `RIN` | Rinne | +| `SD` | TopJob SD | | `WS` | Weiß | +| `TRN` | Trennklemme | | `SW` | Schwarz | +| `PE` | Schutzleiter | | `GR` | Grau | +| `N` | Neutralleiter | | `BL` | Blau | +| `L` | Phase | | `GN` | GrΓΌn/Gelb | +| `KTR` | Kontroll | | `ANT` | Anthrazit | + +--- + +## Serien-KΓΌrzel (SERIE) + +| KΓΌrzel | Serie | Hersteller | +|--------|-------|------------| +| `221` | Wago Flex 221-xxx | Wago | +| `222` | Wago Standard alt | Wago | +| `2273` | Wago Compact 2273-xxx | Wago | +| `243` | Wago Verbindungsdose | Wago | +| `914` | Balance SI | Busch-Jaeger | +| `214` | Reflex SI | Busch-Jaeger | +| `M22` | M22 BefehlsgerΓ€te | Eaton | + +--- + +## Beispiele nach Kategorie + +### Leitungsschutzschalter +``` +LSS-HAG-B16-1P Hager LSS B16A 1-polig +LSS-HAG-B16-3P Hager LSS B16A 3-polig +LSS-EAT-C16-3P Eaton LSS C16A 3-polig +LSS-EAT-C20-3P Eaton LSS C20A 3-polig +``` + +### FI-Schutzschalter +``` +FI-HAG-40A-4P-30 Hager FI 40A 4-polig 30mA +FI-EAT-40A-4P-30 Eaton FI 40A 4-polig 30mA +FI-HAG-63A-4P-30 Hager FI 63A 4-polig 30mA +``` + +### Wago Steckklemmen +``` +STK-WAG-FLX-2F-221 Wago Flex 2-fach (Serie 221) +STK-WAG-FLX-3F-221 Wago Flex 3-fach (Serie 221) +STK-WAG-FLX-5F-221 Wago Flex 5-fach (Serie 221) +STK-WAG-STD-2F-2273 Wago Compact 2-fach (Serie 2273) +STK-WAG-STD-3F-2273 Wago Compact 3-fach (Serie 2273) +STK-WAG-STD-5F-2273 Wago Compact 5-fach (Serie 2273) +``` + +### Wago Reihenklemmen +``` +RKL-WAG-S-PE-4MM TopJob S Schutzleiter 4mmΒ² +RKL-WAG-S-N-4MM TopJob S Neutralleiter 4mmΒ² +RKL-WAG-SD-PE-16MM TopJob SD Schutzleiter 16mmΒ² +RKL-WAG-SD-TRN-N TopJob SD Trennklemme N +``` + +### Kabel +``` +NYM-5x1_5 NYM-J 5x1,5mmΒ² +NYM-5x2_5 NYM-J 5x2,5mmΒ² +NYY-5x1_5 NYY-J 5x1,5mmΒ² +H07VK-10MM-GN H07V-K 10mmΒ² GrΓΌn/Gelb +H07VK-10MM-SW H07V-K 10mmΒ² Schwarz +H05VV-4x0_75-WS H05VV-F 4x0,75mmΒ² Weiß +``` + +### Busch-Jaeger Balance SI +``` +RAH-BJ-1F-914 Rahmen 1-fach Balance SI +RAH-BJ-2F-914 Rahmen 2-fach Balance SI +RAH-BJ-3F-914 Rahmen 3-fach Balance SI +APR-BJ-1F-914 AP-Rahmen 1-fach Balance SI +STD-BJ-EUC-914 Steckdose EUC Balance SI +WIP-BJ-STD-914 Wippe Standard Balance SI +WSE-BJ-WEC Wechselschalter-Einsatz +``` + +### Patchkabel +``` +PAK-DIG-025M-6A Digitus Patchkabel 0,25m Cat6A +PAK-DIG-050M-6A Digitus Patchkabel 0,5m Cat6A +PAK-DIG-1M-6A Digitus Patchkabel 1m Cat6A +PAK-EFB-15M-6A EFB Patchkabel 15m Cat6A +``` + +--- + +## SonderfΓ€lle + +### Produkte ohne Hersteller (Eigenmarke/Generic) +Verwende `GEN` als Hersteller: +``` +NYM-5x1_5 Kabel ohne Hersteller +DUE-GEN-6x30 DΓΌbel ohne Marke +``` + +### Import-Produkte (NEW-*) +Bleiben temporΓ€r unverΓ€ndert bis zur manuellen Pflege: +``` +NEW-SON-* Sonepar Import (nicht umbenennen) +NEW-WAL-* Waldmann Import (nicht umbenennen) +``` + +### Kontierung (keine physischen Produkte) +``` +KON-GWG Geringwertige WirtschaftsgΓΌter +KON-SKO Skonto +KON-VER Versand +KON-RAB Rabatt +``` diff --git a/admin/about.php b/admin/about.php new file mode 100644 index 0000000..d4ff783 --- /dev/null +++ b/admin/about.php @@ -0,0 +1,118 @@ + + * Copyright (C) 2026 Eduard Wisch + * Copyright (C) 2024 FrΓ©dΓ©ric France + * + * 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 . + */ + +/** + * \file produktverwaltung/admin/about.php + * \ingroup produktverwaltung + * \brief About page of module ProduktVerwaltung. + */ + +// 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/produktverwaltung.lib.php'; + +/** + * @var Conf $conf + * @var DoliDB $db + * @var HookManager $hookmanager + * @var Translate $langs + * @var User $user + */ + +// Translations +$langs->loadLangs(array("errors", "admin", "produktverwaltung@produktverwaltung")); + +// 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 = "ProduktVerwaltungSetup"; + +llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-produktverwaltung page-admin_about'); + +// Subheader +$linkback = ''.$langs->trans("BackToModuleList").''; + +print load_fiche_titre($langs->trans($title), $linkback, 'title_setup'); + +// Configuration header +$head = produktverwaltungAdminPrepareHead(); +print dol_get_fiche_head($head, 'about', $langs->trans($title), 0, 'produktverwaltung@produktverwaltung'); + +dol_include_once('/produktverwaltung/core/modules/modProduktVerwaltung.class.php'); +$tmpmodule = new modProduktVerwaltung($db); +print $tmpmodule->getDescLong(); + +// Page end +print dol_get_fiche_end(); +llxFooter(); +$db->close(); diff --git a/admin/setup.php b/admin/setup.php new file mode 100644 index 0000000..5ad46a4 --- /dev/null +++ b/admin/setup.php @@ -0,0 +1,171 @@ + + * + * 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 . + */ + +/** + * \file produktverwaltung/admin/setup.php + * \ingroup produktverwaltung + * \brief ProduktVerwaltung setup page - Ref-Schema Konfiguration + */ + +// 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/admin.lib.php"; +require_once '../lib/produktverwaltung.lib.php'; + +$langs->loadLangs(array("admin", "produktverwaltung@produktverwaltung")); + +if (!$user->admin) { + accessforbidden(); +} + +$action = GETPOST('action', 'aZ09'); + +// Default Ref-Schema +$defaultSchema = 'KAT-HER-[TYP-]SPEC[-SERIE] + +KAT = Produktkategorie (2-4 Zeichen), z.B. LSS, FI, STK, NYM +HER = Hersteller (2-4 Zeichen), z.B. HAG, EAT, WAG, BJ +TYP = Variante (optional), z.B. FLX, STD, AP, UP +SPEC = Spezifikation/Groesse, z.B. B16, 3F, 5x1_5, 60x110 +SERIE = Produktserie (optional), z.B. 221, 2273, 914'; + +$defaultExample = 'LSS-HAG-B16-1P = Hager Leitungsschutzschalter B16A 1-polig +STK-WAG-FLX-3F-221 = Wago Flex 3-fach (Serie 221) +NYM-5x1_5 = NYM-J Mantelleitung 5x1,5mmΒ² +RAH-BJ-1F-914 = Busch-Jaeger Rahmen 1-fach Balance SI'; + + +/* + * Actions + */ +if ($action == 'update' && $user->admin) { + $showSchema = GETPOST('PRODUKTVERWALTUNG_SHOW_SCHEMA', 'alpha'); + $refSchema = GETPOST('PRODUKTVERWALTUNG_REF_SCHEMA', 'restricthtml'); + $refExample = GETPOST('PRODUKTVERWALTUNG_REF_EXAMPLE', 'restricthtml'); + $defaultExpanded = GETPOST('PRODUKTVERWALTUNG_DEFAULT_EXPANDED', 'alpha'); + + dolibarr_set_const($db, 'PRODUKTVERWALTUNG_SHOW_SCHEMA', $showSchema, 'chaine', 0, '', $conf->entity); + dolibarr_set_const($db, 'PRODUKTVERWALTUNG_REF_SCHEMA', $refSchema, 'chaine', 0, '', $conf->entity); + dolibarr_set_const($db, 'PRODUKTVERWALTUNG_REF_EXAMPLE', $refExample, 'chaine', 0, '', $conf->entity); + dolibarr_set_const($db, 'PRODUKTVERWALTUNG_DEFAULT_EXPANDED', $defaultExpanded, 'chaine', 0, '', $conf->entity); + + setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); +} + + +/* + * View + */ +$form = new Form($db); + +llxHeader('', $langs->trans("ProduktVerwaltungSetup"), '', '', 0, 0, '', '', '', 'mod-produktverwaltung page-admin'); + +$linkback = ''.$langs->trans("BackToModuleList").''; +print load_fiche_titre($langs->trans("ProduktVerwaltungSetup"), $linkback, 'title_setup'); + +$head = produktverwaltungAdminPrepareHead(); +print dol_get_fiche_head($head, 'settings', $langs->trans("ProduktVerwaltungSetup"), -1, "fa-sitemap"); + +// Current values +$showSchema = getDolGlobalString('PRODUKTVERWALTUNG_SHOW_SCHEMA', '1'); +$defaultExpanded = getDolGlobalString('PRODUKTVERWALTUNG_DEFAULT_EXPANDED', '0'); +$refSchema = getDolGlobalString('PRODUKTVERWALTUNG_REF_SCHEMA', $defaultSchema); +$refExample = getDolGlobalString('PRODUKTVERWALTUNG_REF_EXAMPLE', $defaultExample); + +print '
'; +print ''; +print ''; + +print ''; + +// Section title +print ''; +print ''; +print ''; + +// Show schema on main page +print ''; +print ''; +print ''; +print ''; + +// Default expanded +print ''; +print ''; +print ''; +print ''; + +// Schema pattern +print ''; +print ''; +print ''; +print ''; + +// Examples +print ''; +print ''; +print ''; +print ''; + +print '
'.$langs->trans("RefSchemaTitle").'
'.$langs->trans("RefSchemaShow").''; +print $form->selectyesno('PRODUKTVERWALTUNG_SHOW_SCHEMA', $showSchema, 1); +print '
'.$langs->trans("DefaultExpanded").''; +print $form->selectyesno('PRODUKTVERWALTUNG_DEFAULT_EXPANDED', $defaultExpanded, 1); +print '
'.$langs->trans("RefSchemaPattern").''; +print ''; +print '
'.$langs->trans("RefSchemaHelp").''; +print '
'.$langs->trans("RefSchemaExample").''; +print ''; +print '
'; + +print '
'; +print ''; +print '
'; + +print '
'; + +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/ajax/product_actions.php b/ajax/product_actions.php new file mode 100644 index 0000000..f95f7ea --- /dev/null +++ b/ajax/product_actions.php @@ -0,0 +1,524 @@ + + * + * 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 produktverwaltung/ajax/product_actions.php + * \brief AJAX handler for product actions (inline edit, delete, category assign) + */ + +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREHTML')) { + define('NOREQUIREHTML', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} +if (!defined('NOCSRFCHECK')) { + define('NOCSRFCHECK', '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) { + http_response_code(500); + echo json_encode(array('success' => false, 'error' => 'Include of main fails')); + exit; +} + +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php'; + +header('Content-Type: application/json; charset=utf-8'); + +$langs->loadLangs(array("produktverwaltung@produktverwaltung")); + +$action = GETPOST('action', 'aZ09'); +$productId = GETPOSTINT('product_id'); +$categoryId = GETPOSTINT('category_id'); +$value = GETPOST('value', 'restricthtml'); + +$response = array('success' => false); + +switch ($action) { + case 'update_ref': + if (!$user->hasRight('produktverwaltung', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($productId) || empty($value)) { + $response['error'] = 'Missing parameters'; + break; + } + + $value = strtoupper(trim($value)); + + // Check if ref already exists + $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."product WHERE ref = '".$db->escape($value)."' AND rowid != ".((int) $productId); + $resql = $db->query($sql); + if ($resql && $db->num_rows($resql) > 0) { + $response['error'] = $langs->trans('RefAlreadyExists'); + break; + } + + $product = new Product($db); + if ($product->fetch($productId) > 0) { + $product->ref = $value; + $result = $product->update($product->id, $user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $product->error ?: 'Update failed'; + } + } else { + $response['error'] = 'Product not found'; + } + break; + + case 'update_label': + if (!$user->hasRight('produktverwaltung', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($productId) || empty($value)) { + $response['error'] = 'Missing parameters'; + break; + } + + $product = new Product($db); + if ($product->fetch($productId) > 0) { + $product->label = trim($value); + $result = $product->update($product->id, $user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $product->error ?: 'Update failed'; + } + } else { + $response['error'] = 'Product not found'; + } + break; + + case 'delete_product': + if (!$user->hasRight('produktverwaltung', 'delete')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($productId)) { + $response['error'] = 'Missing product_id'; + break; + } + + $product = new Product($db); + if ($product->fetch($productId) > 0) { + $result = $product->delete($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $product->error ?: 'Delete failed'; + } + } else { + $response['error'] = 'Product not found'; + } + break; + + case 'add_to_category': + if (!$user->hasRight('produktverwaltung', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($productId) || empty($categoryId)) { + $response['error'] = 'Missing parameters'; + break; + } + + $product = new Product($db); + if ($product->fetch($productId) <= 0) { + $response['error'] = 'Product not found'; + break; + } + + $categorie = new Categorie($db); + if ($categorie->fetch($categoryId) <= 0) { + $response['error'] = 'Category not found'; + break; + } + + $result = $categorie->add_type($product, Categorie::TYPE_PRODUCT); + if ($result >= 0) { + $response['success'] = true; + } else { + $response['error'] = $categorie->error ?: 'Assignment failed'; + } + break; + + case 'remove_from_category': + if (!$user->hasRight('produktverwaltung', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($productId) || empty($categoryId)) { + $response['error'] = 'Missing parameters'; + break; + } + + $product = new Product($db); + if ($product->fetch($productId) <= 0) { + $response['error'] = 'Product not found'; + break; + } + + $categorie = new Categorie($db); + if ($categorie->fetch($categoryId) <= 0) { + $response['error'] = 'Category not found'; + break; + } + + $result = $categorie->del_type($product, Categorie::TYPE_PRODUCT); + if ($result >= 0) { + $response['success'] = true; + } else { + $response['error'] = $categorie->error ?: 'Remove failed'; + } + break; + + case 'get_categories': + if (!$user->hasRight('produktverwaltung', 'read')) { + $response['error'] = 'Permission denied'; + break; + } + + $categorie = new Categorie($db); + $fullTree = $categorie->get_full_arbo(Categorie::TYPE_PRODUCT); + + $categories = array(); + if (is_array($fullTree)) { + foreach ($fullTree as $cat) { + $categories[] = array( + 'id' => $cat['id'], + 'label' => $cat['label'], + 'fullpath' => str_replace(' >> ', ' > ', $cat['fulllabel']), + ); + } + } + + // Sort by fullpath + usort($categories, function ($a, $b) { + return strcmp($a['fullpath'], $b['fullpath']); + }); + + $response['success'] = true; + $response['categories'] = $categories; + break; + + case 'get_product': + if (!$user->hasRight('produktverwaltung', 'read')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($productId)) { + $response['error'] = 'Missing product_id'; + break; + } + + $product = new Product($db); + if ($product->fetch($productId) > 0) { + $response['success'] = true; + $response['product'] = array( + 'id' => $product->id, + 'ref' => $product->ref, + 'label' => $product->label, + 'description' => $product->description, + 'sell_price' => $product->price, + 'cost_price' => $product->cost_price, + 'pmp' => $product->pmp, + 'tva_tx' => $product->tva_tx, + 'price_base_type' => $product->price_base_type, + ); + } else { + $response['error'] = 'Product not found'; + } + break; + + case 'update_product': + if (!$user->hasRight('produktverwaltung', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($productId)) { + $response['error'] = 'Missing product_id'; + break; + } + + $newRef = GETPOST('new_ref', 'alpha'); + $newLabel = GETPOST('new_label', 'restricthtml'); + $newDescription = GETPOST('new_description', 'restricthtml'); + $newSellPrice = GETPOST('new_sell_price', 'alpha'); + + $product = new Product($db); + if ($product->fetch($productId) <= 0) { + $response['error'] = 'Product not found'; + break; + } + + $db->begin(); + $error = 0; + + // Update ref if changed + if ($newRef !== '' && $newRef !== null) { + $newRef = strtoupper(trim($newRef)); + if ($newRef !== $product->ref) { + // Check uniqueness + $sqlCheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."product WHERE ref = '".$db->escape($newRef)."' AND rowid != ".((int) $productId); + $resqlCheck = $db->query($sqlCheck); + if ($resqlCheck && $db->num_rows($resqlCheck) > 0) { + $response['error'] = $langs->trans('RefAlreadyExists'); + $error++; + } else { + $product->ref = $newRef; + } + } + } + + // Update label if changed + if (!$error && $newLabel !== '' && $newLabel !== null) { + $newLabel = trim($newLabel); + if ($newLabel !== $product->label) { + $product->label = $newLabel; + } + } + + // Update description if provided + if (!$error && $newDescription !== null) { + $product->description = trim($newDescription); + } + + // Save product changes (ref, label, description) + if (!$error) { + $result = $product->update($product->id, $user); + if ($result <= 0) { + $response['error'] = $product->error ?: 'Update failed'; + $error++; + } + } + + // Update sell price if provided (separate method) + if (!$error && $newSellPrice !== '' && $newSellPrice !== null) { + $newSellPrice = (float) price2num($newSellPrice); + if ($newSellPrice != $product->price) { + $result = $product->updatePrice($newSellPrice, $product->price_base_type, $user, $product->tva_tx); + if ($result <= 0) { + $response['error'] = $product->error ?: 'Price update failed'; + $error++; + } + } + } + + if (!$error) { + $db->commit(); + $response['success'] = true; + // Return updated values + $product->fetch($productId); + $response['product'] = array( + 'id' => $product->id, + 'ref' => $product->ref, + 'label' => $product->label, + 'sell_price' => $product->price, + ); + } else { + $db->rollback(); + } + break; + + case 'get_category_data': + if (!$user->hasRight('produktverwaltung', 'read')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($categoryId)) { + $response['error'] = 'Missing category_id'; + break; + } + + $categorie = new Categorie($db); + if ($categorie->fetch($categoryId) > 0) { + $response['success'] = true; + $response['category'] = array( + 'id' => $categorie->id, + 'label' => $categorie->label, + 'description' => $categorie->description, + 'color' => $categorie->color, + 'fk_parent' => $categorie->fk_parent, + ); + } else { + $response['error'] = 'Category not found'; + } + break; + + case 'create_category': + if (!$user->hasRight('produktverwaltung', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + + $catLabel = GETPOST('cat_label', 'alphanohtml'); + $catDescription = GETPOST('cat_description', 'restricthtml'); + $catColor = GETPOST('cat_color', 'alphanohtml'); + $catParent = GETPOSTINT('cat_parent'); + + if (empty($catLabel)) { + $response['error'] = 'Label is required'; + break; + } + + $categorie = new Categorie($db); + $categorie->label = trim($catLabel); + $categorie->description = trim($catDescription); + $categorie->color = str_replace('#', '', trim($catColor)); + $categorie->fk_parent = $catParent > 0 ? $catParent : 0; + $categorie->type = Categorie::TYPE_PRODUCT; + $categorie->visible = 1; + + $result = $categorie->create($user); + if ($result > 0) { + $response['success'] = true; + $response['category_id'] = $result; + } else { + $response['error'] = $categorie->error ?: 'Create failed'; + } + break; + + case 'update_category': + if (!$user->hasRight('produktverwaltung', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($categoryId)) { + $response['error'] = 'Missing category_id'; + break; + } + + $categorie = new Categorie($db); + if ($categorie->fetch($categoryId) <= 0) { + $response['error'] = 'Category not found'; + break; + } + + $catLabel = GETPOST('cat_label', 'alphanohtml'); + $catDescription = GETPOST('cat_description', 'restricthtml'); + $catColor = GETPOST('cat_color', 'alphanohtml'); + $catParent = GETPOST('cat_parent', 'int'); + + if (!empty($catLabel)) { + $categorie->label = trim($catLabel); + } + if ($catDescription !== null) { + $categorie->description = trim($catDescription); + } + if ($catColor !== null) { + $categorie->color = str_replace('#', '', trim($catColor)); + } + if ($catParent !== null && $catParent !== '') { + $newParent = (int) $catParent; + // Prevent setting self as parent + if ($newParent !== $categorie->id) { + $categorie->fk_parent = $newParent; + } + } + + $result = $categorie->update($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $categorie->error ?: 'Update failed'; + } + break; + + case 'delete_category': + if (!$user->hasRight('produktverwaltung', 'delete')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($categoryId)) { + $response['error'] = 'Missing category_id'; + break; + } + + $categorie = new Categorie($db); + if ($categorie->fetch($categoryId) <= 0) { + $response['error'] = 'Category not found'; + break; + } + + $result = $categorie->delete($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $categorie->error ?: 'Delete failed'; + } + break; + + case 'update_description': + if (!$user->hasRight('produktverwaltung', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if (empty($productId)) { + $response['error'] = 'Missing product_id'; + break; + } + + $product = new Product($db); + if ($product->fetch($productId) > 0) { + $product->description = trim($value); + $result = $product->update($product->id, $user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $product->error ?: 'Update failed'; + } + } else { + $response['error'] = 'Product not found'; + } + break; + + default: + $response['error'] = 'Unknown action: '.$action; + break; +} + +echo json_encode($response); +$db->close(); diff --git a/build/buildzip.php b/build/buildzip.php new file mode 100644 index 0000000..3508bbb --- /dev/null +++ b/build/buildzip.php @@ -0,0 +1,316 @@ +#!/usr/bin/env php -d memory_limit=256M + + * + * 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 . + */ + +/* + 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(?.*)\.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*'(?.*)'\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); +} diff --git a/build/makepack-produktverwaltung.conf b/build/makepack-produktverwaltung.conf new file mode 100644 index 0000000..16dc1e7 --- /dev/null +++ b/build/makepack-produktverwaltung.conf @@ -0,0 +1,11 @@ +# Your module name here +# +# Goal: Goal of module +# Version: +# Author: Copyright - +# 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/ \ No newline at end of file diff --git a/core/modules/modProduktVerwaltung.class.php b/core/modules/modProduktVerwaltung.class.php new file mode 100644 index 0000000..6941063 --- /dev/null +++ b/core/modules/modProduktVerwaltung.class.php @@ -0,0 +1,484 @@ + + * Copyright (C) 2018-2019 Nicolas ZABOURI + * Copyright (C) 2019-2024 Frédéric France + * Copyright (C) 2026 Eduard Wisch + * + * 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 . + */ + +/** + * \defgroup produktverwaltung Module ProduktVerwaltung + * \brief ProduktVerwaltung module descriptor. + * + * \file htdocs/produktverwaltung/core/modules/modProduktVerwaltung.class.php + * \ingroup produktverwaltung + * \brief Description and activation file for module ProduktVerwaltung + */ +include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php'; + + +/** + * Description and activation class for module ProduktVerwaltung + */ +class modProduktVerwaltung 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 = 500032; // 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 = 'produktverwaltung'; + + // 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 'ModuleProduktVerwaltungName' not found (ProduktVerwaltung is name of module). + $this->name = preg_replace('/^mod/i', '', get_class($this)); + + // DESCRIPTION_FLAG + // Module description, used if translation string 'ModuleProduktVerwaltungDesc' not found (ProduktVerwaltung is name of module). + $this->description = "ProduktVerwaltungDescription"; + // Used only if file README.md and README-LL.md not found. + $this->descriptionlong = "ProduktVerwaltungDescription"; + + // 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@produktverwaltung' + + // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' + $this->version = '1.0'; + // 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 PRODUKTVERWALTUNG 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-sitemap'; + + // 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, + // CSS and JS are loaded locally in pages that need them, not globally + 'css' => array(), + 'js' => array(), + // 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("/produktverwaltung/temp","/produktverwaltung/subdir"); + $this->dirs = array("/produktverwaltung/temp"); + + // Config pages. Put here list of php page, stored into produktverwaltung/admin directory, to use to setup module. + $this->config_page_url = array("setup.php@produktverwaltung"); + + // Dependencies + // A condition to hide module + $this->hidden = getDolGlobalInt('MODULE_PRODUKTVERWALTUNG_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("produktverwaltung@produktverwaltung"); + + // 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 = 1; + + // 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'=>'ProduktVerwaltungWasAutomaticallyActivatedBecauseOfYourCountryChoice'); + //$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('PRODUKTVERWALTUNG_MYNEWCONST1', 'chaine', 'myvalue', 'This is a constant to add', 1), + // 2 => array('PRODUKTVERWALTUNG_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("produktverwaltung")) { + $conf->produktverwaltung = new stdClass(); + $conf->produktverwaltung->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@produktverwaltung:$user->hasRight(\'produktverwaltung\', \'read\'):/produktverwaltung/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@produktverwaltung:$user->hasRight(\'othermodule\', \'read\'):/produktverwaltung/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' => 'produktverwaltung@produktverwaltung', + // 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('produktverwaltung'), isModEnabled('produktverwaltung'), isModEnabled('produktverwaltung')), + // 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 produktverwaltung/core/boxes that contains a class to show a widget. + /* BEGIN MODULEBUILDER WIDGETS */ + $this->boxes = array( + // 0 => array( + // 'file' => 'produktverwaltungwidget1.php@produktverwaltung', + // 'note' => 'Widget provided by ProduktVerwaltung', + // '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' => 'MyJob label', + // 'jobtype' => 'method', + // 'class' => '/produktverwaltung/class/myobject.class.php', + // 'objectname' => 'MyObject', + // 'method' => 'doScheduledJob', + // 'parameters' => '', + // 'comment' => 'Comment', + // 'frequency' => 2, + // 'unitfrequency' => 3600, + // 'status' => 0, + // 'test' => 'isModEnabled("produktverwaltung")', + // '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("produktverwaltung")', 'priority'=>50), + // 1=>array('label'=>'My label', 'jobtype'=>'command', 'command'=>'', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>1, 'unitfrequency'=>3600*24, 'status'=>0, 'test'=>'isModEnabled("produktverwaltung")', 'priority'=>50) + // ); + + // Permissions provided by this module + $this->rights = array(); + $r = 0; + + $this->rights[$r][0] = $this->numero . '01'; + $this->rights[$r][1] = 'Produktverwaltung anzeigen'; + $this->rights[$r][3] = 1; // Default enabled + $this->rights[$r][4] = 'read'; + $r++; + + $this->rights[$r][0] = $this->numero . '02'; + $this->rights[$r][1] = 'Produkte bearbeiten (Ref/Label)'; + $this->rights[$r][4] = 'write'; + $r++; + + $this->rights[$r][0] = $this->numero . '03'; + $this->rights[$r][1] = 'Produkte loeschen'; + $this->rights[$r][4] = 'delete'; + $r++; + + $this->rights[$r][0] = $this->numero . '04'; + $this->rights[$r][1] = 'Produktverwaltung exportieren'; + $this->rights[$r][4] = 'export'; + $r++; + + + // Main menu entries to add + $this->menu = array(); + $r = 0; + + // Left menu under "Products" (mainmenu=products) + $this->menu[$r++] = array( + 'fk_menu' => 'fk_mainmenu=products', + 'type' => 'left', + 'titre' => 'KategorieBaum', + 'prefix' => img_picto('', 'fa-sitemap', 'class="pictofixedwidth valignmiddle paddingright"'), + 'mainmenu' => 'products', + 'leftmenu' => 'produktverwaltung', + 'url' => '/produktverwaltung/produktverwaltungindex.php', + 'langs' => 'produktverwaltung@produktverwaltung', + 'position' => 1100 + $r, + 'enabled' => 'isModEnabled("produktverwaltung")', + 'perms' => '$user->hasRight("produktverwaltung", "read")', + 'target' => '', + 'user' => 2, + ); + + + // Exports profiles provided by this module + $r = 0; + /* BEGIN MODULEBUILDER EXPORT MYOBJECT */ + /* + $langs->load("produktverwaltung@produktverwaltung"); + $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='/produktverwaltung/class/myobject.class.php'; $keyforelement='myobject@produktverwaltung'; + 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='/produktverwaltung/class/myobject.class.php'; $keyforelement='myobjectline@produktverwaltung'; $keyforalias='tl'; + //include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php'; + $keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@produktverwaltung'; + include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php'; + //$keyforselect='myobjectline'; $keyforaliasextra='extraline'; $keyforelement='myobjectline@produktverwaltung'; + //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().'produktverwaltung_myobject as t'; + //$this->export_sql_end[$r] .=' LEFT JOIN '.$this->db->prefix().'produktverwaltung_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("produktverwaltung@produktverwaltung"); + $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().'produktverwaltung_myobject', 'extra' => $this->db->prefix().'produktverwaltung_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='/produktverwaltung/class/myobject.class.php'; $keyforelement='myobject@produktverwaltung'; + include DOL_DOCUMENT_ROOT.'/core/commonfieldsinimport.inc.php'; + $import_extrafield_sample = array(); + $keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@produktverwaltung'; + include DOL_DOCUMENT_ROOT.'/core/extrafieldsinimport.inc.php'; + $this->import_fieldshidden_array[$r] = array('extra.fk_object' => 'lastrowid-'.$this->db->prefix().'produktverwaltung_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('PRODUKTVERWALTUNG_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('PRODUKTVERWALTUNG_MYOBJECT_ADDON')), + 'path'=>"/core/modules/produktverwaltung/".(!getDolGlobalString('PRODUKTVERWALTUNG_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('PRODUKTVERWALTUNG_MYOBJECT_ADDON')).'.php', + 'classobject'=>'MyObject', + 'pathobject'=>'/produktverwaltung/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/', 'produktverwaltung'); + $result = $this->_load_tables('/produktverwaltung/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('produktverwaltung_separator1', "Separator 1", 'separator', 1, 0, 'thirdparty', 0, 0, '', array('options'=>array(1=>1)), 1, '', 1, 0, '', '', 'produktverwaltung@produktverwaltung', 'isModEnabled("produktverwaltung")'); + //$result1=$extrafields->addExtraField('produktverwaltung_myattr1', "New Attr 1 label", 'boolean', 1, 3, 'thirdparty', 0, 0, '', '', 1, '', -1, 0, '', '', 'produktverwaltung@produktverwaltung', 'isModEnabled("produktverwaltung")'); + //$result2=$extrafields->addExtraField('produktverwaltung_myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project', 0, 0, '', '', 1, '', -1, 0, '', '', 'produktverwaltung@produktverwaltung', 'isModEnabled("produktverwaltung")'); + //$result3=$extrafields->addExtraField('produktverwaltung_myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', -1, 0, '', '', 'produktverwaltung@produktverwaltung', 'isModEnabled("produktverwaltung")'); + //$result4=$extrafields->addExtraField('produktverwaltung_myattr4', "New Attr 4 label", 'select', 1, 3, 'thirdparty', 0, 1, '', array('options'=>array('code1'=>'Val1','code2'=>'Val2','code3'=>'Val3')), 1,'', -1, 0, '', '', 'produktverwaltung@produktverwaltung', 'isModEnabled("produktverwaltung")'); + //$result5=$extrafields->addExtraField('produktverwaltung_myattr5', "New Attr 5 label", 'text', 1, 10, 'user', 0, 0, '', '', 1, '', -1, 0, '', '', 'produktverwaltung@produktverwaltung', 'isModEnabled("produktverwaltung")'); + + // Permissions + $this->remove($options); + + $sql = array(); + + // Document templates + $moduledir = dol_sanitizeFileName('produktverwaltung'); + $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); + } +} diff --git a/css/produktverwaltung.css b/css/produktverwaltung.css new file mode 100644 index 0000000..8b811d6 --- /dev/null +++ b/css/produktverwaltung.css @@ -0,0 +1,490 @@ +/* Produktverwaltung - Kategorie-Baumansicht */ + +/* === Toolbar === */ +.pv-toolbar { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + background: var(--colorbacktitle1, #f0f0f0); + border: 1px solid var(--colorborder, #ddd); + border-radius: 4px; + margin-bottom: 10px; + flex-wrap: wrap; +} +.pv-toolbar .pv-search { + margin-left: auto; + display: flex; + align-items: center; + gap: 5px; +} +.pv-toolbar .pv-search input { + padding: 4px 8px; + border: 1px solid var(--colorborder, #ccc); + border-radius: 3px; + min-width: 200px; +} +.pv-toolbar .pv-export-buttons { + display: flex; + gap: 5px; +} + +/* === Ref-Schema Info === */ +.pv-ref-schema { + background: #e8f4fd; + border: 1px solid #b8daff; + border-radius: 4px; + padding: 8px 12px; + margin-bottom: 10px; +} +.pv-ref-schema .pv-schema-header { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-weight: bold; + color: #004085; +} +.pv-ref-schema .pv-schema-header .fas { + transition: transform 0.2s; +} +.pv-ref-schema.collapsed .pv-schema-header .fa-chevron-down { + transform: rotate(-90deg); +} +.pv-ref-schema .pv-schema-body { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid #b8daff; +} +.pv-ref-schema.collapsed .pv-schema-body { + display: none; +} +.pv-ref-schema code { + background: #fff; + padding: 2px 6px; + border-radius: 3px; + font-weight: bold; + color: #333; +} + +/* === Baumstruktur === */ +.pv-tree { + list-style: none; + padding: 0; + margin: 0; +} +.pv-tree ul { + list-style: none; + padding-left: 24px; + margin: 0; +} +.pv-tree li { + margin: 0; + padding: 0; +} +.pv-category-header { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 8px; + cursor: pointer; + border-radius: 3px; + user-select: none; + font-weight: 600; + font-size: 0.95em; +} +.pv-category-header:hover { + background: var(--colorbacklinepairhover, #f5f5f5); +} +.pv-category-header .pv-toggle { + display: inline-flex; + width: 16px; + justify-content: center; + color: #666; + transition: transform 0.15s; +} +.pv-category-header .pv-toggle.collapsed { + transform: rotate(-90deg); +} +.pv-category-header .pv-cat-label { + flex: 1; +} +.pv-category-header .pv-cat-color { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + border: 1px solid rgba(0,0,0,0.2); +} +.pv-category-header .pv-cat-count { + color: #888; + font-weight: normal; + font-size: 0.85em; +} +.pv-category-header .pv-cat-actions { + display: none; + margin-left: 8px; +} +.pv-category-header:hover .pv-cat-actions { + display: inline-flex; + gap: 2px; +} +.pv-cat-actions button { + padding: 1px 4px; + background: none; + border: none; + cursor: pointer; + opacity: 0.5; + transition: opacity 0.15s; +} +.pv-cat-actions button:hover { + opacity: 1; +} +.pv-category-children { + /* animated via JS */ +} +.pv-category-children.collapsed { + display: none; +} + +/* === Produkttabelle === */ +.pv-products { + margin: 2px 0 6px 22px; + border-collapse: collapse; + width: calc(100% - 22px); + font-size: 0.9em; +} +.pv-products th { + background: var(--colorbacktitle1, #f8f8f8); + padding: 4px 8px; + text-align: left; + font-weight: 600; + font-size: 0.85em; + color: #555; + border-bottom: 2px solid var(--colorborder, #ddd); + white-space: nowrap; +} +.pv-products td { + padding: 3px 8px; + border-bottom: 1px solid var(--colorborder, #eee); + vertical-align: middle; +} +.pv-products tr:hover td { + background: var(--colorbacklinepairhover, #f9f9f9); +} +.pv-products .pv-col-status { + width: 40px; + text-align: center; + white-space: nowrap; +} +.pv-products .pv-col-status .fas { + font-size: 0.75em; + margin: 0 1px; +} +.pv-status-on { + color: #27ae60; +} +.pv-status-off { + color: #ccc; +} +.pv-products .pv-col-ref { + width: 160px; + font-family: monospace; + font-weight: 600; +} +.pv-products .pv-col-label { + /* flex */ +} +.pv-products .pv-col-desc { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.85em; + color: #666; +} +.pv-products .pv-col-price { + width: 80px; + text-align: right; + white-space: nowrap; +} +.pv-products .pv-col-margin { + width: 60px; + text-align: right; + white-space: nowrap; +} +.pv-supplier-tag { + display: inline-block; + font-size: 0.7em; + color: #fff; + background: #6c757d; + padding: 1px 4px; + border-radius: 3px; + font-weight: 600; + font-family: sans-serif; + text-transform: uppercase; + vertical-align: middle; + line-height: 1.4; + margin-left: 3px; +} +.pv-calc-margin { + font-weight: 600; +} +.pv-margin-ok { + color: #27ae60; +} +.pv-margin-warn { + color: #e74c3c; +} +.pv-products .pv-col-actions { + width: 110px; + text-align: center; + white-space: nowrap; +} +.pv-products .pv-col-actions a, +.pv-products .pv-col-actions button { + padding: 2px 4px; + background: none; + border: none; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.15s; +} +.pv-products .pv-col-actions a:hover, +.pv-products .pv-col-actions button:hover { + opacity: 1; +} + +/* === Inline Editing === */ +.pv-editable { + cursor: pointer; + padding: 2px 4px; + border-radius: 2px; + border: 1px solid transparent; + transition: border-color 0.15s, background 0.15s; +} +.pv-editable:hover { + border-color: var(--colorborder, #ccc); + background: #fff; +} +.pv-edit-input { + width: 100%; + padding: 2px 4px; + border: 2px solid #4a90d9; + border-radius: 2px; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + outline: none; + box-sizing: border-box; +} +.pv-col-ref .pv-edit-input { + font-family: monospace; + font-weight: 600; + text-transform: uppercase; +} +.pv-saving { + opacity: 0.5; + pointer-events: none; +} + +/* === Ohne Kategorie Sektion === */ +.pv-no-category { + margin-top: 15px; + border: 2px solid #f0ad4e; + border-radius: 4px; + overflow: hidden; +} +.pv-no-category-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #fcf8e3; + border-bottom: 1px solid #f0ad4e; + cursor: pointer; + font-weight: 600; + color: #8a6d3b; +} +.pv-no-category-header:hover { + background: #faf2cc; +} +.pv-no-category-body { + padding: 0; +} +.pv-no-category-body.collapsed { + display: none; +} +.pv-no-category .pv-products { + margin: 0; + width: 100%; +} + +/* === Kategorie-Zuweisungs-Dialog === */ +.pv-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.4); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; +} +.pv-dialog { + background: #fff; + border-radius: 6px; + padding: 20px; + min-width: 350px; + max-width: 500px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} +.pv-dialog h3 { + margin: 0 0 15px 0; + font-size: 1.1em; +} +.pv-dialog select { + width: 100%; + padding: 6px 8px; + margin-bottom: 15px; +} +.pv-dialog .pv-dialog-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +/* === Produkt-Bearbeitungs-Dialog === */ +.pv-edit-dialog { + min-width: 450px; + max-width: 550px; +} +.pv-edit-dialog h3 { + display: flex; + align-items: center; + gap: 8px; + color: #333; +} +.pv-edit-form { + margin-bottom: 18px; +} +.pv-form-row { + margin-bottom: 12px; +} +.pv-form-row label { + display: block; + font-weight: 600; + font-size: 0.85em; + color: #555; + margin-bottom: 3px; +} +.pv-form-row input { + width: 100%; + padding: 7px 10px; + border: 1px solid var(--colorborder, #ccc); + border-radius: 3px; + font-size: 0.95em; + box-sizing: border-box; + transition: border-color 0.15s; +} +.pv-form-row input:focus { + border-color: #4a90d9; + outline: none; + box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.15); +} +.pv-form-row-half { + display: flex; + gap: 12px; +} +.pv-form-row-half > div { + flex: 1; +} +.pv-price-input { + text-align: right; + font-family: monospace; +} +.pv-dialog-buttons { + display: flex; + align-items: center; + gap: 8px; +} +.pv-dialog-spacer { + flex: 1; +} +.pv-btn-card { + font-size: 0.85em; + opacity: 0.8; +} +.pv-btn-card:hover { + opacity: 1; +} +.pv-btn-save:disabled { + opacity: 0.6; + cursor: wait; +} + +/* === Farb-Swatches === */ +.pv-color-swatches { + display: flex; + flex-wrap: wrap; + gap: 4px; + max-width: 200px; +} +.pv-color-swatch { + display: inline-block; + width: 24px; + height: 24px; + border-radius: 4px; + cursor: pointer; + border: 2px solid transparent; + transition: transform 0.1s, border-color 0.1s; +} +.pv-color-swatch:hover { + transform: scale(1.15); + border-color: #333; +} +.pv-color-swatch.selected { + border-color: #333; + box-shadow: 0 0 0 2px #fff, 0 0 0 4px #333; +} + +/* === Toast / Benachrichtigung === */ +.pv-toast { + position: fixed; + bottom: 20px; + right: 20px; + padding: 10px 20px; + border-radius: 4px; + color: #fff; + font-weight: 600; + z-index: 10001; + animation: pv-fadeIn 0.3s ease; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); +} +.pv-toast.success { + background: #27ae60; +} +.pv-toast.error { + background: #e74c3c; +} +@keyframes pv-fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* === Responsive === */ +@media (max-width: 768px) { + .pv-toolbar { + flex-direction: column; + align-items: stretch; + } + .pv-toolbar .pv-search { + margin-left: 0; + } + .pv-products .pv-col-price, + .pv-products .pv-col-margin { + display: none; + } +} diff --git a/export.php b/export.php new file mode 100644 index 0000000..a4bd2d0 --- /dev/null +++ b/export.php @@ -0,0 +1,303 @@ + + * + * 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 produktverwaltung/export.php + * \brief Export Kategorie-Baum als CSV oder PDF + */ + +// 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.'/categories/class/categorie.class.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/functions.lib.php'; + +$langs->loadLangs(array("produktverwaltung@produktverwaltung", "products")); + +// Security +if (!$user->hasRight('produktverwaltung', 'export')) { + accessforbidden(); +} + +$format = GETPOST('format', 'alpha'); + +// Check if preisbot_margin extrafield exists +$hasMarginField = false; +$sql = "SELECT name FROM ".MAIN_DB_PREFIX."extrafields WHERE elementtype = 'product' AND name = 'preisbot_margin'"; +$resql = $db->query($sql); +if ($resql && $db->num_rows($resql) > 0) { + $hasMarginField = true; +} + +// Load category tree +$categorie = new Categorie($db); +$fullTree = $categorie->get_full_arbo(Categorie::TYPE_PRODUCT); + +$catChildren = array(); +$catData = array(); +if (is_array($fullTree)) { + foreach ($fullTree as $cat) { + $catData[$cat['id']] = $cat; + $parentId = isset($cat['fk_parent']) ? (int) $cat['fk_parent'] : 0; + if (!isset($catChildren[$parentId])) { + $catChildren[$parentId] = array(); + } + $catChildren[$parentId][] = $cat; + } +} + +// Load products per category +$productsPerCat = array(); +$sql = "SELECT p.rowid, p.ref, p.label, p.price, p.cost_price, p.pmp"; +if ($hasMarginField) { + $sql .= ", pe.preisbot_margin"; +} +$sql .= ", cp.fk_categorie"; +$sql .= " FROM ".MAIN_DB_PREFIX."product as p"; +$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."categorie_product as cp ON cp.fk_product = p.rowid"; +if ($hasMarginField) { + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_extrafields as pe ON pe.fk_object = p.rowid"; +} +$sql .= " WHERE p.entity IN (".getEntity('product').")"; +$sql .= " ORDER BY p.ref ASC"; + +$resql = $db->query($sql); +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + if (!empty($obj->fk_categorie)) { + $catId = (int) $obj->fk_categorie; + if (!isset($productsPerCat[$catId])) { + $productsPerCat[$catId] = array(); + } + $productsPerCat[$catId][$obj->rowid] = array( + 'ref' => $obj->ref, + 'label' => $obj->label, + 'cost_price' => !empty($obj->cost_price) ? $obj->cost_price : $obj->pmp, + 'sell_price' => $obj->price, + 'margin' => $hasMarginField ? $obj->preisbot_margin : null, + ); + } + } +} + +// Products without category +$productsWithoutCat = array(); +$sql2 = "SELECT p.rowid, p.ref, p.label, p.price, p.cost_price, p.pmp"; +if ($hasMarginField) { + $sql2 .= ", pe.preisbot_margin"; +} +$sql2 .= " FROM ".MAIN_DB_PREFIX."product as p"; +if ($hasMarginField) { + $sql2 .= " LEFT JOIN ".MAIN_DB_PREFIX."product_extrafields as pe ON pe.fk_object = p.rowid"; +} +$sql2 .= " WHERE p.entity IN (".getEntity('product').")"; +$sql2 .= " AND p.rowid NOT IN (SELECT fk_product FROM ".MAIN_DB_PREFIX."categorie_product)"; +$sql2 .= " ORDER BY p.ref ASC"; + +$resql2 = $db->query($sql2); +if ($resql2) { + while ($obj = $db->fetch_object($resql2)) { + $productsWithoutCat[$obj->rowid] = array( + 'ref' => $obj->ref, + 'label' => $obj->label, + 'cost_price' => !empty($obj->cost_price) ? $obj->cost_price : $obj->pmp, + 'sell_price' => $obj->price, + 'margin' => $hasMarginField ? $obj->preisbot_margin : null, + ); + } +} + +// Collect all rows for export +$exportRows = array(); +collectExportRows(0, $catChildren, $catData, $productsPerCat, $hasMarginField, $exportRows, 0); + +// Add without-category products +if (!empty($productsWithoutCat)) { + $exportRows[] = array('type' => 'category', 'level' => 0, 'label' => '*** '.$langs->trans("ProductsWithoutCategory").' ***', 'count' => count($productsWithoutCat)); + foreach ($productsWithoutCat as $prod) { + $row = array('type' => 'product', 'level' => 1); + $row = array_merge($row, $prod); + $exportRows[] = $row; + } +} + + +if ($format === 'csv') { + exportCSV($exportRows, $hasMarginField, $langs); +} elseif ($format === 'pdf') { + exportPDF($exportRows, $hasMarginField, $langs, $conf); +} else { + header('Location: '.dol_buildpath('/produktverwaltung/produktverwaltungindex.php', 1)); + exit; +} + +$db->close(); + + +// ========== Functions ========== + +function collectExportRows($parentId, &$catChildren, &$catData, &$productsPerCat, $hasMarginField, &$rows, $level) +{ + if (!isset($catChildren[$parentId])) { + return; + } + + foreach ($catChildren[$parentId] as $cat) { + $catId = (int) $cat['id']; + $products = isset($productsPerCat[$catId]) ? $productsPerCat[$catId] : array(); + + $rows[] = array('type' => 'category', 'level' => $level, 'label' => $cat['label'], 'count' => count($products)); + + foreach ($products as $prod) { + $row = array('type' => 'product', 'level' => $level + 1); + $row = array_merge($row, $prod); + $rows[] = $row; + } + + if (isset($catChildren[$catId])) { + collectExportRows($catId, $catChildren, $catData, $productsPerCat, $hasMarginField, $rows, $level + 1); + } + } +} + +function exportCSV($rows, $hasMarginField, $langs) +{ + $filename = 'produktkatalog_'.date('Y-m-d').'.csv'; + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename="'.$filename.'"'); + + $output = fopen('php://output', 'w'); + + // BOM for Excel UTF-8 + fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF)); + + // Header + $header = array('Typ', 'Ebene', 'Kategorie/Ref', 'Bezeichnung', 'EK netto', 'VK netto'); + if ($hasMarginField) { + $header[] = 'Gewinnaufschlag %'; + } + fputcsv($output, $header, ';'); + + foreach ($rows as $row) { + $indent = str_repeat(' ', $row['level']); + if ($row['type'] === 'category') { + $line = array('Kategorie', $row['level'], $indent.$row['label'], '('.$row['count'].' Produkte)', '', ''); + if ($hasMarginField) { + $line[] = ''; + } + } else { + $line = array( + 'Produkt', + $row['level'], + $indent.$row['ref'], + $row['label'], + isset($row['cost_price']) && $row['cost_price'] > 0 ? number_format($row['cost_price'], 2, ',', '.') : '', + isset($row['sell_price']) && $row['sell_price'] > 0 ? number_format($row['sell_price'], 2, ',', '.') : '', + ); + if ($hasMarginField) { + $line[] = !empty($row['margin']) ? number_format($row['margin'], 1, ',', '.') : ''; + } + } + fputcsv($output, $line, ';'); + } + + fclose($output); + exit; +} + +function exportPDF($rows, $hasMarginField, $langs, $conf) +{ + require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php'; + + $pdf = pdf_getInstance('A4', 'mm', 'L'); // Landscape + $pdf->SetCreator('Dolibarr - Produktverwaltung'); + $pdf->SetTitle($langs->trans('ExportTitle')); + $pdf->SetAutoPageBreak(true, 15); + $pdf->AddPage(); + + // Title + $pdf->SetFont('', 'B', 16); + $pdf->Cell(0, 10, $langs->trans('ExportTitle').' - '.date('d.m.Y'), 0, 1, 'C'); + $pdf->Ln(5); + + // Column widths (Landscape A4 = 277mm usable) + $colWidths = array(60, 100, 35, 35); + $headers = array($langs->trans('Ref'), $langs->trans('Label'), $langs->trans('BuyingPriceNet'), $langs->trans('SellingPriceNet')); + if ($hasMarginField) { + $colWidths[] = 25; + $headers[] = 'GA%'; + } + + // Table header + $pdf->SetFont('', 'B', 8); + $pdf->SetFillColor(230, 230, 230); + + foreach ($rows as $row) { + // Check page break + if ($pdf->GetY() > 185) { + $pdf->AddPage(); + } + + if ($row['type'] === 'category') { + $indent = $row['level'] * 4; + $pdf->SetFont('', 'B', 9 - min($row['level'], 2)); + $label = str_repeat(' ', $row['level']).$row['label']; + if ($row['count'] > 0) { + $label .= ' ('.$row['count'].')'; + } + $pdf->SetFillColor(240 - ($row['level'] * 15), 240 - ($row['level'] * 10), 255 - ($row['level'] * 5)); + $totalWidth = array_sum($colWidths); + $pdf->Cell($totalWidth, 6, $label, 0, 1, 'L', true); + } else { + $pdf->SetFont('', '', 7); + $indent = $row['level'] * 4; + $pdf->Cell($colWidths[0], 5, str_repeat(' ', $row['level'] * 2).$row['ref'], 0, 0, 'L'); + $pdf->Cell($colWidths[1], 5, dol_trunc($row['label'], 55), 0, 0, 'L'); + $costStr = isset($row['cost_price']) && $row['cost_price'] > 0 ? number_format($row['cost_price'], 2, ',', '.') : '-'; + $sellStr = isset($row['sell_price']) && $row['sell_price'] > 0 ? number_format($row['sell_price'], 2, ',', '.') : '-'; + $pdf->Cell($colWidths[2], 5, $costStr, 0, 0, 'R'); + $pdf->Cell($colWidths[3], 5, $sellStr, 0, 0, 'R'); + if ($hasMarginField) { + $marginStr = !empty($row['margin']) ? number_format($row['margin'], 1, ',', '.').'%' : '-'; + $pdf->Cell($colWidths[4], 5, $marginStr, 0, 0, 'R'); + } + $pdf->Ln(); + } + } + + $filename = 'produktkatalog_'.date('Y-m-d').'.pdf'; + $pdf->Output($filename, 'D'); + exit; +} diff --git a/img/README.md b/img/README.md new file mode 100644 index 0000000..3d818ab --- /dev/null +++ b/img/README.md @@ -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 'produktverwaltung.png@produktverwaltung', you can put into this +directory a .png file called *object_produktverwaltung.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@produktverwaltung', then you can put into this +directory a .png file called *object_myobject.png* (16x16 or 32x32 pixels) + diff --git a/js/produktverwaltung.js b/js/produktverwaltung.js new file mode 100644 index 0000000..5295300 --- /dev/null +++ b/js/produktverwaltung.js @@ -0,0 +1,759 @@ +/** + * Produktverwaltung - JavaScript + * Baum auf-/zuklappen, Inline-Editing, AJAX-Aktionen + */ +document.addEventListener('DOMContentLoaded', function() { + 'use strict'; + + // Only init on produktverwaltung pages + if (!document.querySelector('.pv-tree, .pv-no-category')) return; + + var pvAjaxUrl = pvConfig ? pvConfig.ajaxUrl : ''; + var pvToken = pvConfig ? pvConfig.token : ''; + var pvHasMargin = pvConfig ? pvConfig.hasMargin : false; + + // === Toast Notification === + function showToast(message, type) { + var toast = document.createElement('div'); + toast.className = 'pv-toast ' + (type || 'success'); + toast.textContent = message; + document.body.appendChild(toast); + setTimeout(function() { + toast.style.opacity = '0'; + toast.style.transition = 'opacity 0.3s'; + setTimeout(function() { toast.remove(); }, 300); + }, 2500); + } + + // === Baum auf-/zuklappen === + function toggleCategory(header) { + var toggle = header.querySelector('.pv-toggle'); + var children = header.nextElementSibling; + if (!children) return; + + if (children.classList.contains('collapsed')) { + children.classList.remove('collapsed'); + if (toggle) toggle.classList.remove('collapsed'); + } else { + children.classList.add('collapsed'); + if (toggle) toggle.classList.add('collapsed'); + } + } + + // Alle aufklappen + window.pvExpandAll = function() { + document.querySelectorAll('.pv-category-children.collapsed').forEach(function(el) { + el.classList.remove('collapsed'); + }); + document.querySelectorAll('.pv-toggle.collapsed').forEach(function(el) { + el.classList.remove('collapsed'); + }); + // Also expand no-category section + var ncBody = document.querySelector('.pv-no-category-body'); + if (ncBody) ncBody.classList.remove('collapsed'); + }; + + // Alle zuklappen + window.pvCollapseAll = function() { + document.querySelectorAll('.pv-category-children').forEach(function(el) { + el.classList.add('collapsed'); + }); + document.querySelectorAll('.pv-toggle').forEach(function(el) { + el.classList.add('collapsed'); + }); + }; + + // Category header click + document.querySelectorAll('.pv-category-header').forEach(function(header) { + header.addEventListener('click', function(e) { + if (e.target.tagName === 'A' || e.target.tagName === 'BUTTON') return; + toggleCategory(header); + }); + }); + + // No-category header toggle + var ncHeader = document.querySelector('.pv-no-category-header'); + if (ncHeader) { + ncHeader.addEventListener('click', function() { + var body = document.querySelector('.pv-no-category-body'); + if (body) body.classList.toggle('collapsed'); + }); + } + + // === Ref-Schema toggle === + var schemaBox = document.querySelector('.pv-ref-schema'); + if (schemaBox) { + var schemaHeader = schemaBox.querySelector('.pv-schema-header'); + if (schemaHeader) { + schemaHeader.addEventListener('click', function() { + schemaBox.classList.toggle('collapsed'); + }); + } + } + + // === Suche / Filter === + var searchInput = document.querySelector('.pv-search input'); + if (searchInput) { + var searchTimeout; + searchInput.addEventListener('input', function() { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(function() { + filterTree(searchInput.value.toLowerCase().trim()); + }, 250); + }); + } + + function filterTree(query) { + if (!query) { + // Show all + document.querySelectorAll('.pv-tree li, .pv-products tr').forEach(function(el) { + el.style.display = ''; + }); + document.querySelectorAll('.pv-category-children').forEach(function(el) { + el.classList.add('collapsed'); + }); + document.querySelectorAll('.pv-toggle').forEach(function(el) { + el.classList.add('collapsed'); + }); + return; + } + + // Search in product rows + var anyMatch = false; + document.querySelectorAll('.pv-products tbody tr').forEach(function(row) { + var ref = (row.querySelector('.pv-col-ref') || {}).textContent || ''; + var label = (row.querySelector('.pv-col-label') || {}).textContent || ''; + var match = ref.toLowerCase().indexOf(query) >= 0 || label.toLowerCase().indexOf(query) >= 0; + row.style.display = match ? '' : 'none'; + if (match) anyMatch = true; + }); + + // Expand categories that have visible products + document.querySelectorAll('.pv-tree li').forEach(function(li) { + var table = li.querySelector('.pv-products'); + var hasVisible = false; + if (table) { + table.querySelectorAll('tbody tr').forEach(function(row) { + if (row.style.display !== 'none') hasVisible = true; + }); + } + // Check child categories too + var childLis = li.querySelectorAll(':scope > .pv-category-children > li'); + childLis.forEach(function(child) { + if (child.style.display !== 'none') hasVisible = true; + }); + + if (hasVisible) { + li.style.display = ''; + var children = li.querySelector('.pv-category-children'); + if (children) children.classList.remove('collapsed'); + var toggle = li.querySelector('.pv-toggle'); + if (toggle) toggle.classList.remove('collapsed'); + } else { + // Check if category name matches + var catLabel = li.querySelector('.pv-cat-label'); + if (catLabel && catLabel.textContent.toLowerCase().indexOf(query) >= 0) { + li.style.display = ''; + hasVisible = true; + } else { + li.style.display = hasVisible ? '' : 'none'; + } + } + }); + + // Also search in no-category section + var ncBody = document.querySelector('.pv-no-category-body'); + if (ncBody) { + var ncMatch = false; + ncBody.querySelectorAll('tbody tr').forEach(function(row) { + var ref = (row.querySelector('.pv-col-ref') || {}).textContent || ''; + var label = (row.querySelector('.pv-col-label') || {}).textContent || ''; + var match = ref.toLowerCase().indexOf(query) >= 0 || label.toLowerCase().indexOf(query) >= 0; + row.style.display = match ? '' : 'none'; + if (match) ncMatch = true; + }); + if (ncMatch) ncBody.classList.remove('collapsed'); + } + } + + // === Inline Editing === + document.querySelectorAll('.pv-editable').forEach(function(el) { + el.addEventListener('dblclick', function(e) { + e.stopPropagation(); + startEditing(el); + }); + }); + + function startEditing(el) { + if (el.querySelector('.pv-edit-input')) return; // Already editing + + var field = el.dataset.field; // 'ref', 'label', or 'description' + var productId = el.dataset.productId; + var currentValue = el.textContent.trim(); + + var input = document.createElement('input'); + input.type = 'text'; + input.className = 'pv-edit-input'; + input.value = currentValue; + if (field === 'ref') { + input.style.textTransform = 'uppercase'; + } + + el.textContent = ''; + el.appendChild(input); + input.focus(); + input.select(); + + function save() { + var newValue = input.value.trim(); + if (field === 'ref') { + newValue = newValue.toUpperCase(); + } + + if (newValue === currentValue || newValue === '') { + cancel(); + return; + } + + el.classList.add('pv-saving'); + ajaxCall('update_' + field, { + product_id: productId, + value: newValue + }, function(response) { + el.classList.remove('pv-saving'); + if (response.success) { + el.textContent = newValue; + showToast(pvLang.saveSuccess || 'Gespeichert', 'success'); + } else { + el.textContent = currentValue; + showToast(response.error || pvLang.saveError || 'Fehler', 'error'); + } + }, function() { + el.classList.remove('pv-saving'); + el.textContent = currentValue; + showToast(pvLang.saveError || 'Fehler beim Speichern', 'error'); + }); + } + + function cancel() { + el.textContent = currentValue; + } + + input.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + save(); + } else if (e.key === 'Escape') { + cancel(); + } + }); + + input.addEventListener('blur', function() { + // Small delay to allow click events + setTimeout(function() { + if (el.contains(input)) { + save(); + } + }, 100); + }); + } + + // === Produkt lâschen === + window.pvDeleteProduct = function(productId, productRef) { + var msg = (pvLang.confirmDelete || 'Produkt "%s" wirklich lâschen?').replace('%s', productRef); + if (!confirm(msg)) return; + + ajaxCall('delete_product', { product_id: productId }, function(response) { + if (response.success) { + // Remove row from table + var row = document.querySelector('tr[data-product-id="' + productId + '"]'); + if (row) row.remove(); + showToast(pvLang.productDeleted || 'Produkt gelâscht', 'success'); + } else { + showToast(response.error || pvLang.deleteError || 'Fehler', 'error'); + } + }); + }; + + // === Kategorie zuweisen === + window.pvAssignCategory = function(productId, productRef) { + // Load categories via AJAX + ajaxCall('get_categories', {}, function(response) { + if (!response.success || !response.categories) { + showToast(response.error || 'Fehler beim Laden der Kategorien', 'error'); + return; + } + showCategoryDialog(productId, productRef, response.categories); + }, function() { + showToast('AJAX-Fehler beim Laden der Kategorien', 'error'); + }); + }; + + function showCategoryDialog(productId, productRef, categories) { + var overlay = document.createElement('div'); + overlay.className = 'pv-dialog-overlay'; + + var dialog = document.createElement('div'); + dialog.className = 'pv-dialog'; + dialog.innerHTML = '

' + (pvLang.selectCategory || 'Kategorie zuweisen') + '

' + + '

' + productRef + '

' + + '' + + '
' + + '' + + '' + + '
'; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + // Fill select + var select = document.getElementById('pv-cat-select'); + categories.forEach(function(cat) { + var option = document.createElement('option'); + option.value = cat.id; + option.textContent = cat.fullpath; + select.appendChild(option); + }); + + // Assign button + document.getElementById('pv-cat-assign-btn').addEventListener('click', function() { + var catId = select.value; + if (!catId) return; + + ajaxCall('add_to_category', { + product_id: productId, + category_id: catId + }, function(response) { + overlay.remove(); + if (response.success) { + showToast(pvLang.categoryAssigned || 'Kategorie zugewiesen', 'success'); + // Reload page to reflect changes + setTimeout(function() { location.reload(); }, 800); + } else { + showToast(response.error || 'Fehler', 'error'); + } + }); + }); + + // Close on overlay click + overlay.addEventListener('click', function(e) { + if (e.target === overlay) overlay.remove(); + }); + } + + // === Aus Kategorie entfernen === + window.pvRemoveFromCategory = function(productId, categoryId) { + ajaxCall('remove_from_category', { + product_id: productId, + category_id: categoryId + }, function(response) { + if (response.success) { + showToast(pvLang.categoryRemoved || 'Aus Kategorie entfernt', 'success'); + setTimeout(function() { location.reload(); }, 800); + } else { + showToast(response.error || 'Fehler', 'error'); + } + }); + }; + + // === Produkt bearbeiten (Dialog) === + window.pvEditProduct = function(productId) { + // Fetch current data from server + ajaxCall('get_product', { product_id: productId }, function(response) { + if (!response.success || !response.product) { + showToast(response.error || 'Fehler', 'error'); + return; + } + showEditDialog(response.product); + }); + }; + + function showEditDialog(product) { + var overlay = document.createElement('div'); + overlay.className = 'pv-dialog-overlay'; + + var doliUrl = (typeof pvDolibarrUrl !== 'undefined' ? pvDolibarrUrl : '') + '/product/card.php?id=' + product.id; + + var dialog = document.createElement('div'); + dialog.className = 'pv-dialog pv-edit-dialog'; + dialog.innerHTML = + '

' + (pvLang.editProduct || 'Produkt bearbeiten') + '

' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + ' ' + (pvLang.openProductCard || 'Produktkarte') + + '' + + '' + + '' + + '' + + '
'; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + // Focus ref field + var refInput = document.getElementById('pv-edit-ref'); + refInput.focus(); + refInput.select(); + + // Save handler + var saveBtn = dialog.querySelector('.pv-btn-save'); + saveBtn.addEventListener('click', function() { + saveProductEdit(product.id, overlay); + }); + + // Cancel handler + dialog.querySelector('.pv-btn-cancel').addEventListener('click', function() { + overlay.remove(); + }); + + // Close on overlay click + overlay.addEventListener('click', function(e) { + if (e.target === overlay) overlay.remove(); + }); + + // Enter to save, Escape to close + dialog.addEventListener('keydown', function(e) { + if (e.key === 'Enter' && e.target.tagName === 'INPUT') { + e.preventDefault(); + saveProductEdit(product.id, overlay); + } else if (e.key === 'Escape') { + overlay.remove(); + } + }); + } + + function saveProductEdit(productId, overlay) { + var saveBtn = overlay.querySelector('.pv-btn-save'); + var origText = saveBtn.textContent; + saveBtn.textContent = pvLang.saving || 'Speichert...'; + saveBtn.disabled = true; + + var data = { + product_id: productId, + new_ref: document.getElementById('pv-edit-ref').value.trim(), + new_label: document.getElementById('pv-edit-label').value.trim(), + new_description: document.getElementById('pv-edit-description').value.trim(), + new_sell_price: parsePrice(document.getElementById('pv-edit-sell-price').value) + }; + + ajaxCall('update_product', data, function(response) { + if (response.success && response.product) { + overlay.remove(); + showToast(pvLang.saveSuccess || 'Gespeichert', 'success'); + updateProductRow(response.product); + } else { + saveBtn.textContent = origText; + saveBtn.disabled = false; + showToast(response.error || pvLang.saveError || 'Fehler', 'error'); + } + }, function() { + saveBtn.textContent = origText; + saveBtn.disabled = false; + showToast(pvLang.saveError || 'Fehler beim Speichern', 'error'); + }); + } + + function updateProductRow(product) { + var row = document.querySelector('tr[data-product-id="' + product.id + '"]'); + if (!row) return; + + // Update ref + var refEl = row.querySelector('.pv-col-ref .pv-editable'); + if (refEl) { + refEl.textContent = product.ref; + } else { + var refTd = row.querySelector('.pv-col-ref'); + if (refTd) refTd.textContent = product.ref; + } + + // Update label + var labelEl = row.querySelector('.pv-col-label .pv-editable'); + if (labelEl) { + labelEl.textContent = product.label; + } else { + var labelTd = row.querySelector('.pv-col-label'); + if (labelTd) labelTd.textContent = product.label; + } + + // Update VK price in the row (2nd price cell = VK) + var priceCells = row.querySelectorAll('.pv-col-price'); + if (priceCells.length >= 2) { + priceCells[1].textContent = product.sell_price > 0 ? formatPrice(product.sell_price) : '-'; + } + + // Update data attributes + row.setAttribute('data-sell-price', product.sell_price || ''); + + // Recalculate margin with best buy price from data attribute + var bestBuy = parseFloat(row.getAttribute('data-best-buy-price')); + var calcMarginEl = row.querySelector('.pv-calc-margin'); + if (calcMarginEl && bestBuy > 0 && product.sell_price > 0) { + var cm = ((product.sell_price - bestBuy) / bestBuy * 100).toFixed(1); + calcMarginEl.textContent = formatPrice(cm) + '%'; + } + } + + // Helper: parse price input (German/international format) + function parsePrice(val) { + if (!val) return ''; + val = val.replace(/\s/g, '').replace(/\./g, '').replace(',', '.'); + return val; + } + + // Helper: format number for display in input + function formatNum(val) { + if (!val || val == 0) return ''; + return parseFloat(val).toFixed(2).replace('.', ','); + } + + // Helper: format price for table display + function formatPrice(val) { + if (!val || val == 0) return '-'; + return parseFloat(val).toFixed(2).replace('.', ','); + } + + // Helper: escape HTML + function escHtml(str) { + if (!str) return ''; + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + + // === Produkt-Fenster (wiederverwendbar) === + var pvProductWindow = null; + window.pvOpenProductWindow = function(url) { + if (pvProductWindow && !pvProductWindow.closed) { + pvProductWindow.location.href = url; + pvProductWindow.focus(); + } else { + pvProductWindow = window.open(url, 'pvProductCard', 'width=1100,height=800,scrollbars=yes,resizable=yes'); + } + return false; + }; + + // === Kategorie bearbeiten === + window.pvEditCategory = function(categoryId) { + ajaxCall('get_category_data', { category_id: categoryId }, function(response) { + if (!response.success || !response.category) { + showToast(response.error || 'Fehler', 'error'); + return; + } + showCategoryEditDialog(response.category, false); + }, function() { + showToast('Fehler beim Laden der Kategorie', 'error'); + }); + }; + + // === Kategorie hinzufΓΌgen === + window.pvAddCategory = function(parentId) { + showCategoryEditDialog({ + id: 0, + label: '', + description: '', + color: '', + fk_parent: parentId || 0 + }, true); + }; + + // === Kategorie lΓΆschen === + window.pvDeleteCategory = function(categoryId, categoryLabel) { + var msg = (pvLang.deleteCategory || 'Kategorie "%s" wirklich lΓΆschen?').replace('%s', categoryLabel); + if (!confirm(msg)) return; + + ajaxCall('delete_category', { category_id: categoryId }, function(response) { + if (response.success) { + showToast(pvLang.categoryDeleted || 'Kategorie gelΓΆscht', 'success'); + setTimeout(function() { location.reload(); }, 800); + } else { + showToast(response.error || 'Fehler', 'error'); + } + }); + }; + + function showCategoryEditDialog(category, isNew) { + var overlay = document.createElement('div'); + overlay.className = 'pv-dialog-overlay'; + + var title = isNew ? (pvLang.addCategory || 'Kategorie hinzufΓΌgen') : (pvLang.editCategory || 'Kategorie bearbeiten'); + var colorVal = category.color || ''; + + var dialog = document.createElement('div'); + dialog.className = 'pv-dialog pv-edit-dialog'; + dialog.innerHTML = + '

' + escHtml(title) + '

' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '' + + '' + + '' + + '
'; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + // Init color swatches + var presetColors = [ + '', 'e74c3c', 'e91e63', '9b59b6', '8e44ad', + '3498db', '2980b9', '1abc9c', '16a085', '27ae60', + '2ecc71', 'f39c12', 'f1c40f', 'e67e22', 'd35400', + '95a5a6', '7f8c8d', '34495e', '2c3e50', '795548' + ]; + var swatchContainer = document.getElementById('pv-color-swatches'); + var hiddenInput = document.getElementById('pv-cat-edit-color'); + presetColors.forEach(function(c) { + var swatch = document.createElement('span'); + swatch.className = 'pv-color-swatch' + (c === colorVal ? ' selected' : ''); + swatch.style.background = c ? '#' + c : '#fff'; + if (!c) swatch.style.border = '2px solid #ccc'; + swatch.setAttribute('data-color', c); + swatch.title = c ? '#' + c : 'Keine'; + swatch.addEventListener('click', function() { + swatchContainer.querySelectorAll('.pv-color-swatch').forEach(function(s) { s.classList.remove('selected'); }); + swatch.classList.add('selected'); + hiddenInput.value = c; + }); + swatchContainer.appendChild(swatch); + }); + + // Load categories for parent dropdown + ajaxCall('get_categories', {}, function(response) { + var select = document.getElementById('pv-cat-edit-parent'); + var opt = document.createElement('option'); + opt.value = '0'; + opt.textContent = '(' + (pvLang.none || 'Keine') + ')'; + select.appendChild(opt); + + if (response.success && response.categories) { + response.categories.forEach(function(cat) { + // Don't show the category itself as a parent option + if (cat.id == category.id) return; + var o = document.createElement('option'); + o.value = cat.id; + o.textContent = cat.fullpath; + if (cat.id == category.fk_parent) o.selected = true; + select.appendChild(o); + }); + } + }); + + // Focus label field + document.getElementById('pv-cat-edit-label').focus(); + + // Save handler + dialog.querySelector('.pv-btn-save').addEventListener('click', function() { + var label = document.getElementById('pv-cat-edit-label').value.trim(); + if (!label) { + showToast('Bezeichnung ist erforderlich', 'error'); + return; + } + + var colorHex = document.getElementById('pv-cat-edit-color').value || ''; + + var data = { + cat_label: label, + cat_description: document.getElementById('pv-cat-edit-desc').value.trim(), + cat_color: colorHex, + cat_parent: document.getElementById('pv-cat-edit-parent').value + }; + + var action = isNew ? 'create_category' : 'update_category'; + if (!isNew) { + data.category_id = category.id; + } + + var saveBtn = dialog.querySelector('.pv-btn-save'); + saveBtn.textContent = pvLang.saving || 'Speichert...'; + saveBtn.disabled = true; + + ajaxCall(action, data, function(response) { + if (response.success) { + overlay.remove(); + showToast(pvLang.saveSuccess || 'Gespeichert', 'success'); + setTimeout(function() { location.reload(); }, 800); + } else { + saveBtn.textContent = isNew ? (pvLang.create || 'Erstellen') : (pvLang.save || 'Speichern'); + saveBtn.disabled = false; + showToast(response.error || 'Fehler', 'error'); + } + }, function() { + saveBtn.textContent = isNew ? (pvLang.create || 'Erstellen') : (pvLang.save || 'Speichern'); + saveBtn.disabled = false; + showToast('Fehler', 'error'); + }); + }); + + // Cancel / close + dialog.querySelector('.pv-btn-cancel').addEventListener('click', function() { + overlay.remove(); + }); + overlay.addEventListener('click', function(e) { + if (e.target === overlay) overlay.remove(); + }); + dialog.addEventListener('keydown', function(e) { + if (e.key === 'Escape') overlay.remove(); + }); + } + + // === AJAX Helper === + function ajaxCall(action, data, onSuccess, onError) { + var formData = new FormData(); + formData.append('action', action); + formData.append('token', pvToken); + for (var key in data) { + if (data.hasOwnProperty(key)) { + formData.append(key, data[key]); + } + } + + fetch(pvAjaxUrl, { + method: 'POST', + body: formData + }) + .then(function(response) { return response.json(); }) + .then(function(json) { + if (onSuccess) onSuccess(json); + }) + .catch(function(err) { + console.error('PV AJAX Error:', err); + if (onError) onError(err); + }); + } + +}); diff --git a/langs/de_DE/produktverwaltung.lang b/langs/de_DE/produktverwaltung.lang new file mode 100644 index 0000000..3df0381 --- /dev/null +++ b/langs/de_DE/produktverwaltung.lang @@ -0,0 +1,85 @@ +# Produktverwaltung - Deutsche Sprachdatei + +# Modul +ModuleProduktVerwaltungName = Produktverwaltung +ModuleProduktVerwaltungDesc = Kategorie-Baumansicht mit Produktverwaltung und Inline-Bearbeitung + +# Seiten +ProduktVerwaltungArea = Produktverwaltung - Kategorie-Übersicht +KategorieBaum = Kategoriebaum +ProduktVerwaltungSetup = Produktverwaltung Einstellungen +ProduktVerwaltungAbout = Über Produktverwaltung + +# Baum +ExpandAll = Alle aufklappen +CollapseAll = Alle zuklappen +SearchProducts = Produkte suchen... +ProductsWithoutCategory = Produkte ohne Kategorie +NoProductsInCategory = Keine Produkte in dieser Kategorie +ProductCount = %s Produkte + +# Tabelle +Ref = Referenz +Label = Bezeichnung +BuyingPriceNet = EK netto +BestBuyPrice = Best EK +SellingPriceNet = VK netto +MarginPercent = GA%% +Actions = Aktionen + +# Inline-Editing +ClickToEdit = Doppelklick zum Bearbeiten +SaveSuccess = Erfolgreich gespeichert +SaveError = Fehler beim Speichern +RefAlreadyExists = Diese Referenz existiert bereits + +# Aktionen +EditProduct = Produkt bearbeiten +OpenProductCard = Produktkarte âffnen +Saving = Speichert... +EditInDolibarr = In Dolibarr bearbeiten +AssignCategory = Kategorie zuweisen +RemoveFromCategory = Aus Kategorie entfernen +ConfirmDeleteProduct = Sind Sie sicher, dass Sie dieses Produkt lâschen mâchten? +ProductDeleted = Produkt wurde gelâscht +ProductDeleteError = Fehler beim Lâschen des Produkts +CategoryAssigned = Kategorie wurde zugewiesen +CategoryRemoved = Produkt wurde aus Kategorie entfernt +SelectCategory = Kategorie auswÀhlen + +# Export +ExportCSV = CSV Export +ExportPDF = PDF Export +ExportTitle = Produktkatalog - Kategoriebaum + +# Admin / Ref-Schema +RefSchemaTitle = Referenz-Schema +RefSchemaDesc = Schema-Vorlage die über den Ref-Eingabefeldern angezeigt wird +RefSchemaExample = Beispiel +RefSchemaShow = Ref-Schema-Hinweis anzeigen +RefSchemaPattern = Schema-Muster +RefSchemaHelp = Definieren Sie das Referenz-Schema für Produktreferenzen. Dieses wird als Hilfetext über den Eingabefeldern angezeigt. +DefaultExpanded = Kategorien standardmÀßig aufklappen +DefaultExpandedHelp = Wenn aktiviert, werden alle Kategorien beim Seitenaufruf aufgeklappt angezeigt + +# Status +OnSale = Verkaufbar +OnBuy = Beziehbar +CalcMargin = Aufschlag + +# Kategorien +EditCategory = Kategorie bearbeiten +AddCategory = Kategorie hinzufügen +ConfirmDeleteCategory = Kategorie "%s" wirklich lâschen? +CategoryDeleted = Kategorie gelâscht +Color = Farbe +ParentCategory = Übergeordnete Kategorie +None = Keine +Create = Erstellen +Close = Schließen + +# Berechtigungen +Permission50003201 = Produktverwaltung anzeigen +Permission50003202 = Produkte bearbeiten (Ref/Label) +Permission50003203 = Produkte loeschen +Permission50003204 = Produktverwaltung exportieren diff --git a/langs/en_US/produktverwaltung.lang b/langs/en_US/produktverwaltung.lang new file mode 100644 index 0000000..3ebea8e --- /dev/null +++ b/langs/en_US/produktverwaltung.lang @@ -0,0 +1,84 @@ +# Produktverwaltung - English translation + +# Module +ModuleProduktVerwaltungName = Product Management +ModuleProduktVerwaltungDesc = Category tree view with product management and inline editing + +# Pages +ProduktVerwaltungArea = Product Management - Category Overview +KategorieBaum = Category Tree +ProduktVerwaltungSetup = Product Management Settings +ProduktVerwaltungAbout = About Product Management + +# Tree +ExpandAll = Expand all +CollapseAll = Collapse all +SearchProducts = Search products... +ProductsWithoutCategory = Products without category +NoProductsInCategory = No products in this category +ProductCount = %s products + +# Table +Ref = Reference +Label = Description +BuyingPriceNet = Buy net +BestBuyPrice = Best buy +SellingPriceNet = Sell net +MarginPercent = Margin%% +Actions = Actions + +# Inline Editing +ClickToEdit = Double-click to edit +SaveSuccess = Saved successfully +SaveError = Error saving +RefAlreadyExists = This reference already exists + +# Actions +EditProduct = Edit product +OpenProductCard = Open product card +Saving = Saving... +EditInDolibarr = Edit in Dolibarr +AssignCategory = Assign category +RemoveFromCategory = Remove from category +ConfirmDeleteProduct = Are you sure you want to delete this product? +ProductDeleted = Product deleted +ProductDeleteError = Error deleting product +CategoryAssigned = Category assigned +CategoryRemoved = Product removed from category +SelectCategory = Select category + +# Export +ExportCSV = CSV Export +ExportPDF = PDF Export +ExportTitle = Product Catalog - Category Tree + +# Admin / Ref-Schema +RefSchemaTitle = Reference Schema +RefSchemaDesc = Schema template displayed above reference input fields +RefSchemaExample = Example +RefSchemaShow = Show reference schema hint +RefSchemaPattern = Schema Pattern +RefSchemaHelp = Define the reference schema for product references. This is displayed as help text above input fields. +DefaultExpanded = Expand categories by default + +# Status +OnSale = On sale +OnBuy = Purchasable +CalcMargin = Markup + +# Categories +EditCategory = Edit category +AddCategory = Add category +ConfirmDeleteCategory = Are you sure you want to delete category "%s"? +CategoryDeleted = Category deleted +Color = Color +ParentCategory = Parent category +None = None +Create = Create +Close = Close + +# Permissions +Permission50003201 = View product management +Permission50003202 = Edit products (Ref/Label) +Permission50003203 = Delete products +Permission50003204 = Export product management diff --git a/lib/produktverwaltung.lib.php b/lib/produktverwaltung.lib.php new file mode 100644 index 0000000..d9985b2 --- /dev/null +++ b/lib/produktverwaltung.lib.php @@ -0,0 +1,85 @@ + + * + * 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 . + */ + +/** + * \file produktverwaltung/lib/produktverwaltung.lib.php + * \ingroup produktverwaltung + * \brief Library files with common functions for ProduktVerwaltung + */ + +/** + * Prepare admin pages header + * + * @return array + */ +function produktverwaltungAdminPrepareHead() +{ + global $langs, $conf; + + // global $db; + // $extrafields = new ExtraFields($db); + // $extrafields->fetch_name_optionals_label('myobject'); + + $langs->load("produktverwaltung@produktverwaltung"); + + $h = 0; + $head = array(); + + $head[$h][0] = dol_buildpath("/produktverwaltung/admin/setup.php", 1); + $head[$h][1] = $langs->trans("Settings"); + $head[$h][2] = 'settings'; + $h++; + + /* + $head[$h][0] = dol_buildpath("/produktverwaltung/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] .= '' . $nbExtrafields . ''; + } + $head[$h][2] = 'myobject_extrafields'; + $h++; + + $head[$h][0] = dol_buildpath("/produktverwaltung/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] .= '' . $nbExtrafields . ''; + } + $head[$h][2] = 'myobject_extrafieldsline'; + $h++; + */ + + $head[$h][0] = dol_buildpath("/produktverwaltung/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:@produktverwaltung:/produktverwaltung/mypage.php?id=__ID__' + //); // to add new tab + //$this->tabs = array( + // 'entity:-tabname:Title:@produktverwaltung:/produktverwaltung/mypage.php?id=__ID__' + //); // to remove a tab + complete_head_from_modules($conf, $langs, null, $head, $h, 'produktverwaltung@produktverwaltung'); + + complete_head_from_modules($conf, $langs, null, $head, $h, 'produktverwaltung@produktverwaltung', 'remove'); + + return $head; +} diff --git a/modulebuilder.txt b/modulebuilder.txt new file mode 100644 index 0000000..670a177 --- /dev/null +++ b/modulebuilder.txt @@ -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. \ No newline at end of file diff --git a/produktverwaltungindex.php b/produktverwaltungindex.php new file mode 100644 index 0000000..d81aea6 --- /dev/null +++ b/produktverwaltungindex.php @@ -0,0 +1,575 @@ + + * + * 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 produktverwaltung/produktverwaltungindex.php + * \ingroup produktverwaltung + * \brief Kategorie-Baumansicht mit Produkten und Inline-Editing + */ + +// 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.'/categories/class/categorie.class.php'; +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/functions.lib.php'; + +$langs->loadLangs(array("produktverwaltung@produktverwaltung", "products")); + +// Security check +if (!$user->hasRight('produktverwaltung', 'read')) { + accessforbidden(); +} + +$canEdit = $user->hasRight('produktverwaltung', 'write'); +$canDelete = $user->hasRight('produktverwaltung', 'delete'); +$canExport = $user->hasRight('produktverwaltung', 'export'); +$defaultExpanded = getDolGlobalString('PRODUKTVERWALTUNG_DEFAULT_EXPANDED', '0'); + +// Check if preisbot_margin extrafield exists +$hasMarginField = false; +$sql = "SELECT name FROM ".MAIN_DB_PREFIX."extrafields WHERE elementtype = 'product' AND name = 'preisbot_margin'"; +$resql = $db->query($sql); +if ($resql && $db->num_rows($resql) > 0) { + $hasMarginField = true; +} + +// Load category tree +$categorie = new Categorie($db); +$fullTree = $categorie->get_full_arbo(Categorie::TYPE_PRODUCT); + +// Build category hierarchy +$catChildren = array(); // parent_id => array of cat data +$catData = array(); // cat_id => cat data +if (is_array($fullTree)) { + foreach ($fullTree as $cat) { + $catData[$cat['id']] = $cat; + $parentId = isset($cat['fk_parent']) ? (int) $cat['fk_parent'] : 0; + if (!isset($catChildren[$parentId])) { + $catChildren[$parentId] = array(); + } + $catChildren[$parentId][] = $cat; + } +} + +// Load all products with their category assignments +$productsPerCat = array(); // cat_id => array of products +$allProductIds = array(); + +$sql = "SELECT DISTINCT p.rowid, p.ref, p.label, p.description, p.price, p.price_ttc,"; +$sql .= " p.tobuy, p.tosell, p.fk_product_type"; +$sql .= ", (SELECT pfp.unitprice FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp WHERE pfp.fk_product = p.rowid ORDER BY pfp.unitprice ASC LIMIT 1) as best_buy_price"; +$sql .= ", (SELECT LEFT(COALESCE(NULLIF(s2.name_alias,''), s2.nom), 3) FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp2 LEFT JOIN ".MAIN_DB_PREFIX."societe as s2 ON s2.rowid = pfp2.fk_soc WHERE pfp2.fk_product = p.rowid ORDER BY pfp2.unitprice ASC LIMIT 1) as best_buy_supplier"; +if ($hasMarginField) { + $sql .= ", pe.preisbot_margin"; +} +$sql .= ", cp.fk_categorie"; +$sql .= " FROM ".MAIN_DB_PREFIX."product as p"; +$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."categorie_product as cp ON cp.fk_product = p.rowid"; +if ($hasMarginField) { + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_extrafields as pe ON pe.fk_object = p.rowid"; +} +$sql .= " WHERE p.entity IN (".getEntity('product').")"; +$sql .= " ORDER BY p.ref ASC"; + +$resql = $db->query($sql); +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $productData = array( + 'id' => $obj->rowid, + 'ref' => $obj->ref, + 'label' => $obj->label, + 'description' => $obj->description, + 'price' => $obj->price, + 'price_ttc' => $obj->price_ttc, + 'best_buy_price' => $obj->best_buy_price, + 'best_buy_supplier' => $obj->best_buy_supplier, + 'tosell' => $obj->tosell, + 'tobuy' => $obj->tobuy, + 'fk_product_type' => $obj->fk_product_type, + 'margin' => $hasMarginField ? $obj->preisbot_margin : null, + 'fk_categorie' => $obj->fk_categorie, + ); + + $allProductIds[$obj->rowid] = true; + + if (!empty($obj->fk_categorie)) { + $catId = (int) $obj->fk_categorie; + if (!isset($productsPerCat[$catId])) { + $productsPerCat[$catId] = array(); + } + // Avoid duplicates (product can be in result multiple times if in multiple categories) + $productsPerCat[$catId][$obj->rowid] = $productData; + } + } + $db->free($resql); +} + +// Products without category +$productsWithoutCat = array(); +$sql2 = "SELECT p.rowid, p.ref, p.label, p.description, p.price, p.price_ttc, p.tobuy, p.tosell, p.fk_product_type"; +$sql2 .= ", (SELECT pfp.unitprice FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp WHERE pfp.fk_product = p.rowid ORDER BY pfp.unitprice ASC LIMIT 1) as best_buy_price"; +$sql2 .= ", (SELECT LEFT(COALESCE(NULLIF(s2.name_alias,''), s2.nom), 3) FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp2 LEFT JOIN ".MAIN_DB_PREFIX."societe as s2 ON s2.rowid = pfp2.fk_soc WHERE pfp2.fk_product = p.rowid ORDER BY pfp2.unitprice ASC LIMIT 1) as best_buy_supplier"; +if ($hasMarginField) { + $sql2 .= ", pe.preisbot_margin"; +} +$sql2 .= " FROM ".MAIN_DB_PREFIX."product as p"; +if ($hasMarginField) { + $sql2 .= " LEFT JOIN ".MAIN_DB_PREFIX."product_extrafields as pe ON pe.fk_object = p.rowid"; +} +$sql2 .= " WHERE p.entity IN (".getEntity('product').")"; +$sql2 .= " AND p.rowid NOT IN (SELECT fk_product FROM ".MAIN_DB_PREFIX."categorie_product)"; +$sql2 .= " ORDER BY p.ref ASC"; + +$resql2 = $db->query($sql2); +if ($resql2) { + while ($obj = $db->fetch_object($resql2)) { + $productsWithoutCat[$obj->rowid] = array( + 'id' => $obj->rowid, + 'ref' => $obj->ref, + 'label' => $obj->label, + 'description' => $obj->description, + 'price' => $obj->price, + 'price_ttc' => $obj->price_ttc, + 'best_buy_price' => $obj->best_buy_price, + 'best_buy_supplier' => $obj->best_buy_supplier, + 'tosell' => $obj->tosell, + 'tobuy' => $obj->tobuy, + 'fk_product_type' => $obj->fk_product_type, + 'margin' => $hasMarginField ? $obj->preisbot_margin : null, + ); + } + $db->free($resql2); +} + + +/* + * View + */ + +// Pass config to JS +$jsConfig = array( + 'ajaxUrl' => dol_buildpath('/produktverwaltung/ajax/product_actions.php', 1), + 'token' => newToken(), + 'hasMargin' => $hasMarginField, +); + +$morejs = ''; +$morejs .= ''; + +$moreCss = array('/produktverwaltung/css/produktverwaltung.css'); +$moreJs = array('/produktverwaltung/js/produktverwaltung.js'); + +llxHeader($morejs, $langs->trans("ProduktVerwaltungArea"), '', '', 0, 0, $moreJs, $moreCss, '', 'mod-produktverwaltung page-index'); + +print load_fiche_titre($langs->trans("ProduktVerwaltungArea"), '', 'fa-sitemap'); + + +// === Ref-Schema Infobox === +$showSchema = getDolGlobalString('PRODUKTVERWALTUNG_SHOW_SCHEMA', '1'); +if ($showSchema) { + $refSchema = getDolGlobalString('PRODUKTVERWALTUNG_REF_SCHEMA', 'KAT-HER-[TYP-]SPEC[-SERIE]'); + $refExample = getDolGlobalString('PRODUKTVERWALTUNG_REF_EXAMPLE', ''); + + print ''; +} + + +// === Toolbar === +print '
'; +print ''; +print ''; + +// Add root category +if ($canEdit) { + print ''; +} + +// Search +print ''; + +// Export buttons +if ($canExport) { + print ''; +} +print '
'; + + +// === Category Tree === +print '
    '; +renderCategoryTree(0, $catChildren, $catData, $productsPerCat, $hasMarginField, $canEdit, $canDelete, $langs, $conf, $defaultExpanded); +print '
'; + + +// === Products without category === +$countWithout = count($productsWithoutCat); +if ($countWithout > 0) { + print '
'; + print '
'; + print ''; + print ' '.$langs->trans("ProductsWithoutCategory").' '.$countWithout.''; + print '
'; + $ncCollapsed = $defaultExpanded ? '' : ' collapsed'; + print '
'; + renderProductTable($productsWithoutCat, $hasMarginField, $canEdit, $canDelete, 0, $langs, $conf); + print '
'; + print '
'; +} + +llxFooter(); +$db->close(); + + +// ========== Helper Functions ========== + +/** + * Render category tree recursively + */ +function renderCategoryTree($parentId, &$catChildren, &$catData, &$productsPerCat, $hasMarginField, $canEdit, $canDelete, $langs, $conf, $defaultExpanded = '0') +{ + if (!isset($catChildren[$parentId])) { + return; + } + + foreach ($catChildren[$parentId] as $cat) { + $catId = (int) $cat['id']; + $hasChildren = isset($catChildren[$catId]); + $products = isset($productsPerCat[$catId]) ? $productsPerCat[$catId] : array(); + $productCount = count($products); + + // Count total products including subcategories + $totalCount = countProductsRecursive($catId, $catChildren, $productsPerCat); + + $color = isset($cat['color']) ? $cat['color'] : ''; + + print '
  • '; + + // Category header + print '
    '; + if ($hasChildren || $productCount > 0) { + $collapsedClass = $defaultExpanded ? '' : ' collapsed'; + print ''; + } else { + print ''; + } + if (!empty($color)) { + print ''; + } + print ''.dol_escape_htmltag($cat['label']).''; + if ($totalCount > 0) { + print '('.$totalCount.')'; + } + + // Category action buttons + if ($canEdit) { + print ''; + print ''; + print ''; + if ($canDelete) { + print ''; + } + print ''; + } + + print '
    '; + + // Children container + if ($hasChildren || $productCount > 0) { + $collapsedClass2 = $defaultExpanded ? '' : ' collapsed'; + print '
    '; + + // Products in this category + if ($productCount > 0) { + renderProductTable($products, $hasMarginField, $canEdit, $canDelete, $catId, $langs, $conf); + } + + // Sub-categories + if ($hasChildren) { + print '
      '; + renderCategoryTree($catId, $catChildren, $catData, $productsPerCat, $hasMarginField, $canEdit, $canDelete, $langs, $conf, $defaultExpanded); + print '
    '; + } + + print '
    '; + } + + print '
  • '; + } +} + +/** + * Count products in category and all subcategories + */ +function countProductsRecursive($catId, &$catChildren, &$productsPerCat) +{ + $count = isset($productsPerCat[$catId]) ? count($productsPerCat[$catId]) : 0; + + if (isset($catChildren[$catId])) { + foreach ($catChildren[$catId] as $child) { + $count += countProductsRecursive((int) $child['id'], $catChildren, $productsPerCat); + } + } + + return $count; +} + +/** + * Render product table for a category + */ +function renderProductTable($products, $hasMarginField, $canEdit, $canDelete, $categoryId, $langs, $conf) +{ + if (empty($products)) { + return; + } + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + if ($hasMarginField) { + print ''; + } + print ''; + print ''; + print ''; + print ''; + + foreach ($products as $prod) { + $productId = (int) $prod['id']; + $ref = dol_escape_htmltag($prod['ref']); + $label = dol_escape_htmltag($prod['label']); + $description = dol_escape_htmltag(dol_trunc($prod['description'], 80)); + $bestBuyPrice = !empty($prod['best_buy_price']) ? $prod['best_buy_price'] : 0; + $sellPrice = $prod['price']; + + // Calculate actual margin: ((VK - EK) / EK * 100) + $calcMargin = ''; + if ($bestBuyPrice > 0 && $sellPrice > 0) { + $calcMargin = round(($sellPrice - $bestBuyPrice) / $bestBuyPrice * 100, 1); + } + + print ''; + + // Status (tosell/tobuy) - nur aktive anzeigen + print ''; + + // Ref (editable) + print ''; + + // Label (editable) + print ''; + + // Description (editable) + print ''; + + // Best EK (gΓΌnstigster Lieferantenpreis) + print ''; + + // VK netto + print ''; + + // Stored Margin % (preisbot_margin) + if ($hasMarginField) { + print ''; + } + + // Calculated Margin % + print ''; + + // Actions + print ''; + print ''; + } + + print '
    '.$langs->trans("Ref").''.$langs->trans("Label").''.$langs->trans("Description").''.$langs->trans("BestBuyPrice").''.$langs->trans("SellingPriceNet").''.$langs->trans("MarginPercent").''.$langs->trans("CalcMargin").''.$langs->trans("Actions").'
    '; + if (!empty($prod['tosell'])) { + print ''; + } + if (!empty($prod['tobuy'])) { + print ''; + } + print ''; + if ($canEdit) { + print ''.$ref.''; + } else { + print $ref; + } + print ''; + if ($canEdit) { + print ''.$label.''; + } else { + print $label; + } + print ''; + if ($canEdit) { + print ''.$description.''; + } else { + print $description; + } + print ''; + if ($bestBuyPrice > 0) { + print price($bestBuyPrice, 0, $langs, 1, -1, 2); + if (!empty($prod['best_buy_supplier'])) { + print ' '.dol_escape_htmltag($prod['best_buy_supplier']).''; + } + } else { + print '-'; + } + print ''; + print $sellPrice > 0 ? price($sellPrice, 0, $langs, 1, -1, 2) : '-'; + print ''; + print !empty($prod['margin']) ? price($prod['margin'], 0, $langs, 1, -1, 1).'%' : '-'; + print ''; + if ($calcMargin !== '') { + $marginClass = ''; + if ($hasMarginField && !empty($prod['margin'])) { + $diff = abs((float) $calcMargin - (float) $prod['margin']); + $marginClass = $diff > 2 ? ' pv-margin-warn' : ' pv-margin-ok'; + } + print ''.price($calcMargin, 0, $langs, 1, -1, 1).'%'; + } else { + print '-'; + } + print ''; + + // Edit dialog + if ($canEdit) { + print ''; + } + + // Open in Dolibarr (dedicated popup window) + print ''; + print ''; + print ''; + + // Assign category + if ($canEdit) { + print ''; + } + + // Remove from category + if ($canEdit && $categoryId > 0) { + print ''; + } + + // Delete + if ($canDelete) { + print ''; + } + + print '
    '; +} diff --git a/sql/dolibarr_allversions.sql b/sql/dolibarr_allversions.sql new file mode 100644 index 0000000..5026bb4 --- /dev/null +++ b/sql/dolibarr_allversions.sql @@ -0,0 +1,3 @@ +-- +-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version. +--