From ad180db510a45b98624f81b4279d5f1403147875 Mon Sep 17 00:00:00 2001 From: data Date: Tue, 17 Feb 2026 17:54:13 +0100 Subject: [PATCH] =?UTF-8?q?v4.6:=20Men=C3=BC=20unter=20Produkte,=20bessere?= =?UTF-8?q?=20Barcode-Erkennung,=20Tab-Wechsel=20ohne=20Reload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Menü aus Header entfernt, neuer Eintrag unter Produkte > Scanner - Barcode-Erkennung: patchSize medium, grösserer Scan-Bereich, höhere Frequenz - Timeout-Hinweis nach 8s wenn kein Barcode erkannt wird - Tab-Wechsel (Order/Shop/Inventur) ohne Seitenreload, Kamera bleibt aktiv - PWA: gleiche Tab-Logik, Buttons statt Links - Changelog und README aktualisiert Co-Authored-By: Claude Opus 4.6 --- COPYING | 621 +++++++++++++ ChangeLog.md | 19 + README.md | 161 ++++ admin/about.php | 118 +++ admin/setup.php | 255 +++++ ajax/addtoorder.php | 212 +++++ ajax/debug_barcodes.php | 87 ++ ajax/debug_supplier.php | 65 ++ ajax/findproduct.php | 186 ++++ ajax/getsuppliers.php | 67 ++ ajax/pwa_login.php | 152 +++ ajax/pwa_verify.php | 136 +++ ajax/updatestock.php | 124 +++ build/buildzip.php | 316 +++++++ build/makepack-handybarcodescanner.conf | 11 + class/actions_handybarcodescanner.class.php | 372 ++++++++ core/modules/modHandyBarcodeScanner.class.php | 544 +++++++++++ css/scanner.css | 602 ++++++++++++ handybarcodescannerindex.php | 189 ++++ img/README.md | 14 + img/icon-192.png | Bin 0 -> 6704 bytes img/icon-512.png | Bin 0 -> 10025 bytes img/icon.svg | 20 + js/barcodezoom.js.php | 179 ++++ js/scanner.js | 868 ++++++++++++++++++ langs/de_DE/handybarcodescanner.lang | 83 ++ langs/en_US/handybarcodescanner.lang | 83 ++ lib/handybarcodescanner.lib.php | 85 ++ manifest.json | 27 + modulebuilder.txt | 3 + pwa.php | 829 +++++++++++++++++ sql/dolibarr_allversions.sql | 3 + sw.js | 82 ++ 33 files changed, 6513 insertions(+) create mode 100755 COPYING create mode 100755 ChangeLog.md create mode 100755 README.md create mode 100755 admin/about.php create mode 100755 admin/setup.php create mode 100755 ajax/addtoorder.php create mode 100755 ajax/debug_barcodes.php create mode 100755 ajax/debug_supplier.php create mode 100755 ajax/findproduct.php create mode 100755 ajax/getsuppliers.php create mode 100755 ajax/pwa_login.php create mode 100755 ajax/pwa_verify.php create mode 100755 ajax/updatestock.php create mode 100755 build/buildzip.php create mode 100755 build/makepack-handybarcodescanner.conf create mode 100755 class/actions_handybarcodescanner.class.php create mode 100755 core/modules/modHandyBarcodeScanner.class.php create mode 100755 css/scanner.css create mode 100755 handybarcodescannerindex.php create mode 100755 img/README.md create mode 100755 img/icon-192.png create mode 100755 img/icon-512.png create mode 100755 img/icon.svg create mode 100755 js/barcodezoom.js.php create mode 100755 js/scanner.js create mode 100755 langs/de_DE/handybarcodescanner.lang create mode 100755 langs/en_US/handybarcodescanner.lang create mode 100755 lib/handybarcodescanner.lib.php create mode 100755 manifest.json create mode 100755 modulebuilder.txt create mode 100755 pwa.php create mode 100755 sql/dolibarr_allversions.sql create mode 100755 sw.js diff --git a/COPYING b/COPYING new file mode 100755 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 100755 index 0000000..e1ce639 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,19 @@ +# CHANGELOG MODULE HANDYBARCODESCANNER FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) + +## 4.6 + +- Menü von Header-Navigation ins linke Seitenmenü unter "Produkte" verschoben (kürzerer Name "Scanner") +- Barcode-Erkennung verbessert: patchSize auf "medium" geändert, Scan-Bereich vergrößert, Frequenz erhöht +- Timeout-Hinweis nach 8 Sekunden wenn kein Barcode erkannt wird +- Tab-Wechsel (Order/Shop/Inventur) ohne Seitenreload – Kamera bleibt aktiv +- Gleiche Tab-Logik auch in der PWA-Version + +## 4.5 + +- PWA Standalone-Version mit eigenem Login +- Barcode-Zoom auf Produktkarten (Hook) +- Vibrations- und Sound-Feedback konfigurierbar + +## 1.0 + +Initial version diff --git a/README.md b/README.md new file mode 100755 index 0000000..af47cb8 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# HANDYBARCODESCANNER FOR [DOLIBARR ERP & CRM](https://www.dolibarr.org) + +Mobiler Barcode-Scanner für Dolibarr - optimiert für die Verwendung auf Smartphones. + +## Features + +Das Modul bietet drei Modi für die mobile Barcode-Erfassung: + +### 1. Bestellmodus (Order) +- Produkt per Barcode scannen +- Alle verfügbaren Lieferanten mit Einkaufspreisen werden angezeigt +- Günstigster Lieferant ist vorausgewählt +- Produkt wird zu einer lieferantenspezifischen Entwurfsbestellung hinzugefügt +- Bestellungen werden automatisch als "Direktbestellung-[Lieferantenname]" erstellt +- Falls kein Lieferant zugewiesen: Manuelle Auswahl aller verfügbaren Lieferanten + +### 2. Shop-Modus +- Produkt per Barcode scannen +- Zeigt Links zu den Webshops der Lieferanten +- Schneller Zugriff auf Lieferanten-Onlineshops + +### 3. Inventur-Modus +- Produkt per Barcode scannen +- Aktueller Lagerbestand wird angezeigt +- Neuen Bestand eingeben und mit Bestätigungsdialog speichern +- Lagerbewegungen werden korrekt protokolliert + +## Barcode-Unterstützung + +Das Modul sucht Barcodes in folgender Reihenfolge: +1. Produkt-Barcode (`llx_product.barcode`) +2. Lieferanten-Barcode (`llx_product_fournisseur_price.barcode`) +3. Produkt-Referenz (`llx_product.ref`) + +Unterstützte Barcode-Formate: +- EAN-13, EAN-8 +- Code 128, Code 39 + +## Installation + +### Voraussetzungen +- Dolibarr ERP & CRM (Version 14.0 oder höher empfohlen) +- Aktiviertes Modul "Lieferanten" (Fournisseur/Supplier) +- Aktiviertes Modul "Lager" (Stock) für Inventur-Modus +- HTTPS-Verbindung (erforderlich für Kamerazugriff im Browser) + +### Installation via Git + +```bash +cd /path/to/dolibarr/htdocs/custom +git clone https://git.data-it-solution.de/data/dolibarr.handybarcodescanner.git handybarcodescanner +``` + +### Installation via ZIP + +1. ZIP-Datei herunterladen +2. In Dolibarr: `Home > Setup > Modules > Deploy external module` +3. ZIP-Datei hochladen + +### Aktivierung + +1. Als Administrator in Dolibarr einloggen +2. Gehe zu `Setup > Modules` +3. Suche nach "HandyBarcodeScanner" +4. Modul aktivieren + +## Konfiguration + +Die Einstellungen sind unter `Setup > Modules > HandyBarcodeScanner > Settings` verfügbar: + +### Allgemein +- **Bestellpräfix**: Präfix für automatisch erstellte Bestellungen (Standard: "Direktbestellung") +- **Standard-Lager**: Standard-Lager für Inventur-Bewegungen + +### Aktivierte Modi +- **Bestellmodus aktivieren**: Ein/Aus +- **Shop-Modus aktivieren**: Ein/Aus +- **Inventur-Modus aktivieren**: Ein/Aus + +### Feedback +- **Vibration aktivieren**: Vibriert bei erfolgreichem Scan (auf unterstützten Geräten) +- **Ton aktivieren**: Akustisches Signal bei erfolgreichem Scan + +### QR-Code für mobilen Zugriff +Auf der Einstellungsseite wird ein QR-Code angezeigt, der mit dem Smartphone gescannt werden kann, um direkt zur Scanner-Seite zu gelangen. + +## Berechtigungen + +Das Modul definiert folgende Berechtigungen: + +| Berechtigung | Beschreibung | +|-------------|--------------| +| `handybarcodescanner->use` | Scanner verwenden | +| `handybarcodescanner->order->create` | Bestellungen erstellen | +| `handybarcodescanner->inventory->write` | Lagerbestand ändern | + +## Verwendung + +Der Scanner ist im linken Menü unter **Produkte > Scanner** erreichbar. + +### Moduswechsel ohne Unterbrechung +Die Modi (Bestellen/Shop/Inventur) können per Tab gewechselt werden, ohne dass die Kamera stoppt. Bereits gescannte Produkte werden im neuen Modus direkt angezeigt. + +### Im mobilen Browser / PWA +1. QR-Code von der Admin-Seite scannen oder URL direkt eingeben +2. Kamerazugriff erlauben (HTTPS erforderlich!) +3. Gewünschten Modus wählen und "Scan starten" tippen +4. Barcode vor die Kamera halten + +## Technische Details + +### Dateistruktur + +``` +handybarcodescanner/ +├── admin/ +│ ├── about.php +│ └── setup.php +├── ajax/ +│ ├── addtoorder.php # Produkt zu Bestellung hinzufügen +│ ├── findproduct.php # Produkt per Barcode suchen +│ ├── getsuppliers.php # Alle Lieferanten abrufen +│ └── updatestock.php # Lagerbestand aktualisieren +├── core/modules/ +│ └── modHandyBarcodeScanner.class.php +├── css/ +│ └── scanner.css +├── js/ +│ └── scanner.js +├── langs/ +│ ├── de_DE/handybarcodescanner.lang +│ └── en_US/handybarcodescanner.lang +├── lib/ +│ └── handybarcodescanner.lib.php +└── handybarcodescannerindex.php +``` + +### Verwendete Bibliotheken +- [QuaggaJS](https://github.com/ericblade/quagga2) - Browser-basierte Barcode-Erkennung + +## Changelog + +Siehe [ChangeLog.md](ChangeLog.md) + +## Lizenz + +### Hauptcode +GPLv3 oder (nach Wahl) jede spätere Version. Siehe Datei [COPYING](COPYING) für weitere Informationen. + +### Dokumentation +Alle Texte und READMEs sind unter [GFDL](https://www.gnu.org/licenses/fdl-1.3.en.html) lizenziert. + +## Autor + +Eduard Wisch - [DATA IT-Solution](https://data-it-solution.de) + +## Support + +Bei Fragen oder Problemen: +- Issue erstellen: https://git.data-it-solution.de/data/dolibarr.handybarcodescanner/issues +- E-Mail: data@data-it-solution.de diff --git a/admin/about.php b/admin/about.php new file mode 100755 index 0000000..9d56b54 --- /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 handybarcodescanner/admin/about.php + * \ingroup handybarcodescanner + * \brief About page of module HandyBarcodeScanner. + */ + +// 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/handybarcodescanner.lib.php'; + +/** + * @var Conf $conf + * @var DoliDB $db + * @var HookManager $hookmanager + * @var Translate $langs + * @var User $user + */ + +// Translations +$langs->loadLangs(array("errors", "admin", "handybarcodescanner@handybarcodescanner")); + +// 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 = "HandyBarcodeScannerSetup"; + +llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-handybarcodescanner page-admin_about'); + +// Subheader +$linkback = ''.$langs->trans("BackToModuleList").''; + +print load_fiche_titre($langs->trans($title), $linkback, 'title_setup'); + +// Configuration header +$head = handybarcodescannerAdminPrepareHead(); +print dol_get_fiche_head($head, 'about', $langs->trans($title), 0, 'handybarcodescanner@handybarcodescanner'); + +dol_include_once('/handybarcodescanner/core/modules/modHandyBarcodeScanner.class.php'); +$tmpmodule = new modHandyBarcodeScanner($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 100755 index 0000000..243805f --- /dev/null +++ b/admin/setup.php @@ -0,0 +1,255 @@ + + * Copyright (C) 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. + */ + +/** + * \file handybarcodescanner/admin/setup.php + * \ingroup handybarcodescanner + * \brief HandyBarcodeScanner setup page. + */ + +// Load Dolibarr environment +$res = 0; +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} + +// Libraries +require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php"; +require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php'; +require_once '../lib/handybarcodescanner.lib.php'; + +/** + * @var Conf $conf + * @var DoliDB $db + * @var HookManager $hookmanager + * @var Translate $langs + * @var User $user + */ + +// Translations +$langs->loadLangs(array("admin", "handybarcodescanner@handybarcodescanner", "stocks")); + +// Access control +if (!$user->admin) { + accessforbidden(); +} + +// Parameters +$action = GETPOST('action', 'aZ09'); +$backtopage = GETPOST('backtopage', 'alpha'); + +$error = 0; + +/* + * Actions + */ + +if ($action == 'update') { + // Order prefix + $orderPrefix = GETPOST('order_prefix', 'alphanohtml'); + if (!empty($orderPrefix)) { + dolibarr_set_const($db, 'HANDYBARCODESCANNER_ORDER_PREFIX', $orderPrefix, 'chaine', 0, '', $conf->entity); + } + + // Default warehouse + $defaultWarehouse = GETPOSTINT('default_warehouse'); + dolibarr_set_const($db, 'HANDYBARCODESCANNER_DEFAULT_WAREHOUSE', $defaultWarehouse, 'chaine', 0, '', $conf->entity); + + // Mode toggles + $enableOrder = GETPOSTINT('enable_order'); + $enableShop = GETPOSTINT('enable_shop'); + $enableInventory = GETPOSTINT('enable_inventory'); + + dolibarr_set_const($db, 'HANDYBARCODESCANNER_ENABLE_ORDER', $enableOrder, 'chaine', 0, '', $conf->entity); + dolibarr_set_const($db, 'HANDYBARCODESCANNER_ENABLE_SHOP', $enableShop, 'chaine', 0, '', $conf->entity); + dolibarr_set_const($db, 'HANDYBARCODESCANNER_ENABLE_INVENTORY', $enableInventory, 'chaine', 0, '', $conf->entity); + + // Vibration feedback + $enableVibration = GETPOSTINT('enable_vibration'); + dolibarr_set_const($db, 'HANDYBARCODESCANNER_ENABLE_VIBRATION', $enableVibration, 'chaine', 0, '', $conf->entity); + + // Sound feedback + $enableSound = GETPOSTINT('enable_sound'); + dolibarr_set_const($db, 'HANDYBARCODESCANNER_ENABLE_SOUND', $enableSound, 'chaine', 0, '', $conf->entity); + + setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); +} + + +/* + * View + */ + +$form = new Form($db); + +$help_url = ''; +$title = "HandyBarcodeScannerSetup"; + +llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-handybarcodescanner page-admin'); + +// Subheader +$linkback = ''.$langs->trans("BackToModuleList").''; + +print load_fiche_titre($langs->trans($title), $linkback, 'title_setup'); + +// Configuration header +$head = handybarcodescannerAdminPrepareHead(); +print dol_get_fiche_head($head, 'settings', $langs->trans($title), -1, "handybarcodescanner@handybarcodescanner"); + +// Get current values +$orderPrefix = getDolGlobalString('HANDYBARCODESCANNER_ORDER_PREFIX', 'Direktbestellung'); +$defaultWarehouse = getDolGlobalInt('HANDYBARCODESCANNER_DEFAULT_WAREHOUSE', 0); +$enableOrder = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_ORDER', 1); +$enableShop = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_SHOP', 1); +$enableInventory = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_INVENTORY', 1); +$enableVibration = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_VIBRATION', 1); +$enableSound = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_SOUND', 0); + +// Get warehouses for select +$warehouses = array(); +$sql = "SELECT rowid, ref, lieu FROM ".MAIN_DB_PREFIX."entrepot WHERE statut = 1 AND entity IN (".getEntity('stock').") ORDER BY ref"; +$resql = $db->query($sql); +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $warehouses[$obj->rowid] = $obj->ref . ($obj->lieu ? ' - ' . $obj->lieu : ''); + } +} + +print '
'; +print ''; +print ''; + +print ''; + +// Section: General +print ''; + +// Order prefix +print ''; +print ''; +print ''; + +// Default warehouse +print ''; +print ''; +print ''; + +// Section: Enabled Modes +print ''; + +// Order mode +print ''; +print ''; +print ''; + +// Shop mode +print ''; +print ''; +print ''; + +// Inventory mode +print ''; +print ''; +print ''; + +// Section: Feedback +print ''; + +// Vibration +print ''; +print ''; +print ''; + +// Sound +print ''; +print ''; +print ''; + +print '
'.$langs->trans("General").'
'.$langs->trans("OrderPrefix").''; +print ''; +print '
'.$langs->trans("OrderPrefixDesc").''; +print '
'.$langs->trans("DefaultWarehouse").''; +print ''; +print '
'.$langs->trans("DefaultWarehouseDesc").''; +print '
'.$langs->trans("EnabledModes").'
'.$langs->trans("EnableOrderMode").''; +print ''; +print '
'.$langs->trans("EnableShopMode").''; +print ''; +print '
'.$langs->trans("EnableInventoryMode").''; +print ''; +print '
'.$langs->trans("Feedback").'
'.$langs->trans("EnableVibration").''; +print ''; +print '
'.$langs->trans("EnableVibrationDesc").''; +print '
'.$langs->trans("EnableSound").''; +print ''; +print '
'.$langs->trans("EnableSoundDesc").''; +print '
'; + +print '
'; +print ''; +print '
'; + +print '
'; + +// Show QR Code for mobile access +print '

'; +print '
'; +print '
'; +print ''; +print ''; +print ''; +print '
'.$langs->trans("MobileAccess").'
'; + +$scannerUrl = dol_buildpath('/handybarcodescanner/handybarcodescannerindex.php', 2); +print '

'.$langs->trans("ScanQRCodeOrOpenURL").':

'; +print '

'.$scannerUrl.'

'; + +// QR Code using Google Charts API +$qrUrl = 'https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl='.urlencode($scannerUrl); +print '

QR Code

'; + +print '
'; +print '
'; +print '
'; + +// Page end +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/ajax/addtoorder.php b/ajax/addtoorder.php new file mode 100755 index 0000000..61fcee0 --- /dev/null +++ b/ajax/addtoorder.php @@ -0,0 +1,212 @@ + + * + * AJAX: Add product to supplier order (Direktbestellung) + * Creates or uses existing draft order per supplier + */ + +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREHTML')) { + define('NOREQUIREHTML', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} + +// Load Dolibarr environment +$res = 0; +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res && file_exists("../../../../main.inc.php")) { + $res = @include "../../../../main.inc.php"; +} +if (!$res) { + die(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr'])); +} + +require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php'; +require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php'; + +header('Content-Type: application/json; charset=utf-8'); + +// Security check +if (!$user->hasRight('fournisseur', 'commande', 'creer') && !$user->hasRight('supplier_order', 'creer')) { + echo json_encode(['success' => false, 'error' => 'Access denied']); + exit; +} + +// Get parameters +$productId = GETPOSTINT('product_id'); +$supplierId = GETPOSTINT('supplier_id'); +$qty = GETPOSTINT('qty'); +$price = GETPOSTFLOAT('price'); + +if (empty($productId) || empty($supplierId) || empty($qty)) { + echo json_encode(['success' => false, 'error' => 'Missing parameters']); + exit; +} + +// Load supplier +$supplier = new Societe($db); +if ($supplier->fetch($supplierId) <= 0) { + echo json_encode(['success' => false, 'error' => 'Supplier not found']); + exit; +} + +// Load product +$product = new Product($db); +if ($product->fetch($productId) <= 0) { + echo json_encode(['success' => false, 'error' => 'Product not found']); + exit; +} + +// Build ref_supplier (Lieferanten-Best.-Nr.) with date format: YYMMDD-Direkt (e.g. 260217-Direkt for 2026-02-17) +$refSupplier = date('ymd') . '-Direkt'; + +// Search for existing draft order for this supplier with today's ref_supplier +$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."commande_fournisseur"; +$sql .= " WHERE fk_soc = ".((int) $supplierId); +$sql .= " AND fk_statut = 0"; // Draft status +$sql .= " AND ref_supplier = '".$db->escape($refSupplier)."'"; +$sql .= " AND entity IN (".getEntity('supplier_order').")"; +$sql .= " ORDER BY rowid DESC LIMIT 1"; + +$resql = $db->query($sql); +$existingOrderId = 0; + +if ($resql && $db->num_rows($resql) > 0) { + $obj = $db->fetch_object($resql); + $existingOrderId = $obj->rowid; +} + +$order = new CommandeFournisseur($db); + +if ($existingOrderId > 0) { + // Use existing draft order + $order->fetch($existingOrderId); +} else { + // Create new order - ref is auto-generated by Dolibarr, ref_supplier is our custom reference + $order->socid = $supplierId; + $order->ref_supplier = $refSupplier; // Lieferanten-Best.-Nr.: 260217-Direkt + $order->cond_reglement_id = $supplier->cond_reglement_supplier_id ?: getDolGlobalInt('FOURN_COND_REGLEMENT_ID_DEFAULT', 1); + $order->mode_reglement_id = $supplier->mode_reglement_supplier_id ?: getDolGlobalInt('FOURN_MODE_REGLEMENT_ID_DEFAULT', 1); + $order->date = dol_now(); + $order->date_livraison = dol_now() + (7 * 24 * 60 * 60); // +7 days default + + $result = $order->create($user); + + if ($result < 0) { + echo json_encode(['success' => false, 'error' => 'Failed to create order: ' . $order->error]); + exit; + } +} + +// Get supplier price info +$productFourn = new ProductFournisseur($db); +$supplierPrices = $productFourn->list_product_fournisseur_price($productId, '', '', 0, 0, $supplierId); + +$supplierRef = ''; +$unitPrice = $price; +$tva_tx = 19; // Default VAT + +if (!empty($supplierPrices)) { + foreach ($supplierPrices as $sp) { + if ($sp->fourn_id == $supplierId) { + $supplierRef = $sp->ref_fourn; + if ($unitPrice <= 0) { + $unitPrice = $sp->fourn_price; + } + $tva_tx = $sp->tva_tx; + break; + } + } +} + +// Check if product already in order +$existingLine = null; +foreach ($order->lines as $line) { + if ($line->fk_product == $productId) { + $existingLine = $line; + break; + } +} + +if ($existingLine) { + // Update quantity + $newQty = $existingLine->qty + $qty; + $result = $order->updateline( + $existingLine->id, + $existingLine->desc, + $unitPrice, + $newQty, + $existingLine->remise_percent, + $tva_tx, + 0, // localtax1 + 0, // localtax2 + 'HT', + 0, + 0, + GETPOST('product_type', 'int') ?: 0, + false, + null, + null, + 0, + $supplierRef + ); +} else { + // Add new line + $result = $order->addline( + $product->description ?: $product->label, + $unitPrice, + $qty, + $tva_tx, + 0, // localtax1 + 0, // localtax2 + $productId, + 0, // fourn_price_id + $supplierRef, + 0, // remise_percent + 'HT', + 0, // pu_devise + $product->type, + 0, // rang + 0, // special_code + null, // array_options + null, // fk_unit + 0, // origin + 0 // origin_id + ); +} + +if ($result < 0) { + echo json_encode(['success' => false, 'error' => 'Failed to add line: ' . $order->error]); + exit; +} + +// Count items in order +$order->fetch($order->id); +$totalItems = count($order->lines); +$totalQty = 0; +foreach ($order->lines as $line) { + $totalQty += $line->qty; +} + +echo json_encode([ + 'success' => true, + 'order_id' => $order->id, + 'order_ref' => $order->ref, + 'total_items' => $totalItems, + 'total_qty' => $totalQty, + 'message' => 'Product added to ' . $order->ref +]); diff --git a/ajax/debug_barcodes.php b/ajax/debug_barcodes.php new file mode 100755 index 0000000..c7f80f1 --- /dev/null +++ b/ajax/debug_barcodes.php @@ -0,0 +1,87 @@ +query($sql); + +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + echo "ID: ".$obj->rowid."\n"; + echo "Ref: ".$obj->ref."\n"; + echo "Label: ".$obj->label."\n"; + echo "Barcode: [".$obj->barcode."] (Length: ".strlen($obj->barcode).")\n"; + echo "---\n"; + } +} else { + echo "SQL Error: ".$db->lasterror()."\n"; +} + +echo "\n=== SUPPLIER BARCODES ===\n\n"; + +$sql2 = "SELECT pfp.rowid, pfp.fk_product, pfp.barcode, pfp.ref_fourn, p.ref as product_ref + FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp + LEFT JOIN ".MAIN_DB_PREFIX."product as p ON pfp.fk_product = p.rowid + WHERE pfp.barcode IS NOT NULL AND pfp.barcode != '' + ORDER BY pfp.rowid DESC LIMIT 20"; + +$resql2 = $db->query($sql2); + +if ($resql2) { + while ($obj = $db->fetch_object($resql2)) { + echo "Product ID: ".$obj->fk_product." (".$obj->product_ref.")\n"; + echo "Supplier Barcode: [".$obj->barcode."] (Length: ".strlen($obj->barcode).")\n"; + echo "Supplier Ref: ".$obj->ref_fourn."\n"; + echo "---\n"; + } +} else { + echo "SQL Error: ".$db->lasterror()."\n"; +} + +echo "\n=== SEARCH TEST ===\n"; +$testCode = isset($_GET['code']) ? $_GET['code'] : '202500000068'; +echo "Testing barcode: [".$testCode."]\n\n"; + +// Test search +$sql3 = "SELECT rowid, ref, barcode FROM ".MAIN_DB_PREFIX."product WHERE barcode = '".$db->escape($testCode)."'"; +$resql3 = $db->query($sql3); +if ($resql3 && $db->num_rows($resql3) > 0) { + $obj = $db->fetch_object($resql3); + echo "FOUND in product.barcode: ID=".$obj->rowid.", Ref=".$obj->ref."\n"; +} else { + echo "NOT FOUND in product.barcode\n"; +} + +// Test with extra digit +$testCodeWith1 = $testCode . '1'; +$sql4 = "SELECT rowid, ref, barcode FROM ".MAIN_DB_PREFIX."product WHERE barcode = '".$db->escape($testCodeWith1)."'"; +$resql4 = $db->query($sql4); +if ($resql4 && $db->num_rows($resql4) > 0) { + $obj = $db->fetch_object($resql4); + echo "FOUND with extra '1': ID=".$obj->rowid.", Ref=".$obj->ref.", Barcode=".$obj->barcode."\n"; +} else { + echo "NOT FOUND with extra '1' (".$testCodeWith1.")\n"; +} diff --git a/ajax/debug_supplier.php b/ajax/debug_supplier.php new file mode 100755 index 0000000..01e363f --- /dev/null +++ b/ajax/debug_supplier.php @@ -0,0 +1,65 @@ +escape($barcode)."'"; +$resql = $db->query($sql); +if ($resql && $db->num_rows($resql) > 0) { + $obj = $db->fetch_object($resql); + $product->fetch($obj->rowid); +} else { + echo "Produkt nicht gefunden fuer Barcode: $barcode\n"; + exit; +} + +echo "=== PRODUKT ===\n"; +echo "ID: ".$product->id."\n"; +echo "Ref: ".$product->ref."\n"; +echo "Label: ".$product->label."\n"; +echo "\n"; + +// Get supplier prices +$productFourn = new ProductFournisseur($db); +$suppliers = $productFourn->list_product_fournisseur_price($product->id); + +echo "=== LIEFERANTEN ===\n"; +if ($suppliers) { + foreach ($suppliers as $i => $s) { + echo "\n--- Lieferant ".($i+1)." ---\n"; + echo "fourn_id: ".$s->fourn_id."\n"; + echo "fourn_name: ".$s->fourn_name."\n"; + echo "ref_fourn (ref_supplier): ".$s->ref_supplier."\n"; + echo "fourn_ref: ".$s->fourn_ref."\n"; + echo "fourn_price: ".$s->fourn_price."\n"; + + // Get societe URL + $sqlSoc = "SELECT rowid, nom, url FROM ".MAIN_DB_PREFIX."societe WHERE rowid = ".(int)$s->fourn_id; + $resSoc = $db->query($sqlSoc); + $soc = $db->fetch_object($resSoc); + echo "societe.nom: ".$soc->nom."\n"; + echo "societe.url: '".$soc->url."'\n"; + } +} else { + echo "Keine Lieferanten gefunden\n"; +} diff --git a/ajax/findproduct.php b/ajax/findproduct.php new file mode 100755 index 0000000..c169262 --- /dev/null +++ b/ajax/findproduct.php @@ -0,0 +1,186 @@ + + * + * AJAX: Find product by barcode + * Searches in: product.barcode, product_fournisseur_price (supplier EAN) + */ + +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREHTML')) { + define('NOREQUIREHTML', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} + +// Load Dolibarr environment +$res = 0; +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res && file_exists("../../../../main.inc.php")) { + $res = @include "../../../../main.inc.php"; +} +if (!$res) { + die(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr'])); +} + +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; + +header('Content-Type: application/json; charset=utf-8'); + +// Security check +if (!$user->hasRight('produit', 'lire') && !$user->hasRight('product', 'read')) { + echo json_encode(['success' => false, 'error' => 'Access denied']); + exit; +} + +$barcode = GETPOST('barcode', 'alphanohtml'); + +if (empty($barcode)) { + echo json_encode(['success' => false, 'error' => 'No barcode provided']); + exit; +} + +$product = new Product($db); +$found = false; + +// Build list of barcode variants to search +// QuaggaJS sometimes adds a check digit to 12-digit codes, making them 13 digits +$barcodesToSearch = array($barcode); + +// If 13 digits, also try without last digit (might be added check digit) +if (strlen($barcode) == 13 && preg_match('/^\d{13}$/', $barcode)) { + $barcodesToSearch[] = substr($barcode, 0, 12); +} + +// If 12 digits, also try with EAN-13 check digit +if (strlen($barcode) == 12 && preg_match('/^\d{12}$/', $barcode)) { + $checkDigit = calculateEAN13CheckDigit($barcode); + $barcodesToSearch[] = $barcode . $checkDigit; +} + +// 1. Search by product barcode (try all variants) +foreach ($barcodesToSearch as $searchCode) { + $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."product WHERE barcode = '".$db->escape($searchCode)."' AND entity IN (".getEntity('product').")"; + $resql = $db->query($sql); + if ($resql && $db->num_rows($resql) > 0) { + $obj = $db->fetch_object($resql); + $product->fetch($obj->rowid); + $found = true; + break; + } +} + +// 2. Search by supplier barcode (EAN in product_fournisseur_price) - try all variants +if (!$found) { + foreach ($barcodesToSearch as $searchCode) { + $sql = "SELECT DISTINCT pfp.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON pfp.fk_product = p.rowid"; + $sql .= " WHERE (pfp.barcode = '".$db->escape($searchCode)."' OR pfp.ref_fourn = '".$db->escape($searchCode)."')"; + $sql .= " AND p.entity IN (".getEntity('product').")"; + + $resql = $db->query($sql); + if ($resql && $db->num_rows($resql) > 0) { + $obj = $db->fetch_object($resql); + $product->fetch($obj->fk_product); + $found = true; + break; + } + } +} + +// 3. Search by product ref (in case barcode equals ref) +if (!$found) { + $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."product WHERE ref = '".$db->escape($barcode)."' AND entity IN (".getEntity('product').")"; + $resql = $db->query($sql); + if ($resql && $db->num_rows($resql) > 0) { + $obj = $db->fetch_object($resql); + $product->fetch($obj->rowid); + $found = true; + } +} + +/** + * Calculate EAN-13 check digit for a 12-digit code + */ +function calculateEAN13CheckDigit($code) { + $sum = 0; + for ($i = 0; $i < 12; $i++) { + $sum += intval($code[$i]) * ($i % 2 === 0 ? 1 : 3); + } + return (10 - ($sum % 10)) % 10; +} + +if (!$found) { + echo json_encode(['success' => false, 'error' => 'Product not found']); + exit; +} + +// Get supplier prices +$suppliers = []; +$productFourn = new ProductFournisseur($db); +$resql = $productFourn->list_product_fournisseur_price($product->id); + +if ($resql) { + foreach ($resql as $supplierPrice) { + // Get supplier info mit shop_url aus extrafields + $supplierSql = "SELECT s.nom, s.url, se.shop_url + FROM ".MAIN_DB_PREFIX."societe s + LEFT JOIN ".MAIN_DB_PREFIX."societe_extrafields se ON se.fk_object = s.rowid + WHERE s.rowid = ".((int) $supplierPrice->fourn_id); + $supplierRes = $db->query($supplierSql); + $supplierObj = $db->fetch_object($supplierRes); + + // ref_fourn kann unter verschiedenen Namen im Objekt sein + $refFourn = ''; + if (!empty($supplierPrice->ref_supplier)) { + $refFourn = $supplierPrice->ref_supplier; + } elseif (!empty($supplierPrice->fourn_ref)) { + $refFourn = $supplierPrice->fourn_ref; + } elseif (!empty($supplierPrice->ref_fourn)) { + $refFourn = $supplierPrice->ref_fourn; + } + + // shop_url aus Extrafields verwenden, sonst normale URL + $shopUrl = $supplierObj->shop_url ?? $supplierObj->url ?? ''; + + $suppliers[] = [ + 'id' => $supplierPrice->fourn_id, + 'name' => $supplierObj->nom ?? 'Unknown', + 'price' => (float) $supplierPrice->fourn_price, + 'ref_fourn' => $refFourn, + 'url' => $shopUrl + ]; + } + + // Sort by price (cheapest first) + usort($suppliers, function($a, $b) { + return $a['price'] <=> $b['price']; + }); +} + +// Response +$response = [ + 'success' => true, + 'product' => [ + 'id' => $product->id, + 'ref' => $product->ref, + 'label' => $product->label, + 'barcode' => $product->barcode, + 'stock' => (float) $product->stock_reel, + 'stock_unit' => $product->fk_unit ? $product->getLabelOfUnit() : 'Stk', + 'suppliers' => $suppliers + ] +]; + +echo json_encode($response); diff --git a/ajax/getsuppliers.php b/ajax/getsuppliers.php new file mode 100755 index 0000000..1b08ad8 --- /dev/null +++ b/ajax/getsuppliers.php @@ -0,0 +1,67 @@ + + * + * AJAX: Get all suppliers for manual selection + */ + +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREHTML')) { + define('NOREQUIREHTML', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} + +// Load Dolibarr environment +$res = 0; +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res && file_exists("../../../../main.inc.php")) { + $res = @include "../../../../main.inc.php"; +} +if (!$res) { + die(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr'])); +} + +header('Content-Type: application/json; charset=utf-8'); + +// Security check +if (!$user->hasRight('societe', 'lire') && !$user->hasRight('societe', 'read')) { + echo json_encode(['success' => false, 'error' => 'Access denied']); + exit; +} + +// Get all suppliers (fournisseur = 1) +$sql = "SELECT rowid, nom, url FROM ".MAIN_DB_PREFIX."societe"; +$sql .= " WHERE fournisseur = 1"; +$sql .= " AND status = 1"; // Active only +$sql .= " AND entity IN (".getEntity('societe').")"; +$sql .= " ORDER BY nom ASC"; + +$resql = $db->query($sql); + +$suppliers = []; + +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $suppliers[] = [ + 'id' => (int) $obj->rowid, + 'name' => $obj->nom, + 'url' => $obj->url ?: '' + ]; + } +} + +echo json_encode([ + 'success' => true, + 'suppliers' => $suppliers +]); diff --git a/ajax/pwa_login.php b/ajax/pwa_login.php new file mode 100755 index 0000000..65e9046 --- /dev/null +++ b/ajax/pwa_login.php @@ -0,0 +1,152 @@ + + * + * AJAX: PWA Login - Authentifiziert Benutzer und gibt Token zurueck + */ + +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREHTML')) { + define('NOREQUIREHTML', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} +// Wichtig: Kein Login erforderlich fuer diese Seite +if (!defined('NOLOGIN')) { + define('NOLOGIN', '1'); +} + +// Load Dolibarr environment +$res = 0; +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res && file_exists("../../../../main.inc.php")) { + $res = @include "../../../../main.inc.php"; +} +if (!$res) { + die(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr'])); +} + +require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; + +header('Content-Type: application/json; charset=utf-8'); + +// Nur POST erlaubt +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['success' => false, 'error' => 'Method not allowed']); + exit; +} + +$login = GETPOST('login', 'alphanohtml'); +$password = GETPOST('password', 'none'); // 'none' = kein Filter fuer Passwort + +if (empty($login) || empty($password)) { + echo json_encode(['success' => false, 'error' => 'Login und Passwort erforderlich']); + exit; +} + +// Benutzer authentifizieren +$userobj = new User($db); +$result = $userobj->fetch('', $login); + +if ($result <= 0) { + // Benutzer nicht gefunden - generische Fehlermeldung aus Sicherheitsgruenden + sleep(1); // Brute-Force-Schutz + echo json_encode(['success' => false, 'error' => 'Login fehlgeschlagen']); + exit; +} + +// Passwort pruefen +$passOk = false; + +// Methode 1: password_verify (moderne Dolibarr-Versionen) +if (function_exists('password_verify') && !empty($userobj->pass_indatabase_crypted)) { + $passOk = password_verify($password, $userobj->pass_indatabase_crypted); +} + +// Methode 2: dol_hash (aeltere Versionen) +if (!$passOk && !empty($userobj->pass_indatabase_crypted)) { + $passOk = (dol_hash($password) === $userobj->pass_indatabase_crypted); +} + +// Methode 3: MD5 (sehr alte Installationen) +if (!$passOk && !empty($userobj->pass_indatabase_crypted)) { + $passOk = (md5($password) === $userobj->pass_indatabase_crypted); +} + +if (!$passOk) { + sleep(1); // Brute-Force-Schutz + echo json_encode(['success' => false, 'error' => 'Login fehlgeschlagen']); + exit; +} + +// Benutzer ist aktiv? +if ($userobj->statut != 1) { + echo json_encode(['success' => false, 'error' => 'Benutzer deaktiviert']); + exit; +} + +// Rechte pruefen +$userobj->getrights(); +$hasAccess = false; + +if ($userobj->hasRight('handybarcodescanner', 'use')) { + $hasAccess = true; +} elseif ($userobj->hasRight('fournisseur', 'commande', 'creer') || $userobj->hasRight('supplier_order', 'creer')) { + $hasAccess = true; +} + +if (!$hasAccess) { + echo json_encode(['success' => false, 'error' => 'Keine Berechtigung fuer Scanner']); + exit; +} + +// Session starten und Benutzer einloggen +if (session_status() === PHP_SESSION_NONE) { + session_start(); +} + +// Dolibarr Session-Variablen setzen +$_SESSION['dol_login'] = $userobj->login; +$_SESSION['dol_authmode'] = 'dolibarr'; +$_SESSION['dol_tz'] = GETPOST('tz', 'alpha'); +$_SESSION['dol_entity'] = $conf->entity; + +// Token generieren fuer 15 Tage +$tokenData = [ + 'user_id' => $userobj->id, + 'login' => $userobj->login, + 'entity' => $conf->entity, + 'created' => time(), + 'expires' => time() + (15 * 24 * 60 * 60), // 15 Tage + 'hash' => bin2hex(random_bytes(16)) +]; + +// Token als Base64-encoded JSON (nicht sicher fuer echte Auth, aber reicht fuer PWA-Cache) +$pwaToken = base64_encode(json_encode($tokenData)); + +// Dolibarr CSRF-Token +$csrfToken = newToken(); + +echo json_encode([ + 'success' => true, + 'pwa_token' => $pwaToken, + 'csrf_token' => $csrfToken, + 'user' => [ + 'id' => $userobj->id, + 'login' => $userobj->login, + 'firstname' => $userobj->firstname, + 'lastname' => $userobj->lastname + ], + 'expires' => $tokenData['expires'], + 'expires_human' => date('Y-m-d H:i:s', $tokenData['expires']) +]); diff --git a/ajax/pwa_verify.php b/ajax/pwa_verify.php new file mode 100755 index 0000000..4cc6f83 --- /dev/null +++ b/ajax/pwa_verify.php @@ -0,0 +1,136 @@ + + * + * AJAX: PWA Token Verify - Prueft gespeicherten Token und startet Session + */ + +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('NOLOGIN')) { + define('NOLOGIN', '1'); +} + +// Load Dolibarr environment +$res = 0; +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res && file_exists("../../../../main.inc.php")) { + $res = @include "../../../../main.inc.php"; +} +if (!$res) { + die(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr'])); +} + +require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; + +header('Content-Type: application/json; charset=utf-8'); + +// Token aus Header oder POST +$pwaToken = ''; +if (!empty($_SERVER['HTTP_X_PWA_TOKEN'])) { + $pwaToken = $_SERVER['HTTP_X_PWA_TOKEN']; +} else { + $pwaToken = GETPOST('pwa_token', 'alphanohtml'); +} + +if (empty($pwaToken)) { + echo json_encode(['success' => false, 'error' => 'Token fehlt', 'need_login' => true]); + exit; +} + +// Token dekodieren +$tokenJson = base64_decode($pwaToken); +if ($tokenJson === false) { + echo json_encode(['success' => false, 'error' => 'Ungueltiger Token', 'need_login' => true]); + exit; +} + +$tokenData = json_decode($tokenJson, true); +if (!$tokenData || !isset($tokenData['user_id']) || !isset($tokenData['expires'])) { + echo json_encode(['success' => false, 'error' => 'Token-Format ungueltig', 'need_login' => true]); + exit; +} + +// Ablauf pruefen +if ($tokenData['expires'] < time()) { + echo json_encode(['success' => false, 'error' => 'Token abgelaufen', 'need_login' => true]); + exit; +} + +// Benutzer laden +$userobj = new User($db); +$result = $userobj->fetch($tokenData['user_id']); + +if ($result <= 0) { + echo json_encode(['success' => false, 'error' => 'Benutzer nicht gefunden', 'need_login' => true]); + exit; +} + +// Benutzer noch aktiv? +if ($userobj->statut != 1) { + echo json_encode(['success' => false, 'error' => 'Benutzer deaktiviert', 'need_login' => true]); + exit; +} + +// Login stimmt ueberein? +if ($userobj->login !== $tokenData['login']) { + echo json_encode(['success' => false, 'error' => 'Token ungueltig', 'need_login' => true]); + exit; +} + +// Rechte pruefen +$userobj->getrights(); +$hasAccess = false; + +if ($userobj->hasRight('handybarcodescanner', 'use')) { + $hasAccess = true; +} elseif ($userobj->hasRight('fournisseur', 'commande', 'creer') || $userobj->hasRight('supplier_order', 'creer')) { + $hasAccess = true; +} + +if (!$hasAccess) { + echo json_encode(['success' => false, 'error' => 'Keine Berechtigung', 'need_login' => true]); + exit; +} + +// Session starten/aktualisieren +if (session_status() === PHP_SESSION_NONE) { + session_start(); +} + +$_SESSION['dol_login'] = $userobj->login; +$_SESSION['dol_authmode'] = 'dolibarr'; +$_SESSION['dol_entity'] = $tokenData['entity'] ?? $conf->entity; + +// Neuen CSRF-Token generieren +$csrfToken = newToken(); + +// Verbleibende Zeit berechnen +$remainingDays = ceil(($tokenData['expires'] - time()) / (24 * 60 * 60)); + +echo json_encode([ + 'success' => true, + 'csrf_token' => $csrfToken, + 'user' => [ + 'id' => $userobj->id, + 'login' => $userobj->login, + 'firstname' => $userobj->firstname, + 'lastname' => $userobj->lastname + ], + 'expires' => $tokenData['expires'], + 'remaining_days' => $remainingDays +]); diff --git a/ajax/updatestock.php b/ajax/updatestock.php new file mode 100755 index 0000000..18b7bc4 --- /dev/null +++ b/ajax/updatestock.php @@ -0,0 +1,124 @@ + + * + * AJAX: Update product stock (Inventory mode) + */ + +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREHTML')) { + define('NOREQUIREHTML', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} + +// Load Dolibarr environment +$res = 0; +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res && file_exists("../../../../main.inc.php")) { + $res = @include "../../../../main.inc.php"; +} +if (!$res) { + die(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr'])); +} + +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php'; + +header('Content-Type: application/json; charset=utf-8'); + +// Security check +if (!$user->hasRight('stock', 'mouvement', 'creer')) { + echo json_encode(['success' => false, 'error' => 'Access denied - no stock movement rights']); + exit; +} + +// Get parameters +$productId = GETPOSTINT('product_id'); +$newStock = GETPOSTFLOAT('stock'); + +if (empty($productId)) { + echo json_encode(['success' => false, 'error' => 'Missing product_id']); + exit; +} + +if ($newStock === null || $newStock === '') { + echo json_encode(['success' => false, 'error' => 'Missing stock value']); + exit; +} + +// Load product +$product = new Product($db); +if ($product->fetch($productId) <= 0) { + echo json_encode(['success' => false, 'error' => 'Product not found']); + exit; +} + +$currentStock = (float) $product->stock_reel; +$newStock = (float) $newStock; +$diff = $newStock - $currentStock; + +if ($diff == 0) { + echo json_encode(['success' => true, 'message' => 'No change needed']); + exit; +} + +// Get default warehouse from config or use first available +$warehouseId = getDolGlobalInt('HANDYBARCODESCANNER_DEFAULT_WAREHOUSE', 0); + +if ($warehouseId <= 0) { + // Get first warehouse + $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."entrepot WHERE statut = 1 AND entity IN (".getEntity('stock').") ORDER BY rowid ASC LIMIT 1"; + $resql = $db->query($sql); + if ($resql && $db->num_rows($resql) > 0) { + $obj = $db->fetch_object($resql); + $warehouseId = $obj->rowid; + } +} + +if ($warehouseId <= 0) { + echo json_encode(['success' => false, 'error' => 'No warehouse configured']); + exit; +} + +// Create stock movement +$movementStock = new MouvementStock($db); +$movementStock->origin_type = 'handybarcodescanner'; +$movementStock->origin_id = 0; + +$inventoryCode = 'INV-SCAN-'.date('Ymd-His'); +$label = 'Inventur per HandyBarcodeScanner'; + +if ($diff > 0) { + // Increase stock + $result = $movementStock->reception($user, $productId, $warehouseId, $diff, 0, $label, '', '', '', '', 0, $inventoryCode); +} else { + // Decrease stock + $result = $movementStock->livraison($user, $productId, $warehouseId, abs($diff), 0, $label, '', '', '', '', 0, $inventoryCode); +} + +if ($result < 0) { + echo json_encode(['success' => false, 'error' => 'Stock movement failed: ' . $movementStock->error]); + exit; +} + +// Reload product to get updated stock +$product->fetch($productId); + +echo json_encode([ + 'success' => true, + 'old_stock' => $currentStock, + 'new_stock' => (float) $product->stock_reel, + 'difference' => $diff, + 'warehouse_id' => $warehouseId +]); diff --git a/build/buildzip.php b/build/buildzip.php new file mode 100755 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-handybarcodescanner.conf b/build/makepack-handybarcodescanner.conf new file mode 100755 index 0000000..16dc1e7 --- /dev/null +++ b/build/makepack-handybarcodescanner.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/class/actions_handybarcodescanner.class.php b/class/actions_handybarcodescanner.class.php new file mode 100755 index 0000000..43e6fad --- /dev/null +++ b/class/actions_handybarcodescanner.class.php @@ -0,0 +1,372 @@ + + * + * 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 class/actions_handybarcodescanner.class.php + * \ingroup handybarcodescanner + * \brief Hook class for HandyBarcodeScanner module + */ + +/** + * Class ActionsHandyBarcodeScanner + */ +class ActionsHandyBarcodeScanner +{ + /** + * @var DoliDB Database handler. + */ + public $db; + + /** + * @var string Error code (or message) + */ + public $error = ''; + + /** + * @var array Errors + */ + public $errors = array(); + + /** + * @var array Hook results. Propagated to $hookmanager->resArray for later reuse + */ + public $results = array(); + + /** + * @var string String displayed by executeHook() immediately after return + */ + public $resprints; + + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + $this->db = $db; + } + + /** + * Execute action - Hook on product card view + * + * @param array $parameters Array of parameters + * @param CommonObject $object The object to process + * @param string $action Action code + * @param HookManager $hookmanager Hook manager propagated to allow calling another hook + * @return int 0=OK, <0 on error, >0 to replace standard code + */ + public function tabContentViewProduct($parameters, &$object, &$action, $hookmanager) + { + global $conf, $langs; + + // Only if module is enabled + if (!isModEnabled('handybarcodescanner')) { + return 0; + } + + // Inject JavaScript and CSS for barcode zoom feature + $this->resprints = $this->getBarcodeZoomAssets(); + + return 0; + } + + /** + * Get JavaScript and CSS for barcode zoom modal + * + * @return string HTML with script and style tags + */ + private function getBarcodeZoomAssets() + { + $html = ''; + + // CSS for the modal + $html .= ' + +'; + + // JavaScript for barcode zoom functionality + $html .= ' + +'; + + return $html; + } +} diff --git a/core/modules/modHandyBarcodeScanner.class.php b/core/modules/modHandyBarcodeScanner.class.php new file mode 100755 index 0000000..105e676 --- /dev/null +++ b/core/modules/modHandyBarcodeScanner.class.php @@ -0,0 +1,544 @@ + + * 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 handybarcodescanner Module HandyBarcodeScanner + * \brief HandyBarcodeScanner module descriptor. + * + * \file htdocs/handybarcodescanner/core/modules/modHandyBarcodeScanner.class.php + * \ingroup handybarcodescanner + * \brief Description and activation file for module HandyBarcodeScanner + */ +include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php'; + + +/** + * Description and activation class for module HandyBarcodeScanner + */ +class modHandyBarcodeScanner 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 = 500024; // 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 = 'handybarcodescanner'; + + // 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 'ModuleHandyBarcodeScannerName' not found (HandyBarcodeScanner is name of module). + $this->name = preg_replace('/^mod/i', '', get_class($this)); + + // DESCRIPTION_FLAG + // Module description, used if translation string 'ModuleHandyBarcodeScannerDesc' not found (HandyBarcodeScanner is name of module). + $this->description = "HandyBarcodeScannerDescription"; + // Used only if file README.md and README-LL.md not found. + $this->descriptionlong = "HandyBarcodeScannerDescription"; + + // 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@handybarcodescanner' + + // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' + $this->version = '4.6'; + // 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 HANDYBARCODESCANNER 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-barcode'; + + // Define some features supported by module (triggers, login, substitutions, menus, css, etc...) + $this->module_parts = array( + // Set this to 1 if module has its own trigger directory (core/triggers) + 'triggers' => 0, + // Set this to 1 if module has its own login method file (core/login) + 'login' => 0, + // Set this to 1 if module has its own substitution function file (core/substitutions) + 'substitutions' => 0, + // Set this to 1 if module has its own menus handler directory (core/menus) + 'menus' => 0, + // Set this to 1 if module overwrite template dir (core/tpl) + 'tpl' => 0, + // Set this to 1 if module has its own barcode directory (core/modules/barcode) + 'barcode' => 0, + // Set this to 1 if module has its own models directory (core/modules/xxx) + 'models' => 0, + // Set this to 1 if module has its own printing directory (core/modules/printing) + 'printing' => 0, + // Set this to 1 if module has its own theme directory (theme) + 'theme' => 0, + // Set this to relative path of css file if module has its own css file + 'css' => array( + // '/handybarcodescanner/css/handybarcodescanner.css.php', + ), + // Set this to relative path of js file if module must load a js on all pages + 'js' => array( + '/handybarcodescanner/js/barcodezoom.js.php', + ), + // Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context to 'all' + /* BEGIN MODULEBUILDER HOOKSCONTEXTS */ + 'hooks' => array( + 'data' => array( + 'productcard', + ), + ), + /* 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("/handybarcodescanner/temp","/handybarcodescanner/subdir"); + $this->dirs = array("/handybarcodescanner/temp"); + + // Config pages. Put here list of php page, stored into handybarcodescanner/admin directory, to use to setup module. + $this->config_page_url = array("setup.php@handybarcodescanner"); + + // Dependencies + // A condition to hide module + $this->hidden = getDolGlobalInt('MODULE_HANDYBARCODESCANNER_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("handybarcodescanner@handybarcodescanner"); + + // Prerequisites + $this->phpmin = array(7, 1); // Minimum version of PHP required by module + // $this->phpmax = array(8, 0); // Maximum version of PHP required by module + $this->need_dolibarr_version = array(19, -3); // Minimum version of Dolibarr required by module + // $this->max_dolibarr_version = array(19, -3); // Maximum version of Dolibarr required by module + $this->need_javascript_ajax = 0; + + // Messages at activation + $this->warnings_activation = array(); // Warning to show when we activate module. array('always'='text') or array('FR'='textfr','MX'='textmx'...) + $this->warnings_activation_ext = array(); // Warning to show when we activate an external module. array('always'='text') or array('FR'='textfr','MX'='textmx'...) + //$this->automatic_activation = array('FR'=>'HandyBarcodeScannerWasAutomaticallyActivatedBecauseOfYourCountryChoice'); + //$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('HANDYBARCODESCANNER_MYNEWCONST1', 'chaine', 'myvalue', 'This is a constant to add', 1), + // 2 => array('HANDYBARCODESCANNER_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("handybarcodescanner")) { + $conf->handybarcodescanner = new stdClass(); + $conf->handybarcodescanner->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@handybarcodescanner:$user->hasRight(\'handybarcodescanner\', \'read\'):/handybarcodescanner/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@handybarcodescanner:$user->hasRight(\'othermodule\', \'read\'):/handybarcodescanner/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' => 'handybarcodescanner@handybarcodescanner', + // 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('handybarcodescanner'), isModEnabled('handybarcodescanner'), isModEnabled('handybarcodescanner')), + // 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 handybarcodescanner/core/boxes that contains a class to show a widget. + /* BEGIN MODULEBUILDER WIDGETS */ + $this->boxes = array( + // 0 => array( + // 'file' => 'handybarcodescannerwidget1.php@handybarcodescanner', + // 'note' => 'Widget provided by HandyBarcodeScanner', + // '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' => '/handybarcodescanner/class/myobject.class.php', + // 'objectname' => 'MyObject', + // 'method' => 'doScheduledJob', + // 'parameters' => '', + // 'comment' => 'Comment', + // 'frequency' => 2, + // 'unitfrequency' => 3600, + // 'status' => 0, + // 'test' => 'isModEnabled("handybarcodescanner")', + // '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("handybarcodescanner")', 'priority'=>50), + // 1=>array('label'=>'My label', 'jobtype'=>'command', 'command'=>'', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>1, 'unitfrequency'=>3600*24, 'status'=>0, 'test'=>'isModEnabled("handybarcodescanner")', 'priority'=>50) + // ); + + // Permissions provided by this module + $this->rights = array(); + $r = 0; + + // Permission to use scanner (basic access) + $this->rights[$r][0] = $this->numero . sprintf("%02d", 1); + $this->rights[$r][1] = 'Use barcode scanner'; + $this->rights[$r][3] = 0; // Not enabled by default + $this->rights[$r][4] = 'use'; + $this->rights[$r][5] = ''; + $r++; + + // Permission to create orders via scanner + $this->rights[$r][0] = $this->numero . sprintf("%02d", 2); + $this->rights[$r][1] = 'Create supplier orders via scanner'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'order'; + $this->rights[$r][5] = 'create'; + $r++; + + // Permission to do inventory via scanner + $this->rights[$r][0] = $this->numero . sprintf("%02d", 3); + $this->rights[$r][1] = 'Update stock via scanner (inventory)'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'inventory'; + $this->rights[$r][5] = 'write'; + + + // Main menu entries to add + $this->menu = array(); + $r = 0; + // Add here entries to declare new menus + /* BEGIN MODULEBUILDER TOPMENU */ + /* Top-Menü entfernt - Eintrag ist jetzt im linken Menü unter Produkte */ + /* END MODULEBUILDER TOPMENU */ + + // Linker Menü-Eintrag unter "Produkte" + $this->menu[$r++] = array( + 'fk_menu' => 'fk_mainmenu=products', + 'type' => 'left', + 'titre' => 'Scanner', + 'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle paddingright"'), + 'mainmenu' => 'products', + 'leftmenu' => 'handybarcodescanner', + 'url' => '/handybarcodescanner/handybarcodescannerindex.php', + 'langs' => 'handybarcodescanner@handybarcodescanner', + 'position' => -5, + 'enabled' => 'isModEnabled("handybarcodescanner")', + 'perms' => '1', + 'target' => '', + 'user' => 2, + ); + + /* BEGIN MODULEBUILDER LEFTMENU MYOBJECT */ + /* + $this->menu[$r++]=array( + 'fk_menu' => 'fk_mainmenu=handybarcodescanner', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type' => 'left', // This is a Left menu entry + 'titre' => 'MyObject', + 'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle paddingright"'), + 'mainmenu' => 'handybarcodescanner', + 'leftmenu' => 'myobject', + 'url' => '/handybarcodescanner/handybarcodescannerindex.php', + 'langs' => 'handybarcodescanner@handybarcodescanner', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled("handybarcodescanner")', // Define condition to show or hide menu entry. Use 'isModEnabled("handybarcodescanner")' if entry must be visible if module is enabled. + 'perms' => '$user->hasRight("handybarcodescanner", "myobject", "read")', + 'target' => '', + 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both + 'object' => 'MyObject' + ); + $this->menu[$r++]=array( + 'fk_menu' => 'fk_mainmenu=handybarcodescanner,fk_leftmenu=myobject', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type' => 'left', // This is a Left menu entry + 'titre' => 'New_MyObject', + 'mainmenu' => 'handybarcodescanner', + 'leftmenu' => 'handybarcodescanner_myobject_new', + 'url' => '/handybarcodescanner/myobject_card.php?action=create', + 'langs' => 'handybarcodescanner@handybarcodescanner', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled("handybarcodescanner")', // Define condition to show or hide menu entry. Use 'isModEnabled("handybarcodescanner")' if entry must be visible if module is enabled. Use '$leftmenu==\'system\'' to show if leftmenu system is selected. + 'perms' => '$user->hasRight("handybarcodescanner", "myobject", "write")' + 'target' => '', + 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both + 'object' => 'MyObject' + ); + $this->menu[$r++]=array( + 'fk_menu' => 'fk_mainmenu=handybarcodescanner,fk_leftmenu=myobject', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type' => 'left', // This is a Left menu entry + 'titre' => 'List_MyObject', + 'mainmenu' => 'handybarcodescanner', + 'leftmenu' => 'handybarcodescanner_myobject_list', + 'url' => '/handybarcodescanner/myobject_list.php', + 'langs' => 'handybarcodescanner@handybarcodescanner', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled("handybarcodescanner")', // Define condition to show or hide menu entry. Use 'isModEnabled("handybarcodescanner")' if entry must be visible if module is enabled. + 'perms' => '$user->hasRight("handybarcodescanner", "myobject", "read")' + 'target' => '', + 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both + 'object' => 'MyObject' + ); + */ + /* END MODULEBUILDER LEFTMENU MYOBJECT */ + + + // Exports profiles provided by this module + $r = 0; + /* BEGIN MODULEBUILDER EXPORT MYOBJECT */ + /* + $langs->load("handybarcodescanner@handybarcodescanner"); + $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='/handybarcodescanner/class/myobject.class.php'; $keyforelement='myobject@handybarcodescanner'; + 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='/handybarcodescanner/class/myobject.class.php'; $keyforelement='myobjectline@handybarcodescanner'; $keyforalias='tl'; + //include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php'; + $keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@handybarcodescanner'; + include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php'; + //$keyforselect='myobjectline'; $keyforaliasextra='extraline'; $keyforelement='myobjectline@handybarcodescanner'; + //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().'handybarcodescanner_myobject as t'; + //$this->export_sql_end[$r] .=' LEFT JOIN '.$this->db->prefix().'handybarcodescanner_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("handybarcodescanner@handybarcodescanner"); + $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().'handybarcodescanner_myobject', 'extra' => $this->db->prefix().'handybarcodescanner_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='/handybarcodescanner/class/myobject.class.php'; $keyforelement='myobject@handybarcodescanner'; + include DOL_DOCUMENT_ROOT.'/core/commonfieldsinimport.inc.php'; + $import_extrafield_sample = array(); + $keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@handybarcodescanner'; + include DOL_DOCUMENT_ROOT.'/core/extrafieldsinimport.inc.php'; + $this->import_fieldshidden_array[$r] = array('extra.fk_object' => 'lastrowid-'.$this->db->prefix().'handybarcodescanner_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('HANDYBARCODESCANNER_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('HANDYBARCODESCANNER_MYOBJECT_ADDON')), + 'path'=>"/core/modules/handybarcodescanner/".(!getDolGlobalString('HANDYBARCODESCANNER_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('HANDYBARCODESCANNER_MYOBJECT_ADDON')).'.php', + 'classobject'=>'MyObject', + 'pathobject'=>'/handybarcodescanner/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/', 'handybarcodescanner'); + $result = $this->_load_tables('/handybarcodescanner/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('handybarcodescanner_separator1', "Separator 1", 'separator', 1, 0, 'thirdparty', 0, 0, '', array('options'=>array(1=>1)), 1, '', 1, 0, '', '', 'handybarcodescanner@handybarcodescanner', 'isModEnabled("handybarcodescanner")'); + //$result1=$extrafields->addExtraField('handybarcodescanner_myattr1', "New Attr 1 label", 'boolean', 1, 3, 'thirdparty', 0, 0, '', '', 1, '', -1, 0, '', '', 'handybarcodescanner@handybarcodescanner', 'isModEnabled("handybarcodescanner")'); + //$result2=$extrafields->addExtraField('handybarcodescanner_myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project', 0, 0, '', '', 1, '', -1, 0, '', '', 'handybarcodescanner@handybarcodescanner', 'isModEnabled("handybarcodescanner")'); + //$result3=$extrafields->addExtraField('handybarcodescanner_myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', -1, 0, '', '', 'handybarcodescanner@handybarcodescanner', 'isModEnabled("handybarcodescanner")'); + //$result4=$extrafields->addExtraField('handybarcodescanner_myattr4', "New Attr 4 label", 'select', 1, 3, 'thirdparty', 0, 1, '', array('options'=>array('code1'=>'Val1','code2'=>'Val2','code3'=>'Val3')), 1,'', -1, 0, '', '', 'handybarcodescanner@handybarcodescanner', 'isModEnabled("handybarcodescanner")'); + //$result5=$extrafields->addExtraField('handybarcodescanner_myattr5', "New Attr 5 label", 'text', 1, 10, 'user', 0, 0, '', '', 1, '', -1, 0, '', '', 'handybarcodescanner@handybarcodescanner', 'isModEnabled("handybarcodescanner")'); + + // Permissions + $this->remove($options); + + $sql = array(); + + // Document templates + $moduledir = dol_sanitizeFileName('handybarcodescanner'); + $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/scanner.css b/css/scanner.css new file mode 100755 index 0000000..3a4360e --- /dev/null +++ b/css/scanner.css @@ -0,0 +1,602 @@ +/* HandyBarcodeScanner - Dolibarr integrated CSS */ + +/* General */ +.hidden { + display: none !important; +} + +.scanner-wrapper { + max-width: 800px; + margin: 0 auto; +} + +/* Mode Tabs */ +.scanner-mode-tabs { + display: flex; + gap: 0; + margin-bottom: 10px; + border-bottom: 2px solid var(--colorborder, #ddd); +} + +.scanner-tab { + flex: 1; + padding: 10px 15px; + border: none; + background: none; + font-size: 14px; + font-weight: 500; + color: var(--colortexttitlenotab, #666); + cursor: pointer; + position: relative; + transition: color 0.2s ease; +} + +.scanner-tab:hover { + color: var(--colortextlink, #333); +} + +.scanner-tab.active { + color: var(--butactionbg, #0077b3); + font-weight: 600; +} + +.scanner-tab.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background: var(--butactionbg, #0077b3); +} + +/* Scanner Video Box */ +.scanner-box { + background: #333; + border-radius: 8px; + overflow: hidden; + margin-bottom: 10px; +} + +.scanner-video-box { + position: relative; + width: 100%; + aspect-ratio: 16/9; + max-height: 150px; + background: #000; +} + +#scanner-video { + width: 100%; + height: 100%; + object-fit: cover; +} + +.scan-region-highlight { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 70%; + height: 60%; + border: 3px solid var(--butactionbg, #0077b3); + border-radius: 8px; + box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); + pointer-events: none; +} + +.scanner-video-box.scanning .scan-region-highlight { + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { border-color: var(--butactionbg, #0077b3); } + 50% { border-color: #00b894; } +} + +/* Scanner Controls */ +.scanner-controls { + padding: 8px; + background: #444; +} + +.scanner-controls .button { + min-width: 150px; + font-size: 16px; + padding: 10px 25px; +} + +/* Last Scan Info */ +.scanner-last-scan { + padding: 10px 15px; + background: var(--colorbacklinepair1, #f8f8f8); + border-radius: 6px; + font-size: 14px; +} + +.scanner-last-scan .badge { + font-family: monospace; + font-size: 14px; + padding: 4px 10px; +} + +/* Result Area */ +.scanner-result { + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Product Card */ +.product-card { + background: var(--colorbacklinepair1, #f8f8f8); + border: 1px solid var(--colorborder, #ddd); + border-radius: 8px; + padding: 15px; + margin-bottom: 15px; +} + +.product-name { + font-size: 18px; + font-weight: 600; + margin-bottom: 5px; + color: var(--colortextlink, #333); +} + +.product-ref { + font-size: 13px; + color: var(--colortexttitlenotab, #666); + margin-bottom: 10px; +} + +.product-stock { + font-size: 14px; + padding: 6px 12px; + background: var(--colorbacktitle1, #e8e8e8); + border-radius: 6px; + display: inline-block; +} + +/* Supplier Selection */ +.supplier-list { + margin: 15px 0; +} + +.supplier-label { + font-size: 14px; + font-weight: 600; + color: var(--colortexttitlenotab, #666); + margin-bottom: 10px; + display: block; +} + +.supplier-option { + display: flex; + align-items: center; + padding: 12px 15px; + background: var(--colorbacklinepair1, #fff); + border: 2px solid var(--colorborder, #ddd); + border-radius: 8px; + margin-bottom: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.supplier-option:hover { + background: var(--colorbacklinepair2, #f5f5f5); +} + +.supplier-option.selected { + border-color: var(--butactionbg, #0077b3); + background: rgba(0, 119, 179, 0.05); +} + +.supplier-option:active { + transform: scale(0.99); +} + +.supplier-radio { + width: 20px; + height: 20px; + border: 2px solid var(--colorborder, #999); + border-radius: 50%; + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.supplier-option.selected .supplier-radio { + border-color: var(--butactionbg, #0077b3); +} + +.supplier-option.selected .supplier-radio::after { + content: ''; + width: 10px; + height: 10px; + background: var(--butactionbg, #0077b3); + border-radius: 50%; +} + +.supplier-info { + flex: 1; +} + +.supplier-name { + font-size: 15px; + font-weight: 500; +} + +.supplier-ref { + font-size: 12px; + color: var(--colortexttitlenotab, #888); + margin-top: 3px; +} + +.supplier-price { + font-size: 16px; + font-weight: 700; + color: var(--butactionbg, #0077b3); + text-align: right; + white-space: nowrap; +} + +.cheapest-badge { + background: #00b894; + color: #fff; + font-size: 10px; + font-weight: 700; + padding: 2px 6px; + border-radius: 3px; + margin-left: 8px; + text-transform: uppercase; +} + +/* Quantity Input */ +.quantity-section { + margin: 20px 0; + text-align: center; +} + +.quantity-label { + font-size: 14px; + font-weight: 600; + color: var(--colortexttitlenotab, #666); + margin-bottom: 10px; + display: block; +} + +.quantity-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.qty-btn { + width: 50px; + height: 50px; + border: 1px solid var(--colorborder, #ddd); + border-radius: 8px; + background: var(--colorbacklinepair1, #f8f8f8); + color: var(--colortextlink, #333); + font-size: 24px; + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.qty-btn:hover { + background: var(--colorbacktitle1, #e8e8e8); +} + +.qty-btn:active { + transform: scale(0.95); +} + +.qty-input { + width: 80px; + height: 50px; + text-align: center; + font-size: 20px; + font-weight: 700; + border: 1px solid var(--colorborder, #ddd); + border-radius: 8px; + background: var(--colorbacklinepair1, #f8f8f8); + color: var(--colortextlink, #333); +} + +.qty-input:focus { + border-color: var(--butactionbg, #0077b3); + outline: none; +} + +/* Action Buttons */ +.action-btn { + width: 100%; + padding: 14px; + font-size: 16px; + font-weight: 600; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + margin-top: 15px; +} + +.action-btn:active { + transform: scale(0.98); +} + +.btn-primary { + background: var(--butactionbg, #0077b3); + color: #fff; +} + +.btn-primary:hover { + opacity: 0.9; +} + +.btn-secondary { + background: var(--colorbacklinepair1, #f8f8f8); + color: var(--colortextlink, #333); + border: 1px solid var(--colorborder, #ddd); +} + +.btn-danger { + background: #dc3545; + color: #fff; +} + +/* Inventory Mode */ +.stock-display { + display: flex; + justify-content: space-around; + align-items: center; + padding: 20px; + background: var(--colorbacklinepair1, #f8f8f8); + border: 1px solid var(--colorborder, #ddd); + border-radius: 8px; + margin-bottom: 15px; +} + +.stock-item { + text-align: center; +} + +.stock-value { + font-size: 28px; + font-weight: 700; + color: var(--butactionbg, #0077b3); +} + +.stock-label { + font-size: 12px; + color: var(--colortexttitlenotab, #666); + text-transform: uppercase; + margin-top: 5px; +} + +.stock-arrow { + font-size: 24px; + color: var(--colortexttitlenotab, #999); +} + +/* Shop Mode Links */ +.shop-links { + margin-top: 15px; +} + +.shop-link-btn { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + background: var(--colorbacktitle1, #3b3c3e) !important; + border: 1px solid var(--colorborder, #2b2c2e) !important; + border-radius: 8px; + margin-bottom: 10px; + text-decoration: none; + color: var(--colortext, #ddd) !important; + transition: all 0.2s ease; + width: 100%; + cursor: pointer; + text-align: left; +} + +.shop-link-btn:hover { + background: var(--colorbackline, #444) !important; + text-decoration: none; +} + +.shop-link-btn:active { + transform: scale(0.99); +} + +.shop-link-name { + font-size: 15px; + font-weight: 600; + flex: 1; +} + +.shop-link-ref { + font-size: 12px; + color: var(--colortextmuted, #999); + margin: 0 15px; +} + +.shop-link-arrow { + font-size: 18px; + color: var(--butactionbg, #ad8c4f); +} + +/* Confirmation Dialog */ +.confirm-dialog { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + padding: 20px; +} + +.confirm-content { + background: #fff; + border-radius: 12px; + padding: 25px; + width: 100%; + max-width: 350px; + text-align: center; + box-shadow: 0 10px 40px rgba(0,0,0,0.3); +} + +.confirm-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 10px; +} + +.confirm-message { + font-size: 14px; + color: #666; + margin-bottom: 20px; +} + +.confirm-buttons { + display: flex; + gap: 10px; +} + +.confirm-buttons .action-btn { + flex: 1; + margin-top: 0; + padding: 12px; + font-size: 14px; +} + +/* Toast Notifications - use Dolibarr's setEventMessages style */ +.scanner-toast { + position: fixed; + bottom: 80px; + left: 50%; + transform: translateX(-50%); + padding: 12px 24px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + z-index: 10001; + animation: toastIn 0.3s ease; +} + +.scanner-toast.success { + background: #00b894; + color: #fff; +} + +.scanner-toast.error { + background: #dc3545; + color: #fff; +} + +@keyframes toastIn { + from { + opacity: 0; + transform: translateX(-50%) translateY(15px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +/* Error State */ +.error-message { + text-align: center; + padding: 30px 20px; + color: #dc3545; +} + +.error-icon { + font-size: 40px; + margin-bottom: 15px; +} + +.error-text { + font-size: 15px; +} + +/* No Supplier State */ +.no-supplier { + text-align: center; + padding: 15px; + background: #fff3cd; + border: 1px solid #ffc107; + border-radius: 8px; + color: #856404; + margin-bottom: 15px; +} + +/* Mobile Optimizations */ +@media (max-width: 768px) { + .scanner-wrapper { + padding: 0 10px; + } + + .scanner-video-box { + max-height: 250px; + } + + .qty-btn { + width: 60px; + height: 60px; + font-size: 28px; + } + + .qty-input { + width: 100px; + height: 60px; + font-size: 24px; + } + + .action-btn { + padding: 16px; + font-size: 17px; + } + + .supplier-option { + padding: 15px; + } + + .stock-value { + font-size: 32px; + } +} + +/* Touch optimizations */ +@media (pointer: coarse) { + .qty-btn, + .supplier-option, + .action-btn, + .shop-link-btn { + min-height: 48px; + } +} diff --git a/handybarcodescannerindex.php b/handybarcodescannerindex.php new file mode 100755 index 0000000..3b5f5ff --- /dev/null +++ b/handybarcodescannerindex.php @@ -0,0 +1,189 @@ + + * + * 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 handybarcodescanner/handybarcodescannerindex.php + * \ingroup handybarcodescanner + * \brief Mobile Barcode Scanner - Hauptseite (Dolibarr integriert) + */ + +// Load Dolibarr environment +$res = 0; +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +if (!$res && file_exists("../main.inc.php")) { + $res = @include "../main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} + +require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php'; + +// Load translation files +$langs->loadLangs(array("handybarcodescanner@handybarcodescanner", "products", "orders", "stocks")); + +// Get parameters +$action = GETPOST('action', 'aZ09'); +$mode = GETPOST('mode', 'alpha') ?: 'order'; + +// Security check +if (!$user->hasRight('handybarcodescanner', 'use')) { + // Fallback fuer aeltere Rechte-Pruefung + if (!$user->hasRight('fournisseur', 'commande', 'creer') && !$user->hasRight('supplier_order', 'creer')) { + accessforbidden('You need permission to use the barcode scanner'); + } +} + +// Check mode-specific permissions +$enableOrder = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_ORDER', 1); +$enableShop = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_SHOP', 1); +$enableInventory = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_INVENTORY', 1); + +/* + * View + */ + +$form = new Form($db); + +// CSS einbinden (JS wird manuell am Ende geladen) +$arrayofcss = array('/handybarcodescanner/css/scanner.css'); + +llxHeader('', $langs->trans("HandyBarcodeScanner"), '', '', 0, 0, array(), $arrayofcss, '', 'mod-handybarcodescanner page-scanner classforhorizontalscrolloftabs'); + +print '
'; + +// Mode Tabs - eigene Buttons (kein Seitenreload, Kamera bleibt aktiv) +print '
'; +if ($enableOrder) { + $activeClass = ($mode == 'order') ? ' active' : ''; + print ''; +} +if ($enableShop) { + $activeClass = ($mode == 'shop') ? ' active' : ''; + print ''; +} +if ($enableInventory) { + $activeClass = ($mode == 'inventory') ? ' active' : ''; + print ''; +} +print '
'; +// Inline Tab-Switch Logik (unabhaengig von scanner.js Ladereihenfolge) +print ''; + +// Scanner Container +print '
'; +print '
'; + +// Video Container +print '
'; +print ''; +print '
'; +print '
'; + +// Scanner Controls +print '
'; +print ''; +print ''; +print '
'; + +print '
'; // scanner-box +print '
'; // div-table-responsive + +// Last Scan Info +print '
'; +print ''.$langs->trans("LastScan").': '; +print '-'; +print '
'; + +// Result Area +print ''; + +print '
'; // scanner-wrapper + +// Hidden config for JavaScript +?> + + + +close(); diff --git a/img/README.md b/img/README.md new file mode 100755 index 0000000..4c40b8a --- /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 'handybarcodescanner.png@handybarcodescanner', you can put into this +directory a .png file called *object_handybarcodescanner.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@handybarcodescanner', then you can put into this +directory a .png file called *object_myobject.png* (16x16 or 32x32 pixels) + diff --git a/img/icon-192.png b/img/icon-192.png new file mode 100755 index 0000000000000000000000000000000000000000..c13fe0081aeadbc7431dcafd47df4641f9f9762c GIT binary patch literal 6704 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 literal 0 HcmV?d00001 diff --git a/img/icon-512.png b/img/icon-512.png new file mode 100755 index 0000000000000000000000000000000000000000..6dfc168c01e9b1dedf98b4c22dd9017934e7c856 GIT binary patch literal 10025 zcmeHtc~p~EyYGv%V(XB8wUs&mTBgrhL_rCIAy~B-A)q24vsM%V8Du7q!H+7Rb)Y5+ zG6gIMGKLu$LYyHo1c?xt5@ZSqLl_b=k>u`Rd+s`St$Xjk=d4rK;@x@o`+N5A>}T4~ z-ZxJ=*>C)E+m{doZA2YEdK!Y(fS+riuhxPhe`;L=D0W^rei{uy7$XQG{0c!bP(_%6 zpy+)N#6Jf?)(;^_J3OoL)FJR;?Jo}YM?nexq(_?Yp!g#ExJM)?&#nH?2mizXKeVDy z=wn*^HUHSC0ZGbSIzW|S)UmTs{^z4ET3?8`2nxt-pP7ZpJ~Na3@H6|(t@rJ-Hn-RV zp8NJuchtTAPXr-h{(+a`|L+7Hhc{gY1poYx9-;z6E=ES34+;N2Y4(HWKXsB$k2whj zB}5%Pd?s#Sb~rFgCpufO5EgChukq7P&9D2{d~IiU?z0{J{#)-Go{!vn{?+*?!maiH z{NX1v!iTnRH-A66F0^d}MZ@0Ea;L$$lN$C~r*`^kty_2WYYPv}o7bZH%H=z2!ta^z z9``8lw4C`9Vx9}F*!n?%ZIMgimnGE3@?!rNzfPAACNB9)jjiW~Vg~akKBnqha1PN2 zL}PhB8>XkV!iNtOEqA<|rsPDe>{prna`ZISJT2wBd*i<+M$C^G2;PP)MDNS{QUF$VzV)J0_o=F*4ya_8&q*|d8V(GuTQTK%BtklyxSgf|3^7Zr*N*) z{W|Fr+(1>ekn#qupPwE;cQuD|76I^zLLj)U}tl+2MuV6l${sKajo4{E~TE@3@JuE|Wu*56woW$zbC4)%UHTwXRGTAiBbTQHvQvv3^7<-wLU`dYa-f3&&!5l%&*KIOWp#izJvX zHSQ|&$g=wyyZXTP`?~>yn&&@^cnA{wdKw+tACJ8;)Ty3^>BO8pjkUGew7pMwABaxL z<2vaQX(*LRX)vNU6%Q9WqR0uMFI#bor18;PvHcf$Jq7kN)1MJsWB89<^mGu`MFZ0T zDK>kHT25_L%qNp>)2U(c4os#77@{U?V%FI<{S3@q&@%d4e(0>Fc=Q$t#n8dL=txJy zSgdtW*nnigC6*n@$ZxEAD_KAx^4Rem4-Ga)9=~X#o@Iti60~SiZ_wKz8$pqXsN@tK zWQgae(XW)&o6{w63*~Zs${>IKB}oPo*>qX&q4!G*vtxoV7GN&Q7dQK7>S<`RT;-H@ z%kPeu>0R~S2_fIFYtDk^1IENpms^rHov12plw?H`XGqu>b_1^n(47q&TYO|Qcbuh==;DNtwjG)=ZWIJyD7MFeflfE9hi4$_(8-OXI?VsdsU{^(<(>G>+14lKJCv7 z$r=8`vP=^bw6hijJqrr>sh}_Vv}#uxmTN=r_Nf{!L}u4a{JwR)wgtjFmS?XqQVsKM zdcMiVpv>J>oG3^@k-N$ULs+Z53lE3H4nG(lmFeqA?96x#N1Pv8RrBsO7VCXR_(CNf zxC*LIb550svjqm}5B3L__kSXbZ*sG*t2Y*1u;O0ObLg^(pD9u)1!!1pvFsdVx!AVb z#nX#qvUls^b5pz(y`h2ck`uJbYFU#^Qj?9xnMDb2Z!OGDt$A#sql0!9T@pQlx>I+% z^qtiYq(w~C@1-50>xc?l?*y-s{TAsHN|W*Z+lGU)vBGyKujR5WCPA1d@E)zu-&kro z±Z8#A`iL0J+_2YLpc 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 ZuGOya0v6*wkO>HN%;{+PPv`#izX0kN5Rd=> literal 0 HcmV?d00001 diff --git a/img/icon.svg b/img/icon.svg new file mode 100755 index 0000000..ee87e7d --- /dev/null +++ b/img/icon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/js/barcodezoom.js.php b/js/barcodezoom.js.php new file mode 100755 index 0000000..bc8d948 --- /dev/null +++ b/js/barcodezoom.js.php @@ -0,0 +1,179 @@ + + * + * Barcode Zoom Feature - Makes barcode images clickable for enlarged view + */ + +// Set correct content type +if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', 1); +if (!defined('NOLOGIN')) define('NOLOGIN', 1); +if (!defined('NOCSRFCHECK')) define('NOCSRFCHECK', 1); + +header('Content-Type: application/javascript; charset=UTF-8'); +header('Cache-Control: max-age=3600'); +?> +(function() { + "use strict"; + + // Only run on product card pages + if (window.location.pathname.indexOf('/product/card.php') === -1) { + return; + } + + // Add CSS + var style = document.createElement('style'); + style.textContent = ` +/* Barcode Zoom Modal */ +.barcode-zoom-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + cursor: pointer; +} + +.barcode-zoom-content { + background: #fff; + padding: 40px 50px; + border-radius: 12px; + text-align: center; + max-width: 95vw; + max-height: 95vh; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); +} + +.barcode-zoom-image { + max-width: 100%; + height: auto; + min-height: 150px; + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; +} + +.barcode-zoom-text { + font-family: monospace; + font-size: 32px; + color: #333; + letter-spacing: 4px; + margin-top: 20px; + font-weight: bold; +} + +.barcode-zoom-hint { + font-size: 13px; + color: #999; + margin-top: 20px; +} + +/* Make barcode image clickable */ +.barcode-clickable { + cursor: pointer !important; + transition: transform 0.2s, box-shadow 0.2s; + border: 2px solid transparent; + border-radius: 4px; +} + +.barcode-clickable:hover { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 119, 179, 0.3); + border-color: #0077b3; +} +`; + document.head.appendChild(style); + + // Find and enhance barcode images + function initBarcodeZoom() { + // Find all barcode images (from viewimage.php with barcode modulepart) + var barcodeImages = document.querySelectorAll('img[src*="modulepart=barcode"]'); + + barcodeImages.forEach(function(img) { + // Extract barcode value from URL + var src = img.src; + var codeMatch = src.match(/code=([^&]+)/); + if (!codeMatch) return; + + var barcodeValue = decodeURIComponent(codeMatch[1]); + + // Make image clickable + img.classList.add("barcode-clickable"); + img.title = "Klicken zum Vergroessern: " + barcodeValue; + img.style.cursor = "pointer"; + + // Add click handler + img.addEventListener("click", function(e) { + e.preventDefault(); + e.stopPropagation(); + showBarcodeZoom(src, barcodeValue); + }); + }); + } + + function showBarcodeZoom(imageSrc, barcodeValue) { + // Create overlay + var overlay = document.createElement("div"); + overlay.className = "barcode-zoom-overlay"; + overlay.onclick = function(e) { + if (e.target === overlay) { + overlay.remove(); + } + }; + + // Create content + var content = document.createElement("div"); + content.className = "barcode-zoom-content"; + content.onclick = function(e) { + e.stopPropagation(); + }; + + // Create large barcode image (modify URL for larger size) + var largeImg = document.createElement("img"); + largeImg.className = "barcode-zoom-image"; + // Dolibarr viewimage.php accepts width/height params for barcode + var largeSrc = imageSrc; + // Remove existing width/height if any + largeSrc = largeSrc.replace(/&width=\d+/g, "").replace(/&height=\d+/g, ""); + // Add larger dimensions + largeSrc += "&width=400&height=150"; + largeImg.src = largeSrc; + largeImg.alt = "Barcode: " + barcodeValue; + content.appendChild(largeImg); + + // Add barcode value text + var text = document.createElement("div"); + text.className = "barcode-zoom-text"; + text.textContent = barcodeValue; + content.appendChild(text); + + // Add hint + var hint = document.createElement("div"); + hint.className = "barcode-zoom-hint"; + hint.textContent = "Klicken Sie ausserhalb oder druecken Sie ESC zum Schliessen"; + content.appendChild(hint); + + overlay.appendChild(content); + document.body.appendChild(overlay); + + // Close on Escape key + var closeHandler = function(e) { + if (e.key === "Escape") { + overlay.remove(); + document.removeEventListener("keydown", closeHandler); + } + }; + document.addEventListener("keydown", closeHandler); + } + + // Initialize when DOM is ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initBarcodeZoom); + } else { + initBarcodeZoom(); + } +})(); diff --git a/js/scanner.js b/js/scanner.js new file mode 100755 index 0000000..9751c81 --- /dev/null +++ b/js/scanner.js @@ -0,0 +1,868 @@ +/** + * HandyBarcodeScanner - Mobile Barcode Scanner for Dolibarr + * Integrated version using SCANNER_CONFIG + */ + +(function() { + 'use strict'; + + // State + let CONFIG = null; + let currentMode = 'order'; + let initialized = false; + + // Global init function - can be called after login + window.initScanner = function() { + if (typeof window.SCANNER_CONFIG === 'undefined') { + console.log('HandyBarcodeScanner: Waiting for SCANNER_CONFIG...'); + return false; + } + + CONFIG = window.SCANNER_CONFIG; + + // Validate required config + if (!CONFIG.ajaxUrl || !CONFIG.token) { + console.error('HandyBarcodeScanner: Invalid configuration'); + return false; + } + + currentMode = CONFIG.mode || 'order'; + + // Only init once + if (initialized) { + console.log('HandyBarcodeScanner: Already initialized'); + return true; + } + + init(); + return true; + }; + + let isScanning = false; + let lastScannedCode = null; + let currentProduct = null; + let selectedSupplier = null; + let allSuppliers = []; + let quaggaInitialized = false; + + // Multi-read confirmation - code must be read multiple times to be accepted + let pendingCode = null; + let pendingCodeCount = 0; + const REQUIRED_CONFIRMATIONS = 2; // Code must be read 2 times to be accepted + + // Timeout: Hinweis wenn nichts erkannt wird + let scanTimeoutTimer = null; + const SCAN_TIMEOUT_MS = 8000; // 8 Sekunden ohne Erkennung + + // DOM Elements (will be set on init) + let elements = {}; + + // Initialize + function init() { + // Get DOM elements + elements = { + startBtn: document.getElementById('start-scan-btn'), + stopBtn: document.getElementById('stop-scan-btn'), + videoContainer: document.getElementById('scanner-video-container'), + video: document.getElementById('scanner-video'), + lastScanCode: document.getElementById('last-scan-code'), + resultArea: document.getElementById('result-area'), + manualInput: document.getElementById('manual-barcode-input'), + manualSearchBtn: document.getElementById('manual-search-btn') + }; + + if (!elements.startBtn || !elements.videoContainer) { + // Not on scanner page - exit silently + return; + } + + // Mark as initialized + initialized = true; + console.log('HandyBarcodeScanner: Initialized'); + + // Check for camera support + checkCameraSupport(); + + bindEvents(); + loadAllSuppliers(); + + // Globale Hooks fuer Tab-Switch (inline onclick) + window._scannerHideResult = hideResult; + window._scannerShowResult = showProductResult; + window._scannerCurrentProduct = null; + } + + // Check camera support - just log warnings, never disable button + function checkCameraSupport() { + // Check HTTPS + const isSecure = location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1'; + if (!isSecure) { + console.warn('HandyBarcodeScanner: HTTPS empfohlen fuer Kamerazugriff'); + } + + // Check if Quagga is loaded + if (typeof Quagga === 'undefined') { + console.error('HandyBarcodeScanner: Quagga nicht geladen'); + } + + // Check mediaDevices + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + console.warn('HandyBarcodeScanner: mediaDevices nicht gefunden'); + } + + // Never disable button - always allow click + } + + function showCameraError(message) { + const errorDiv = document.createElement('div'); + errorDiv.className = 'error-message'; + errorDiv.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #fff; text-align: center; padding: 20px;'; + errorDiv.innerHTML = '
⚠️
' + message + '
'; + elements.videoContainer.appendChild(errorDiv); + } + + // Event Bindings + function bindEvents() { + elements.startBtn.addEventListener('click', startScanner); + elements.stopBtn.addEventListener('click', stopScanner); + + // Manual barcode input + if (elements.manualSearchBtn && elements.manualInput) { + elements.manualSearchBtn.addEventListener('click', handleManualSearch); + elements.manualInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + handleManualSearch(); + } + }); + } + } + + // Handle manual barcode input + function handleManualSearch() { + const barcode = elements.manualInput.value.trim(); + if (!barcode) { + showToast('Bitte Barcode eingeben', 'error'); + return; + } + + // Update last scan display + elements.lastScanCode.textContent = barcode; + + // Vibration feedback + if (CONFIG.enableVibration && navigator.vibrate) { + navigator.vibrate(100); + } + + // Search product + searchProduct(barcode); + + // Clear input + elements.manualInput.value = ''; + } + + // Load all suppliers for manual selection + function loadAllSuppliers() { + fetch(CONFIG.ajaxUrl + 'getsuppliers.php?token=' + CONFIG.token) + .then(res => res.json()) + .then(data => { + if (data.success) { + allSuppliers = data.suppliers; + } + }) + .catch(err => console.error('Error loading suppliers:', err)); + } + + // Scanner Functions + function startScanner() { + if (isScanning) { + return; + } + if (elements.startBtn.disabled) { + return; + } + + // Check if Quagga is available + if (typeof Quagga === 'undefined') { + showToast('Quagga Bibliothek nicht geladen', 'error'); + return; + } + + // Disable button while initializing + elements.startBtn.disabled = true; + elements.startBtn.textContent = 'Initialisiere...'; + + // First request camera permission explicitly + navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }) + .then(function(stream) { + // Permission granted - stop the test stream + stream.getTracks().forEach(track => track.stop()); + + // Now start Quagga + initQuagga(); + }) + .catch(function(err) { + console.error('Camera permission error:', err); + elements.startBtn.disabled = false; + elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten'; + + let errorMsg = CONFIG.lang.cameraError || 'Kamerafehler'; + if (err.name === 'NotAllowedError') { + errorMsg = 'Kamerazugriff verweigert. Bitte Berechtigung in den Browser-Einstellungen erteilen.'; + } else if (err.name === 'NotFoundError') { + errorMsg = 'Keine Kamera gefunden.'; + } else if (err.name === 'NotReadableError') { + errorMsg = 'Kamera wird bereits verwendet.'; + } + showToast(errorMsg, 'error'); + }); + } + + function initQuagga() { + Quagga.init({ + inputStream: { + name: "Live", + type: "LiveStream", + target: elements.videoContainer, + constraints: { + facingMode: "environment", + width: { min: 640, ideal: 1280, max: 1920 }, + height: { min: 480, ideal: 720, max: 1080 } + }, + area: { // Scan-Bereich: groesserer Bereich fuer kleine Barcodes + top: "10%", + right: "5%", + left: "5%", + bottom: "10%" + } + }, + decoder: { + readers: [ + "code_128_reader", // Best for internal codes - prioritize + "ean_reader", + "ean_8_reader", + "code_39_reader" + ], + multiple: false + }, + locate: true, + locator: { + patchSize: "medium", // medium = besser fuer kleine Barcodes + halfSample: false // volle Aufloesung = bessere Genauigkeit + }, + numOfWorkers: navigator.hardwareConcurrency || 4, + frequency: 20 // haeufiger scannen fuer bessere Erkennung + }, function(err) { + if (err) { + console.error('Quagga init error:', err); + elements.startBtn.disabled = false; + elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten'; + showToast(CONFIG.lang.cameraError || 'Kamerafehler', 'error'); + return; + } + + // Success - start scanning + Quagga.start(); + quaggaInitialized = true; + isScanning = true; + elements.startBtn.classList.add('hidden'); + elements.stopBtn.classList.remove('hidden'); + elements.videoContainer.classList.add('scanning'); + + // Register detection handler + Quagga.onDetected(onBarcodeDetected); + + // Timeout-Timer starten + startScanTimeout(); + }); + } + + // Timeout-Hinweis wenn nichts erkannt wird + function startScanTimeout() { + clearTimeout(scanTimeoutTimer); + scanTimeoutTimer = setTimeout(function() { + if (isScanning) { + showToast('Kein Barcode erkannt – naeher ran oder Barcode manuell eingeben', 'error'); + // Timer erneut starten fuer wiederholten Hinweis + startScanTimeout(); + } + }, SCAN_TIMEOUT_MS); + } + + function stopScanner() { + if (!isScanning) return; + clearTimeout(scanTimeoutTimer); + + if (quaggaInitialized) { + Quagga.offDetected(onBarcodeDetected); + Quagga.stop(); + } + + isScanning = false; + quaggaInitialized = false; + elements.startBtn.classList.remove('hidden'); + elements.startBtn.disabled = false; + elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten'; + elements.stopBtn.classList.add('hidden'); + elements.videoContainer.classList.remove('scanning'); + } + + function onBarcodeDetected(result) { + let code = result.codeResult.code; + const format = result.codeResult.format; + + // EAN-13 detected but might be internal code with added check digit + // If it's 13 digits and doesn't validate, try removing the last digit + if (format === 'ean_13' || (code.length === 13 && /^\d{13}$/.test(code))) { + if (!validateEAN13(code)) { + // Try without last digit (might be internal 12-digit code) + const shortened = code.substring(0, 12); + console.log('HandyBarcodeScanner: Invalid EAN13, trying shortened:', shortened); + code = shortened; + } + } + + // Validate EAN8 checksum + if (format === 'ean_8' && code.length === 8 && /^\d{8}$/.test(code)) { + if (!validateEAN8(code)) { + // Try without last digit + code = code.substring(0, 7); + } + } + + // Already processed this code recently + if (code === lastScannedCode) return; + + // Multi-read confirmation: same code must be detected multiple times + if (code === pendingCode) { + pendingCodeCount++; + } else { + // Different code - reset counter + pendingCode = code; + pendingCodeCount = 1; + } + + // Not enough confirmations yet + if (pendingCodeCount < REQUIRED_CONFIRMATIONS) { + return; + } + + // Code confirmed! Accept it + lastScannedCode = code; + pendingCode = null; + pendingCodeCount = 0; + + // Timeout-Timer zuruecksetzen + startScanTimeout(); + + // Feedback + if (navigator.vibrate) { + navigator.vibrate([100, 50, 100]); + } + playBeep(); + + showToast('Barcode: ' + code, 'success'); + elements.lastScanCode.textContent = code; + + // Search product + searchProduct(code); + + // Reset duplicate prevention after 3 seconds + setTimeout(() => { + lastScannedCode = null; + }, 3000); + } + + // Validate EAN13 checksum + function validateEAN13(code) { + if (!/^\d{13}$/.test(code)) return false; + let sum = 0; + for (let i = 0; i < 12; i++) { + sum += parseInt(code[i]) * (i % 2 === 0 ? 1 : 3); + } + const checkDigit = (10 - (sum % 10)) % 10; + return checkDigit === parseInt(code[12]); + } + + // Validate EAN8 checksum + function validateEAN8(code) { + if (!/^\d{8}$/.test(code)) return false; + let sum = 0; + for (let i = 0; i < 7; i++) { + sum += parseInt(code[i]) * (i % 2 === 0 ? 3 : 1); + } + const checkDigit = (10 - (sum % 10)) % 10; + return checkDigit === parseInt(code[7]); + } + + // Shared AudioContext (must be created after user interaction) + let audioCtx = null; + + function getAudioContext() { + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + // Resume if suspended (mobile browsers) + if (audioCtx.state === 'suspended') { + audioCtx.resume(); + } + return audioCtx; + } + + // Play success beep sound (high pitch) + function playBeep() { + try { + const ctx = getAudioContext(); + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); + oscillator.connect(gainNode); + gainNode.connect(ctx.destination); + oscillator.frequency.value = 1200; // Higher pitch for success + oscillator.type = 'sine'; + gainNode.gain.value = 0.5; + oscillator.start(); + oscillator.stop(ctx.currentTime + 0.15); + } catch (e) { + console.log('Audio not available:', e); + } + } + + // Play error beep sound (low pitch, longer) + function playErrorBeep() { + try { + const ctx = getAudioContext(); + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); + oscillator.connect(gainNode); + gainNode.connect(ctx.destination); + oscillator.frequency.value = 300; // Low pitch for error + oscillator.type = 'square'; + gainNode.gain.value = 0.4; + oscillator.start(); + oscillator.stop(ctx.currentTime + 0.4); + } catch (e) { + console.log('Audio not available:', e); + } + } + + // Product Search + function searchProduct(barcode) { + showLoading(); + + fetch(CONFIG.ajaxUrl + 'findproduct.php?token=' + CONFIG.token + '&barcode=' + encodeURIComponent(barcode)) + .then(res => res.json()) + .then(data => { + hideLoading(); + if (data.success && data.product) { + currentProduct = data.product; + window._scannerCurrentProduct = data.product; + // Success feedback - double beep + playBeep(); + setTimeout(playBeep, 150); + if (navigator.vibrate) navigator.vibrate([100, 100, 100]); + showToast('Produkt gefunden: ' + data.product.label, 'success'); + showProductResult(data.product); + } else { + // Not found feedback - long vibration, error sound + if (navigator.vibrate) navigator.vibrate(500); + playErrorBeep(); + showNotFound(barcode); + } + }) + .catch(err => { + hideLoading(); + console.error('Search error:', err); + if (navigator.vibrate) navigator.vibrate(500); + showToast(CONFIG.lang.error, 'error'); + }); + } + + // Display Results based on Mode + function showProductResult(product) { + // Always read current mode from CONFIG (may have changed via tab click) + const mode = CONFIG.mode || currentMode; + + switch (mode) { + case 'order': + showOrderMode(product); + break; + case 'shop': + showShopMode(product); + break; + case 'inventory': + showInventoryMode(product); + break; + } + elements.resultArea.classList.remove('hidden'); + } + + // ORDER MODE + function showOrderMode(product) { + const suppliers = product.suppliers || []; + const cheapest = suppliers.length > 0 ? suppliers[0] : null; + + let suppliersHtml = ''; + + if (suppliers.length > 0) { + suppliersHtml = ` +
+ ${CONFIG.lang.selectSupplier}: + ${suppliers.map((s, i) => ` +
+
+
+
${escapeHtml(s.name)}${i === 0 ? '' + CONFIG.lang.cheapest + '' : ''}
+ ${s.ref_fourn ? '
' + CONFIG.lang.supplierOrderRef + ': ' + escapeHtml(s.ref_fourn) + '
' : ''} +
+
${formatPrice(s.price)} €
+
+ `).join('')} +
+ `; + selectedSupplier = cheapest; + } else { + // No supplier assigned - show all available suppliers + if (allSuppliers.length > 0) { + suppliersHtml = ` +
+
${CONFIG.lang.noSupplier}
+ ${CONFIG.lang.selectSupplier}: + ${allSuppliers.map((s, i) => ` +
+
+
+
${escapeHtml(s.name)}
+
+
+ `).join('')} +
+ `; + selectedSupplier = allSuppliers[0]; + } else { + suppliersHtml = `
${CONFIG.lang.noSupplier}
`; + } + } + + elements.resultArea.innerHTML = ` +
+
${escapeHtml(product.label)}
+
${CONFIG.lang.ref}: ${escapeHtml(product.ref)}
+
${CONFIG.lang.stock}: ${product.stock} ${escapeHtml(product.stock_unit || 'Stk')}
+
+ ${suppliersHtml} +
+ ${CONFIG.lang.quantity}: +
+ + + +
+
+ + `; + + bindOrderEvents(); + } + + function bindOrderEvents() { + // Supplier selection + document.querySelectorAll('.supplier-option').forEach(opt => { + opt.addEventListener('click', function() { + document.querySelectorAll('.supplier-option').forEach(o => o.classList.remove('selected')); + this.classList.add('selected'); + selectedSupplier = { + id: this.dataset.supplierId, + price: parseFloat(this.dataset.price) || 0 + }; + }); + }); + + // Quantity controls + const qtyInput = document.getElementById('qty-input'); + document.getElementById('qty-minus').addEventListener('click', () => { + const val = parseInt(qtyInput.value) || 1; + if (val > 1) qtyInput.value = val - 1; + }); + document.getElementById('qty-plus').addEventListener('click', () => { + const val = parseInt(qtyInput.value) || 1; + qtyInput.value = val + 1; + }); + + // Add to order + document.getElementById('add-to-order').addEventListener('click', addToOrder); + } + + function addToOrder() { + if (!currentProduct || !selectedSupplier) { + showToast(CONFIG.lang.error, 'error'); + return; + } + + const qty = parseInt(document.getElementById('qty-input').value) || 1; + + showLoading(); + + fetch(CONFIG.ajaxUrl + 'addtoorder.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: `token=${CONFIG.token}&product_id=${currentProduct.id}&supplier_id=${selectedSupplier.id}&qty=${qty}&price=${selectedSupplier.price || 0}` + }) + .then(res => res.json()) + .then(data => { + hideLoading(); + if (data.success) { + const productName = currentProduct.label || currentProduct.ref || 'Produkt'; + showToast(`${CONFIG.lang.added}: ${productName} (${qty}x)`, 'success'); + hideResult(); + } else { + showToast(data.error || CONFIG.lang.error, 'error'); + } + }) + .catch(err => { + hideLoading(); + console.error('Add to order error:', err); + showToast(CONFIG.lang.error, 'error'); + }); + } + + // SHOP MODE + function showShopMode(product) { + const suppliers = product.suppliers || []; + + let linksHtml = ''; + + if (suppliers.length > 0) { + linksHtml = suppliers.map(s => { + // shop_url + artikelnummer + let shopUrl = s.url || ''; + if (shopUrl && s.ref_fourn) { + shopUrl = shopUrl + s.ref_fourn; + } + + if (shopUrl) { + return ` + + ${escapeHtml(s.name)} + ${s.ref_fourn ? '' + escapeHtml(s.ref_fourn) + '' : ''} + + + `; + } + return ` + + `; + }).join(''); + } else { + linksHtml = `
${CONFIG.lang.noSupplier}
`; + } + + elements.resultArea.innerHTML = ` +
+
${escapeHtml(product.label)}
+
${CONFIG.lang.ref}: ${escapeHtml(product.ref)}
+
${CONFIG.lang.stock}: ${product.stock} ${escapeHtml(product.stock_unit || 'Stk')}
+
+ + `; + } + + // INVENTORY MODE + function showInventoryMode(product) { + elements.resultArea.innerHTML = ` +
+
${escapeHtml(product.label)}
+
${CONFIG.lang.ref}: ${escapeHtml(product.ref)}
+
+
+
+
${product.stock}
+
${CONFIG.lang.currentStock}
+
+
+
+
${product.stock}
+
${CONFIG.lang.newStock}
+
+
+
+ ${CONFIG.lang.newStock}: +
+ + + +
+
+ + `; + + bindInventoryEvents(product.stock); + } + + function bindInventoryEvents(originalStock) { + const stockInput = document.getElementById('stock-input'); + const preview = document.getElementById('new-stock-preview'); + + function updatePreview() { + preview.textContent = stockInput.value; + } + + stockInput.addEventListener('input', updatePreview); + + document.getElementById('stock-minus').addEventListener('click', () => { + const val = parseInt(stockInput.value) || 0; + if (val > 0) { + stockInput.value = val - 1; + updatePreview(); + } + }); + + document.getElementById('stock-plus').addEventListener('click', () => { + const val = parseInt(stockInput.value) || 0; + stockInput.value = val + 1; + updatePreview(); + }); + + document.getElementById('save-stock').addEventListener('click', () => { + const newStock = parseInt(stockInput.value) || 0; + if (newStock !== originalStock) { + showConfirmDialog(originalStock, newStock); + } else { + showToast(CONFIG.lang.saved, 'success'); + hideResult(); + } + }); + } + + function showConfirmDialog(oldStock, newStock) { + const dialog = document.createElement('div'); + dialog.className = 'confirm-dialog'; + dialog.innerHTML = ` +
+
${CONFIG.lang.confirmStockChange}
+
${oldStock} → ${newStock}
+
+ + +
+
+ `; + document.body.appendChild(dialog); + + document.getElementById('confirm-cancel').addEventListener('click', () => { + dialog.remove(); + }); + + document.getElementById('confirm-ok').addEventListener('click', () => { + dialog.remove(); + saveStock(newStock); + }); + } + + function saveStock(newStock) { + if (!currentProduct) return; + + showLoading(); + + fetch(CONFIG.ajaxUrl + 'updatestock.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: `token=${CONFIG.token}&product_id=${currentProduct.id}&stock=${newStock}` + }) + .then(res => res.json()) + .then(data => { + hideLoading(); + if (data.success) { + showToast(CONFIG.lang.saved, 'success'); + hideResult(); + } else { + showToast(data.error || CONFIG.lang.error, 'error'); + } + }) + .catch(err => { + hideLoading(); + console.error('Save stock error:', err); + showToast(CONFIG.lang.error, 'error'); + }); + } + + // Not Found + function showNotFound(barcode) { + elements.resultArea.innerHTML = ` +
+
+
${CONFIG.lang.productNotFound}
+
${escapeHtml(barcode)}
+
+ `; + elements.resultArea.classList.remove('hidden'); + } + + // UI Helpers + function hideResult() { + elements.resultArea.classList.add('hidden'); + elements.resultArea.innerHTML = ''; + currentProduct = null; + window._scannerCurrentProduct = null; + selectedSupplier = null; + } + + function showLoading() { + // Use jQuery blockUI if available (Dolibarr standard) + if (typeof jQuery !== 'undefined' && jQuery.blockUI) { + jQuery.blockUI({ message: '
' }); + } + } + + function hideLoading() { + if (typeof jQuery !== 'undefined' && jQuery.unblockUI) { + jQuery.unblockUI(); + } + } + + function showToast(message, type = 'success') { + // Remove existing toasts + document.querySelectorAll('.scanner-toast').forEach(t => t.remove()); + + const toast = document.createElement('div'); + toast.className = 'scanner-toast ' + type; + toast.textContent = message; + document.body.appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, 3000); + } + + function escapeHtml(str) { + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + function formatPrice(price) { + return parseFloat(price).toFixed(2).replace('.', ','); + } + + // Start when DOM is ready (only if CONFIG already exists) + function tryInit() { + if (typeof window.SCANNER_CONFIG !== 'undefined') { + window.initScanner(); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', tryInit); + } else { + tryInit(); + } +})(); diff --git a/langs/de_DE/handybarcodescanner.lang b/langs/de_DE/handybarcodescanner.lang new file mode 100755 index 0000000..68a356c --- /dev/null +++ b/langs/de_DE/handybarcodescanner.lang @@ -0,0 +1,83 @@ +# Dolibarr language file - de_DE - HandyBarcodeScanner +# Copyright (C) 2026 Eduard Wisch + +# Module +ModuleHandyBarcodeScannerName = Handy Barcode Scanner +ModuleHandyBarcodeScannerDesc = Mobiler Barcode-Scanner fuer Bestellungen, Shop-Links und Inventur +HandyBarcodeScannerDescription = Mobiler Barcode-Scanner fuer Bestellungen, Shop-Links und Inventur +HandyBarcodeScannerArea = Barcode Scanner + +# Scanner +HandyBarcodeScanner = Barcode Scanner +StartScan = Scannen starten +StopScan = Scannen stoppen +LastScan = Letzter Scan +CameraAccessError = Kamera-Zugriff fehlgeschlagen. Bitte Berechtigung erteilen. +BarcodeManualInput = Barcode manuell eingeben... +Search = Suchen + +# Modes +Order = Bestellen +Shop = Shop +Inventory = Inventur + +# Product +ProductNotFound = Produkt nicht gefunden +Ref = Referenz +Stock = Lagerbestand +CurrentStock = Aktueller Bestand +NewStock = Neuer Bestand + +# Suppliers +SelectSupplier = Lieferant waehlen +NoSupplierForProduct = Kein Lieferant fuer dieses Produkt hinterlegt +Cheapest = Guenstigster +SupplierOrderRef = Lieferantenbestellnummer + +# Order +Quantity = Menge +Add = Hinzufuegen +Added = Produkt eingestellt +Price = Preis + +# Inventory +Save = Speichern +Saved = Gespeichert +ConfirmStockChange = Bestandsaenderung bestaetigen + +# Shop +OpenShop = Shop oeffnen + +# General +Cancel = Abbrechen +Confirm = Bestaetigen +Error = Fehler + +# Admin Settings +HandyBarcodeScannerSetup = Barcode Scanner Einstellungen +SettingsPage = Einstellungen +DefaultWarehouse = Standard-Lager fuer Inventur +DefaultWarehouseDesc = Das Lager, in dem Bestandsaenderungen gebucht werden +OrderPrefix = Bestellungs-Praefix +OrderPrefixDesc = Praefix fuer automatisch erstellte Lieferantenbestellungen (z.B. "Direktbestellung") +EnableOrderMode = Bestell-Modus aktivieren +EnableShopMode = Shop-Modus aktivieren +EnableInventoryMode = Inventur-Modus aktivieren + +# Permissions +Permission500241 = Barcode Scanner benutzen +Permission500242 = Bestellungen per Scanner erstellen +Permission500243 = Inventur per Scanner durchfuehren + +# Admin Page +General = Allgemein +EnabledModes = Aktivierte Modi +Feedback = Feedback +DefaultWarehouseDesc2 = Das Lager, in dem Bestandsaenderungen bei der Inventur gebucht werden +FirstAvailable = Erstes verfuegbares +EnableVibration = Vibration bei Scan +EnableVibrationDesc = Handy vibriert bei erfolgreichem Scan +EnableSound = Ton bei Scan +EnableSoundDesc = Akustisches Signal bei erfolgreichem Scan +MobileAccess = Mobiler Zugriff +ScanQRCodeOrOpenURL = QR-Code scannen oder URL oeffnen diff --git a/langs/en_US/handybarcodescanner.lang b/langs/en_US/handybarcodescanner.lang new file mode 100755 index 0000000..002367d --- /dev/null +++ b/langs/en_US/handybarcodescanner.lang @@ -0,0 +1,83 @@ +# Dolibarr language file - en_US - HandyBarcodeScanner +# Copyright (C) 2026 Eduard Wisch + +# Module +ModuleHandyBarcodeScannerName = Handy Barcode Scanner +ModuleHandyBarcodeScannerDesc = Mobile barcode scanner for orders, shop links and inventory +HandyBarcodeScannerDescription = Mobile barcode scanner for orders, shop links and inventory +HandyBarcodeScannerArea = Barcode Scanner + +# Scanner +HandyBarcodeScanner = Barcode Scanner +StartScan = Start Scan +StopScan = Stop Scan +LastScan = Last Scan +CameraAccessError = Camera access failed. Please grant permission. + +# Modes +Order = Order +Shop = Shop +Inventory = Inventory + +# Product +ProductNotFound = Product not found +Ref = Reference +Stock = Stock +CurrentStock = Current Stock +NewStock = New Stock + +# Suppliers +SelectSupplier = Select Supplier +NoSupplierForProduct = No supplier assigned to this product +Cheapest = Cheapest + +# Order +Quantity = Quantity +Add = Add +Added = Added +Price = Price + +# Inventory +Save = Save +Saved = Saved +ConfirmStockChange = Confirm stock change + +# Shop +OpenShop = Open Shop + +# General +Cancel = Cancel +Confirm = Confirm +Error = Error + +# Admin Settings +HandyBarcodeScannerSetup = Barcode Scanner Settings +Settings = Settings +HandyBarcodeScannerSetupPage = Configure your barcode scanner module settings +About = About +HandyBarcodeScannerAbout = About HandyBarcodeScanner +HandyBarcodeScannerAboutPage = Information about the HandyBarcodeScanner module + +# Permissions +Permission500241 = Use barcode scanner +Permission500242 = Create orders via scanner +Permission500243 = Do inventory via scanner + +# Admin Page +General = General +EnabledModes = Enabled Modes +Feedback = Feedback +OrderPrefix = Order Prefix +OrderPrefixDesc = Prefix for automatically created supplier orders (e.g. "DirectOrder-SupplierName") +DefaultWarehouse = Default Warehouse +DefaultWarehouseDesc = Warehouse where stock changes are booked during inventory +FirstAvailable = First available +EnableOrderMode = Enable Order Mode +EnableShopMode = Enable Shop Mode +EnableInventoryMode = Enable Inventory Mode +EnableVibration = Vibration on Scan +EnableVibrationDesc = Phone vibrates on successful scan +EnableSound = Sound on Scan +EnableSoundDesc = Acoustic signal on successful scan +MobileAccess = Mobile Access +ScanQRCodeOrOpenURL = Scan QR code or open URL diff --git a/lib/handybarcodescanner.lib.php b/lib/handybarcodescanner.lib.php new file mode 100755 index 0000000..8f28a0b --- /dev/null +++ b/lib/handybarcodescanner.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 handybarcodescanner/lib/handybarcodescanner.lib.php + * \ingroup handybarcodescanner + * \brief Library files with common functions for HandyBarcodeScanner + */ + +/** + * Prepare admin pages header + * + * @return array + */ +function handybarcodescannerAdminPrepareHead() +{ + global $langs, $conf; + + // global $db; + // $extrafields = new ExtraFields($db); + // $extrafields->fetch_name_optionals_label('myobject'); + + $langs->load("handybarcodescanner@handybarcodescanner"); + + $h = 0; + $head = array(); + + $head[$h][0] = dol_buildpath("/handybarcodescanner/admin/setup.php", 1); + $head[$h][1] = $langs->trans("Settings"); + $head[$h][2] = 'settings'; + $h++; + + /* + $head[$h][0] = dol_buildpath("/handybarcodescanner/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("/handybarcodescanner/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("/handybarcodescanner/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:@handybarcodescanner:/handybarcodescanner/mypage.php?id=__ID__' + //); // to add new tab + //$this->tabs = array( + // 'entity:-tabname:Title:@handybarcodescanner:/handybarcodescanner/mypage.php?id=__ID__' + //); // to remove a tab + complete_head_from_modules($conf, $langs, null, $head, $h, 'handybarcodescanner@handybarcodescanner'); + + complete_head_from_modules($conf, $langs, null, $head, $h, 'handybarcodescanner@handybarcodescanner', 'remove'); + + return $head; +} diff --git a/manifest.json b/manifest.json new file mode 100755 index 0000000..471ec79 --- /dev/null +++ b/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "Barcode Scanner", + "short_name": "Scanner", + "description": "Barcode Scanner für Dolibarr - Bestellen, Shop-Links, Inventur", + "start_url": "./pwa.php", + "scope": "./", + "display": "standalone", + "orientation": "portrait", + "background_color": "#1d1e20", + "theme_color": "#0077b3", + "icons": [ + { + "src": "img/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "img/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "categories": ["business", "productivity"], + "lang": "de-DE" +} diff --git a/modulebuilder.txt b/modulebuilder.txt new file mode 100755 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/pwa.php b/pwa.php new file mode 100755 index 0000000..d37ed1b --- /dev/null +++ b/pwa.php @@ -0,0 +1,829 @@ + + * + * PWA Standalone Scanner - Mit eigenem Login (15 Tage gespeichert) + */ + +// Kein Login erforderlich - wird via JavaScript geprueft +if (!defined('NOLOGIN')) { + define('NOLOGIN', '1'); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} + +// Load Dolibarr environment +$res = 0; +if (!$res && file_exists("../main.inc.php")) { + $res = @include "../main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Dolibarr konnte nicht geladen werden"); +} + +// Load translation files +$langs->loadLangs(array("handybarcodescanner@handybarcodescanner", "products", "orders", "stocks")); + +// Get parameters +$mode = GETPOST('mode', 'alpha') ?: 'order'; + +// Check mode-specific permissions +$enableOrder = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_ORDER', 1); +$enableShop = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_SHOP', 1); +$enableInventory = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_INVENTORY', 1); + +// Get Dolibarr theme colors +$colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3'); + +?> + + + + + + + + + Barcode Scanner + + + + + + + +
+
+
+ + + + + + + + + + + + diff --git a/sql/dolibarr_allversions.sql b/sql/dolibarr_allversions.sql new file mode 100755 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. +-- diff --git a/sw.js b/sw.js new file mode 100755 index 0000000..fc3e6a3 --- /dev/null +++ b/sw.js @@ -0,0 +1,82 @@ +// Service Worker for HandyBarcodeScanner PWA +const CACHE_NAME = 'scanner-v4.4'; +const ASSETS = [ + 'pwa.php', + 'css/scanner.css', + 'js/scanner.js', + 'img/icon-192.png', + 'img/icon-512.png', + 'https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js' +]; + +// Install - cache assets +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME).then(cache => { + return cache.addAll(ASSETS); + }) + ); + self.skipWaiting(); +}); + +// Activate - clean old caches +self.addEventListener('activate', event => { + event.waitUntil( + caches.keys().then(keys => { + return Promise.all( + keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)) + ); + }) + ); + self.clients.claim(); +}); + +// Fetch - network first, fallback to cache +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + + // Always fetch AJAX requests from network (include PWA auth endpoints) + if (url.pathname.includes('/ajax/')) { + event.respondWith( + fetch(event.request).catch(err => { + console.error('Network error for AJAX:', err); + return new Response(JSON.stringify({ + success: false, + error: 'Offline - Keine Netzwerkverbindung' + }), { + headers: {'Content-Type': 'application/json'} + }); + }) + ); + return; + } + + // Network first for PHP pages (to get fresh token) + if (url.pathname.endsWith('.php')) { + event.respondWith( + fetch(event.request).catch(() => caches.match(event.request)) + ); + return; + } + + // Cache first for static assets + event.respondWith( + caches.match(event.request).then(cached => { + return cached || fetch(event.request).then(response => { + // Cache successful responses + if (response.ok) { + const clone = response.clone(); + caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); + } + return response; + }); + }) + ); +}); + +// Listen for messages from main app +self.addEventListener('message', event => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } +});