commit e9d910c23a25cc8b3813a16b09728e3213e535d5 Author: Oscar Alvarez Date: Sun Apr 19 10:54:08 2020 -0500 Migrated from hg diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..bce7cc7 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,16 @@ +Copyright (C) 2012-2015 Oscar Alvarez. +Copyright (C) 2008-2012 Cédric Krier. +Copyright (C) 2008-2012 B2CK SPRL. + +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 . diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..621632e --- /dev/null +++ b/INSTALL @@ -0,0 +1,56 @@ +Installing Tryton POS Client +============================ + +Prerequisites Client +-------------------- + + * Python 3.5 or later (http://www.python.org/) + * python-setuptools + * python3-pyqt5 + * python3-pyqt5.qtsvg + * python3-dateutil + * python-pip + * libusb-1.0-0 + * libusb-1.0-0-dev + * python3-dev + * python3-pil + + * neo (https://bitbucket.org/presik/neo) + + * Optional, Pip packages: + pip3 install pyusb + pip3 install pillow + pip3 install qrcode + pip3 install paramiko + pip3 install pycups + +Prerequisites Server Modules +---------------------------- + + * trytond_account_invoice (http://www.tryton.org/) + * trytond_sale (http://www.tryton.org/) + * trytond_sale_invoice_grouping (http://www.tryton.org/) + * trytond_sale_payment (http://www.bitbucket.org/) + * trytond_sale_shop (http://www.bitbucket.org/) + * trytond_sale_pos (http://www.bitbucket.org/) + * trytonpsk_sale_salesman (http://www.bitbucket.org/) + * trytonpsk_sale_w_tax (http://www.bitbucket.org/) + * trytonpsk_sale_pos_frontend (https://bitbucket.org/presik/trytonpsk_sale_pos_frontend) + * trytonpsk_product_onebarcode (http://www.bitbucket.org/) + + +pip3 install pyserial +sudo adduser myuser dialout + + +Installation +------------ + +Once you've downloaded and unpacked the pos_client_qt5 source release, enter the +directory where the archive was unpacked, and run: + + python setup.py install + +Note that you may need administrator/root privileges for this step, as +this command will by default attempt to install module to the Python +site-packages directory on your system. diff --git a/INSTALL_es b/INSTALL_es new file mode 100644 index 0000000..a95394f --- /dev/null +++ b/INSTALL_es @@ -0,0 +1,352 @@ +Instalación de Tryton POS Client Qt5 +==================================== + +Esta versión solo es compatible con Tryton 4.0+, se asume que el usuario tiene +conocimientos básicos previos sobre la instalación y configuración de Tryton, +especialmente los modulos oficiales relacionados con contabilidad y ventas, no +es el objeto de esta guia abordar temas de configuración básica. + +Para poder instalar Tryton POS Client Qt5 se requieren los siguientes +paquetes instalados en el sistema: + +En Debian, Ubuntu y Derivados, se recomienda usar: $ apt install paquete + + * Python 3.5 or later (http://www.python.org/) + * python-setuptools + * python3-pyqt5 + * python3-pyqt5.qtsvg + * python3-dateutil + * python3-pip + * libusb-1.0-0 + * libusb-1.0-0-dev + * libcups2-dev + * gcc + * python-dev + * python3-dev + * python3-pil + + * neo (https://bitbucket.org/presik/neox) + +Los siguientes paquetes se deben instalar usando PIP + + pip3 install pyserial + pip3 install pyusb + pip3 install pillow + pip3 install qrcode + pip3 install paramiko + pip3 install pycups + + +Tener en cuenta que algunos paquetes se deben instalar con pip para python3. + +Nota: el Cliente POS de momento ha sido testeado en Windows parcialmente, asi que +no hay garantia de que funcione al 100% en este OS. + +Se recomienda instalar Tryton 4.0 creando un ambiente virtual con +virtualenv. + +Los siguientes módulos se deben instalar en la base de datos Tryton +y deben estar configurados al 100% (se ampliara explicación de la configuración +más adelante). + +Los modulos deberian instalarse aproximadamente en el orden en que estan +listados de arriba a abajo, empezando por Oficiales, y luego los modulos +Presik. + + +Modulos Oficiales +---------------------------------------------------------------------------- + * trytond_account (http://www.tryton.org/) + * trytond_account_invoice (http://www.tryton.org/) + * trytond_stock (http://www.tryton.org/) + * trytond_sale (http://www.tryton.org/) + * trytond_sale_price_list (http://www.tryton.org/) + * trytond_sale_invoice_grouping (http://www.tryton.org/) + * trytond_account_statement (http://www.tryton.org/) + +Sugerencia, instalar en el ambiente virtual usando: + +$ pip3 install trytond_module + + +Ingresar al directorio e instalar (dentro del ambiente virtual +anteriormente creado): + + $ python3 setup.py install + + +Modulos No Oficiales (Presik) +---------------------------------------------------------------------------- + * trytonpsk_sale_payment (https://bitbucket.org/presik/trytonpsk_sale_payment) + * trytonpsk_sale_w_tax (https://bitbucket.org/presik/trytonpsk_sale_w_tax) + * trytonpsk_sale_shop (https://bitbucket.org/presik/trytonpsk_sale_shop) + * trytonpsk_sale_pos (https://bitbucket.org/presik/trytonpsk_sale_pos) + * trytonpsk_product_onebarcode (https://bitbucket.org/presik/trytonpsk_product_onebarcode) + * trytonpsk_sale_salesman (https://bitbucket.org/presik/trytonpsk_sale_salesman) + * trytonpsk_sale_discount (https://bitbucket.org/presik/trytonpsk_sale_discount) + * trytonpsk_sale_pos_frontend (https://bitbucket.org/presik/trytonpsk_sale_pos_frontend) + +Ingresar al directorio e instalar (dentro del ambiente virtual +anteriormente creado): + +Sugerencia, descargar los paquetes desde bitbucket usando en el terminal: + + $ hg clone paquete + +Luego instalar: + + $ python3 setup.py install + + +Nota: Verificar todos los modulos anteriores esten en la versión correcta, +si alguno tiene una versión distinta seguramente el POS no funcionará. + + + +CREACION DE BASE DE DATOS Y CONFIGURACIÓN DE VENTAS +---------------------------------------------------------------------------- + +Se asume que se tienen conocimientos previos sobre Tryton por lo cual se +resumiran los pasos: + +- Crear una base de datos + +- Instalar los modulos oficiales en la base de datos: + * Administración > Modulos > Modulos + +- Instalar los modulos Presik: + * Administración > Modulos > Modulos + +- Crear la Compañia, Año Fiscal, Plan de Cuentas, Formas de Pago, etc. + + +Configuración del Modulo de Ventas (especialmente sale_shop, sale_pos) + +- Crear un Libro de Estado de Cuenta: + + Contabilidad > Configuración > Estados de Cuenta > Libros de Estado de Cuenta + + +En el módulo de Ventas > Configuración: + +- Crear una "Tienda" (Shop) + +- Crear un "Terminal de Venta" y asignarle al menos un Libro Contable. + Ej: Efectivo + + +En Terceros: + +- Crear el "Tercero" que sera usado en el POS y asignarlo a las tiendas por defecto. + +- Crear al menos un "Empleado" (que será usado como un vendedor cuando la venta +lo requiera) + + +En el módulo Administración: + +- Crear un usuario POS, en: + "Administración > Usuarios > Preferencias" + +Asignarle la tienda, y el terminal de venta creado anteriormente, +asi mismo verificar que este usuario tenga "Permisos de Acceso" para el modulo +de Ventas. + +Tambien en la pestaña "Permisos de Acceso" marcar los campos: + +Usuario POS Frontend [X] + +Usuario Borra Ventas POS [X] + +Este último es opcional si es aplica. + + + + +INSTALACION Y CONFIGURACION DEL CLIENTE POS +---------------------------------------------------------------------------- + +Requisitos del Cliente POS: + + * libjpeg8 + * libjpeg62-dev + * libfreetype6 + * libfreetype6-dev + + +Instalelos con: apt-get install paquete + +Ahora se debe descargar e instalar el modulo python_escpos, el cual +es requerido para que funcionen las impresoras POS: + +$ hg clone https://bitbucket.org/presik/python_escpos + +$ cd python_escpos + +$ python setup.py install + + +Descargar el Cliente POS, usando el comando hg clone: + +https://bitbucket.org/presik/presik_pos + +Con esto obtendrá la ultima version de desarrollo. + +Descargue el microframework NEO, desarrollado por presik: + +https://bitbucket.org/presik/neo + +Instalelo dentro de la carpeta principal de presik_pos, +los directorios deben quedar así: + +|__presik_pos + |__app + |__doc + |__neo + +Poner la carpeta descargada en algun lugar de su directorio /home/usuario/miaplicacion + +Crear el directorio ".tryton" en el directorio HOME del usuario, ejemplo +si mi usuario se llama "pedro", la ruta debe quedar así: + + /home/pedro/.tryton + +Copiar el archivo "config_pos.ini" del paquete descargado al directorio +creado ".tryton" + +Al final del proceso debe quedar así: + + /home/myuser/.tryton/config_pos.ini + +El archivo config_pos.ini define la información de la configuración del +terminal de venta, y los parametros de conexión, si el archivo queda +mal o con algún error es probable que el cliente NO funcione o genere errores. + + +Archivo de Configuración del POS +-------------------------------- +A continuación se explicaran los principales campos en este archivo de +configuración: + + #Valid protocols: xml, json, local + protocol=xml + +El protocolo debe ser 'xml' tal como se ve en el archivo por defecto, +'json' presenta un bug que no se ha resuelto. + + server=192.168.X.XX + +El servidor al que se va a conectar el terminal. + + port=8000 + +El puerto de conexión + + database=DEMO40 + +El nombre de la base de datos creada + + + user=admin + +El usuario por defecto para el terminal de venta, este debe tener permisos para +ingresar al terminal, por defecto es admin. + + +Para configurar la impresora se maneja el siguiente formato: + + printer_sale_name=interface,rutadelaimpresora + +Las interfaces válidas son tres: usb, cups, network y ssh (en este caso instalar openssh-server) + + Ejemplos + + printer_sale_name=usb,/dev/usb/lp0 + + printer_sale_name=cups,EPSON-TM-T20 + + printer_sale_name=ssh,usuario@contraseña@ipdelequiporemoto@puerto@/dev/usb/lpX + + + + +Id de dispositivo: + + device_id=CAJA-10 + +El numero Id de la caja a conectarse tal como fue creada en Tryton, en +Terminales de Venta, para hallarla se debe usar el boton de herramientas, +y click en la opción "Ver Registro..", en el formulario de la Tienda. + + + +Haga una venta desde Tryton sin POS +----------------------------------- + +Asi que en este punto sin necesidad del Cliente POS usted debería ser +capaz a través del cliente Tryton 4.0, de hacer ventas: + + >> Ventas > Ventas POS + +En caso de no poder hacer ventas a través de este módulo probablemente +tampoco las podra hacer por el Cliente POS. + +Recuerde que para iniciar a hacer ventas debe abrir primero los "Estados +de Cuenta" a través del Wizard en el Módulo de Ventas: + >> Ventas + > Estados de Cuenta + > "Abrir Estados de Cuenta" + +Y al finalizar el día/turno del cajero debería ir a la misma ruta y +"Cerrar los Estados de Cuenta", con el fin de que Tryton contabilice +los pagos y marque las facturas como pagadas. + + +Ejecutar el Cliente Presik POS +------------------------------ + +Antes de ejecutar el cliente es muy importante verificar que Tryton +esta perfectamente configurado y permite hacer ventas por el usuario creado. + +Antes de ejecutar el cliente debe asegurarse de que el servidor este +ejecutandose. Para ejecutar el cliente ingrese al directorio +"presik_pos" y ejecute: + + $ python3 pospro + + + +Manejo Básico del Cliente POS +----------------------------- + +El cliente POS funciona con los códigos de producto, asi que los +productos que no tengan código, no podrán ingresarse. + +Para testear el sistema debe crear un producto tipo "Articulo" +vendible, con código. + + +Oprima la tecla F1 para ver los principales atajos de teclado. + +* Ingresar productos: +Para ingresar un producto, digite el código del producto, y oprima la tecla [+] + + +* Terminar la venta: +Oprima dos veces la tecla [intro], lo cual hará que el sistema pida el valor a +pagar. En la barra superior en el campo de información. + + +* Ingresar el pago: +Ingrese el valor en billetes, monedas de pago del cliente, y oprima la tecla [+], +para confirmar el pago. + + +* Impresion de factura: +Si la impresora esta bien configurada, la impresión de la factura es automática, +en caso de no haber impresora el sistema permite continuar a la siguiente venta, +si desea reimprimir la factura utilice la tecla [F7]. + + +Cualquier bug o error, puede reportarse en bitbucket. Para asesoria +o soporte adicional, puede contactarme: oscar.alvarez.montero@gmail.com diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + 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 + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README b/README new file mode 100644 index 0000000..59b1fd3 --- /dev/null +++ b/README @@ -0,0 +1,46 @@ +Presik POS Client for Tryton +========================================= + +The Point of Sale Client for Tryton application platform development +in Qt5 and Python3. + +Installing +---------- + +See INSTALL + +Support +------- + +If you encounter any problems with Tryton, please don't hesitate to ask +questions on the Tryton bug tracker, mailing list, wiki or IRC channel: + + https://bitbucket.org/presik/presik_pos + https://www.presik.com + http://bugs.tryton.org/ + http://groups.tryton.org/ + http://wiki.tryton.org/ + irc://irc.freenode.net/tryton + +License +------- + +See LICENSE + + +Update Translation +------------------ +Modify your i18 file and execute in terminal: + +$lrelease i18n_es.ts + + +Copyright +--------- + +See COPYRIGHT + + +For more information please visit the Tryton web site: + + http://www.bitbucket.org/ diff --git a/TODO b/TODO new file mode 100644 index 0000000..87e4fab --- /dev/null +++ b/TODO @@ -0,0 +1,5 @@ +TODO + +- Adicionar funcionamiento del POS modo standlone + +- Usar voz para informar al usuario diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..8e392fe --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,3 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +__version__ = "5.0.0" diff --git a/app/__pycache__/__init__.cpython-37.pyc b/app/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..a6c1250 Binary files /dev/null and b/app/__pycache__/__init__.cpython-37.pyc differ diff --git a/app/__pycache__/buttonpad.cpython-37.pyc b/app/__pycache__/buttonpad.cpython-37.pyc new file mode 100644 index 0000000..53db8fb Binary files /dev/null and b/app/__pycache__/buttonpad.cpython-37.pyc differ diff --git a/app/__pycache__/common.cpython-37.pyc b/app/__pycache__/common.cpython-37.pyc new file mode 100644 index 0000000..465aba9 Binary files /dev/null and b/app/__pycache__/common.cpython-37.pyc differ diff --git a/app/__pycache__/constants.cpython-37.pyc b/app/__pycache__/constants.cpython-37.pyc new file mode 100644 index 0000000..7da5bfb Binary files /dev/null and b/app/__pycache__/constants.cpython-37.pyc differ diff --git a/app/__pycache__/localdb.cpython-37.pyc b/app/__pycache__/localdb.cpython-37.pyc new file mode 100644 index 0000000..e3aa2ba Binary files /dev/null and b/app/__pycache__/localdb.cpython-37.pyc differ diff --git a/app/__pycache__/mainwindow.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-11-23-57-24).pyc b/app/__pycache__/mainwindow.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-11-23-57-24).pyc new file mode 100644 index 0000000..8e0506c Binary files /dev/null and b/app/__pycache__/mainwindow.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-11-23-57-24).pyc differ diff --git a/app/__pycache__/mainwindow.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-24-19-22-37).pyc b/app/__pycache__/mainwindow.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-24-19-22-37).pyc new file mode 100644 index 0000000..ed1724e Binary files /dev/null and b/app/__pycache__/mainwindow.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-24-19-22-37).pyc differ diff --git a/app/__pycache__/mainwindow.cpython-37.pyc b/app/__pycache__/mainwindow.cpython-37.pyc new file mode 100644 index 0000000..552c40f Binary files /dev/null and b/app/__pycache__/mainwindow.cpython-37.pyc differ diff --git a/app/__pycache__/manage_tables.cpython-37.pyc b/app/__pycache__/manage_tables.cpython-37.pyc new file mode 100644 index 0000000..1b015e5 Binary files /dev/null and b/app/__pycache__/manage_tables.cpython-37.pyc differ diff --git a/app/__pycache__/proxy.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-11-21-32-44).pyc b/app/__pycache__/proxy.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-11-21-32-44).pyc new file mode 100644 index 0000000..48f2a86 Binary files /dev/null and b/app/__pycache__/proxy.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-11-21-32-44).pyc differ diff --git a/app/__pycache__/proxy.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-12-19-17-24).pyc b/app/__pycache__/proxy.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-12-19-17-24).pyc new file mode 100644 index 0000000..2ad0d56 Binary files /dev/null and b/app/__pycache__/proxy.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-12-19-17-24).pyc differ diff --git a/app/__pycache__/proxy.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-24-19-22-37).pyc b/app/__pycache__/proxy.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-24-19-22-37).pyc new file mode 100644 index 0000000..48f2a86 Binary files /dev/null and b/app/__pycache__/proxy.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-24-19-22-37).pyc differ diff --git a/app/__pycache__/proxy.cpython-37.pyc b/app/__pycache__/proxy.cpython-37.pyc new file mode 100644 index 0000000..8c202b4 Binary files /dev/null and b/app/__pycache__/proxy.cpython-37.pyc differ diff --git a/app/__pycache__/reporting.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-11-21-14-37).pyc b/app/__pycache__/reporting.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-11-21-14-37).pyc new file mode 100644 index 0000000..fc316c3 Binary files /dev/null and b/app/__pycache__/reporting.cpython-37 (SFConflict oscar.alvarez.montero@gmail.com 2020-03-11-21-14-37).pyc differ diff --git a/app/__pycache__/reporting.cpython-37.pyc b/app/__pycache__/reporting.cpython-37.pyc new file mode 100644 index 0000000..53eb148 Binary files /dev/null and b/app/__pycache__/reporting.cpython-37.pyc differ diff --git a/app/__pycache__/states.cpython-37.pyc b/app/__pycache__/states.cpython-37.pyc new file mode 100644 index 0000000..1dc1220 Binary files /dev/null and b/app/__pycache__/states.cpython-37.pyc differ diff --git a/app/app_tablet.css b/app/app_tablet.css new file mode 100644 index 0000000..a650666 --- /dev/null +++ b/app/app_tablet.css @@ -0,0 +1,164 @@ + +QAbstractButton { + font-family: "DejaVu Sans"; + border-style: groove; + font: 12pt; + color: rgb(102, 102, 102); + background-color: rgb(242, 242, 242); + min-height: 50px; + min-width : 110px; + border-color: rgb(208, 208, 208); + border-width: 0px; +} + +#field_total_amount, #field_amount, #label_input, + #field_default, + #field_small_blue, + #field_small_gray, + #field_invoice { + background-color : white; + border-style : groove; + border-width : 0.5px; + border-color : rgb(208, 208, 208); +} + +#label_default { + font : 14pt; + color : rgb(102, 102, 102); + min-height : 10px; + min-width : 10px; +} + +#field_default { + font: bold 14pt; + min-height : 10px; +} + +#field_small_gray, #label_small_gray { + font : 16px; + color : rgb(54, 54, 54); +} + +#field_medium_gray, #label_medium_gray { + font : 24px; + color : rgb(54, 54, 54); +} + +#field_big_gray, #label_big_gray { + font : 32px; + color : rgb(54, 54, 54); +} + +#field_small_blue, #label_small_blue { + font : 16px; + color : rgb(0, 30, 80); +} + +#field_medium_blue, #label_medium_blue { + font : 24px; + color : rgb(0, 30, 80); +} + +#field_big_blue, #label_big_blue { + font : bold 32px; + color : rgb(0, 30, 80); +} + +#field_small_orange, #label_small_orange { + font : 16px; + color : rgb(235, 160, 15); +} + +#field_medium_orange, #label_medium_orange { + font : 24px; + color : rgb(235, 160, 15); +} + +#field_big_orange, #label_big_orange { + font : bold 32px; + color : rgb(235, 160, 15); +} + +QDialog { + min-height : 300px; + max-height : 400px; + min-width : 500px; +} + +#label_gray { + font: bold 14pt; + min-height : 10px; + min-width : 10px; + color: rgb(102, 102, 102); +} + +#label_blue { + font: bold 14pt; + color: rgb(17, 84, 102); + min-height : 10px; + min-width : 10px; +} + +#label_message { + font : 14pt; + min-height : 45px; + min-width : 10px; +} + +#field_invoice { + font : 15pt; + min-height : 45px; + min-width : 110px; +} + +#label_input { + font : 15pt; + min-height : 45px; + min-width : 200px; +} + +#field_sign { + font: bold 26pt; + min-height : 70px; + max-width: 100px; +} + +#field_amount { + font: 24pt; + min-height : 60px; + max-width: 310px; +} + +#field_total_amount, #label_total_amount { + color : rgb(50, 65, 75); +} + +#table_sale_lines { + color : rgb(70, 70, 70); + font : 11pt; + max-height: 380px; +} + +#label_product, #label_qty, #spin_box_qty, #row_field_price, + #label_price, #row_field_note, #row_field_description { + font : 24px; + alignment : center; +} + +QSpinBox { + padding-right: 15px; + border-width: 3; + height: 40px; +} + +QSpinBox::up-button { + subcontrol-position: right; + height: 38px; + width: 38px; +} + +QSpinBox::down-button { + subcontrol-position: left; + height: 38px; + width: 38px; +} diff --git a/app/buttonpad.py b/app/buttonpad.py new file mode 100644 index 0000000..ccec5f6 --- /dev/null +++ b/app/buttonpad.py @@ -0,0 +1,178 @@ +import os +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QWidget, QGridLayout, QHBoxLayout, QStackedWidget + +from neox.commons.custom_button import CustomButton +from .common import get_icon + +DIR_SHARE = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', 'share'))) + +__all__ = ['ButtonsFunction', 'ButtonsStacked', 'ButtonsNumber'] + + +def factoryIcons(): + pass + + +factoryIcons() + + +class ButtonsFunction(QGridLayout): + # Function Numpad + + def __init__(self, parent, tablet_mode=False): + super(ButtonsFunction, self).__init__() + self.setHorizontalSpacing(1) + self.setVerticalSpacing(1) + + rows = 4 + columns = 3 + + self.values = [ + ['button_search_product', self.tr('SEARCH'), 'action_search_product'], + ] + + salesman_desc = self.tr('SALESMAN') + salesman_button = 'button_salesman' + if parent.enviroment == 'restaurant': + salesman_desc = self.tr('WAITER') + salesman_button = 'button_waiter' + + self.values.extend([ + ['button_party', self.tr('CUSTOMER'), 'action_party'], + ['button_cancel', self.tr('CANCEL'), 'action_cancel'], + ['button_search_sale', self.tr('S. SALE'), 'action_search_sale'], + ['button_print_sale', self.tr('PRINT'), 'action_print_sale'], + [salesman_button, salesman_desc, 'action_salesman'], + ['button_global_discount', self.tr('GLOBAL DISCOUNT'), 'action_global_discount'], + ['button_print_order', self.tr('ORDER'), 'action_print_order'] + ]) + + if parent.type_pos_user != 'cashier': + self.values.append( + ['button_new_sale', self.tr('NEW SALE'), 'action_new_sale']) + + if parent.type_pos_user != 'order': + self.values.append(['button_payment', self.tr('PAY MODE'), 'action_payment']) + if parent.enviroment == 'retail': + self.values.append([ + 'button_payment_term', self.tr('PAY TERM'), 'action_payment_term'] + ) + + if parent.enviroment == 'restaurant': + rows = 3 + columns = 5 + self.values.extend([ + ['button_position', self.tr('POSITION'), 'action_position'], + # ['button_delete_line', self.tr('DELETE'), 'action_delete_line'], + ['button_comment', self.tr('NOTE'), 'action_comment'], + ]) + + if parent.enviroment == 'restaurant': + self.values.extend([ + ['button_tip', self.tr('TIP'), 'action_tip'], + ['button_tables', self.tr('TABLES'), 'action_tables'], + ['button_reservations', self.tr('RESERVATIONS'), 'action_reservations'], + ]) + + positions = [(i, j) for i in range(rows) for j in range(columns)] + for position, value in zip(positions, self.values): + name_icon = value[0][7:] + button = CustomButton( + parent, + id=value[0], + icon=get_icon(name_icon), + desc=value[1], + method=value[2], + name_style='toolbar_button' + ) + self.addWidget(button, *position) + + +class ButtonsStacked(QHBoxLayout): + + def __init__(self, parent): + super(ButtonsStacked, self).__init__() + self.stacked = QStackedWidget() + + self.button_accept = CustomButton( + id='button_accept', + parent=parent, + icon=get_icon('accept'), + name_style='toolbar', + method='button_accept_pressed' + ) + self.button_cash = CustomButton( + id='button_cash', + parent=parent, + icon=get_icon('cash'), + name_style='toolbar', + method='button_cash_pressed' + ) + + if parent.type_pos_user != 'order' and not parent.tablet_mode: + self.stacked.addWidget(self.button_accept) + self.stacked.addWidget(self.button_cash) + self.addWidget(self.stacked, 0) + + if parent.type_pos_user == 'cashier': + self.button_to_draft = CustomButton( + id='button_to_draft', + parent=parent, + icon=get_icon('draft'), + name_style='toolbar', + method='button_to_draft_pressed' + ) + self.addWidget(self.button_to_draft, 0) + else: + self.button_plus = CustomButton( + id='button_plus', + parent=parent, + icon=get_icon('plus'), + method='button_plus_pressed', + name_style='toolbar', + ) + #self.addWidget(self.button_plus, 0) + + if parent.type_pos_user == 'order' or parent.type_pos_user == 'salesman': + self.button_send_order = CustomButton( + id='button_send_to_pay', + icon=get_icon('draft'), + parent=parent, + method='button_send_to_pay_pressed', + name_style='toolbar' + ) + self.addWidget(self.button_send_order, 0) + + +class ButtonsNumber(QGridLayout): + + def __init__(self, parent): + # Numpad for Numbers + super(ButtonsNumber, self).__init__() + self.setHorizontalSpacing(1) + self.setVerticalSpacing(1) + + +class Buttonpad(QWidget): + + def __init__(self, parent): + super(Buttonpad, self).__init__() + self._text = '' + self._keyStates = {} + self.functions = ButtonsFunction(parent) + if parent.tablet_mode: + self.numbers = ButtonsNumber(parent) + + self.stacked = ButtonsStacked(parent) + self.set_keys() + + def set_keys(self): + q = Qt + self.keys_numbers = list(range(q.Key_0, q.Key_9 + 1)) + self.keys_alpha = list(range(q.Key_A, q.Key_Z + 1)) + self.keys_special = [ + q.Key_Asterisk, q.Key_Comma, q.Key_Period, + q.Key_Minus, q.Key_Slash] + self.show_keys = self.keys_numbers + self.keys_alpha + self.keys_special diff --git a/app/common.py b/app/common.py new file mode 100644 index 0000000..54f495a --- /dev/null +++ b/app/common.py @@ -0,0 +1,20 @@ + +import os +from PyQt5.QtGui import QIcon + +current_dir = os.path.dirname(__file__) + + +def get_icon(name): + file_icon = name if name else 'fork' + path_icon = os.path.join(current_dir, 'share', file_icon + '.svg') + _icon = QIcon(path_icon) + return _icon + + +def to_numeric(number): + return str(round(number, 2)) + + +def to_float(number, digits): + return str(round(number, 4)) diff --git a/app/constants.py b/app/constants.py new file mode 100644 index 0000000..71b024d --- /dev/null +++ b/app/constants.py @@ -0,0 +1,52 @@ +import os +from datetime import datetime +from decimal import Decimal + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QHeaderView + +PATH_PRINTERS = '/dev/usb' + +UTC_OFFSET_TIMEDELTA = datetime.now() - datetime.utcnow() +DELTA_LOCALE = round(UTC_OFFSET_TIMEDELTA.total_seconds() / 60 / 60) +RATE_CREDIT_LIMIT = 0.8 + +STRETCH = QHeaderView.Stretch +alignRight = Qt.AlignRight +alignLeft = Qt.AlignLeft +alignCenter = Qt.AlignCenter +alignVCenter = Qt.AlignVCenter +alignHCenter = Qt.AlignHCenter + +DIALOG_REPLY_NO = 0 +DIALOG_REPLY_YES = 1 +ZERO = Decimal('0') + +FRACTIONS = [ + ('', ''), + ('1', '1'), + ('0.5', '1/2'), + ('0.25', '1/4'), + ('0.125', '1/8'), + ('0.0625', '1/16'), + ('0.0313', '1/32') +] + +current_dir = os.path.dirname(__file__) + +SCREENS = { + 'large': os.path.join(current_dir, 'large_screen.css'), + 'medium': os.path.join(current_dir, 'medium_screen.css'), + 'small': os.path.join(current_dir, 'small_screen.css') +} + +CONVERSION_DIGITS = { + 'gal': '4', + 'u': '0', + 'g': '4', + 'm': '2', + 'km': '2', + 'kg': '2', +} + +FILE_BANNER = os.path.join(current_dir, 'share', 'pos_banner.png') diff --git a/app/electronic_scale.py b/app/electronic_scale.py new file mode 100755 index 0000000..17d4e62 --- /dev/null +++ b/app/electronic_scale.py @@ -0,0 +1,81 @@ + +''' + Electronic Scale measure methods +''' +import time +try: + import serial +except: + print('Warning: missing pyserial module!') + +from decimal import Decimal +from PyQt5.QtCore import QThread, pyqtSignal + +ZERO_VALUES = [b'00.000\n', b'\r00.000\n'] +FAKE_WEIGHT = b'01.500\n' +MAX_RANGE_ATTEMPT = 30 + + +class ScaleReader(QThread): + """ + Electronic Scale Reader Class + """ + sigSetDevice = pyqtSignal() + sigSetWeight = pyqtSignal() + sigCancelAction = pyqtSignal() + + def __init__(self, device_path='/dev/ttyUSB0', fake=False): + QThread.__init__(self) + self.fake = fake + self.device = device_path + try: + self.scale = serial.Serial(self.device) + except: + print('Error: serial device not found!') + + def run(self): + values = [] + counter = 0 + self.best_weight = Decimal(0) + if self.fake: + self._run_fake() + return + try: + self.scale = serial.Serial(self.device) + except: + print('Error: serial device not found!') + return + + while counter <= MAX_RANGE_ATTEMPT: + read_value = self.scale.readline() + if read_value in ZERO_VALUES: + continue + + counter += 1 + weight = read_value.decode("utf-8") + weight = weight.replace('\n\r', '') + weight = weight.encode("ascii").decode() + try: + weight = Decimal(weight) + except: + continue + values.append(weight) + + self.scale.close() + if values: + self.best_weight = str(max(values)) + self.sigSetWeight.emit() + + def _run_fake(self): + self.scale = 'Fake Electronic Scale' + self.sigSetDevice.emit() + time.sleep(3) + self.best_weight = FAKE_WEIGHT + self.sigGetWeight.emit() + + def reset_zero(self): + self.best_weight = None + + def onClose(self): + if hasattr(self, 'scale'): + self.scale.close() diff --git a/app/help.py b/app/help.py new file mode 100644 index 0000000..428459c --- /dev/null +++ b/app/help.py @@ -0,0 +1,28 @@ + +from neox.commons.dialogs import HelpDialog + + +class Help(HelpDialog): + + def __init__(self, parent): + super(Help, self).__init__(parent) + shortcuts = [ + (self.tr('HELP'), 'F1'), + (self.tr('SEARCH'), 'F2'), + (self.tr('PAYMENT MODE'), 'F3'), + (self.tr('SEARCH CUSTOMER'), 'F4'), + (self.tr('ADD TAXES'), 'F5'), + (self.tr('PRINT ORDER'), 'F6'), + (self.tr('PRINT INVOICE'), 'F7'), + (self.tr('PAYMENT TERM'), 'F8'), + (self.tr('SEARCH SALES'), 'F9'), + (self.tr('ACTIVATE TABLE'), 'F10'), + (self.tr('NEW SALE'), 'F11'), + (self.tr('CANCEL SALE'), 'F12'), + (self.tr('SALESMAN'), 'Home'), + (self.tr('POSITION'), 'Insert'), + (self.tr('CASH'), 'End'), + (self.tr('COMMENT'), 'Page Down'), + ] + + self.set_shortcuts(shortcuts) diff --git a/app/large_screen.css b/app/large_screen.css new file mode 100644 index 0000000..9614164 --- /dev/null +++ b/app/large_screen.css @@ -0,0 +1,137 @@ + +QAbstractButton { + font-family: "DejaVu Sans"; + border-style: groove; + font: 22pt; + color: rgb(102, 102, 102); + background-color: rgb(242, 242, 242); + min-height: 45px; + min-width : 100px; + border-color: rgb(208, 208, 208); + border-width: 0.5px; +} + +#label_input, +#field_default, +#field_large_gray, +#field_large_blue, +#field_large_orange { + background-color : white; + border-style : groove; + border-width : 0.5px; + border-color : rgb(208, 208, 208); +} + +#field_default { + font : 26px; +} + +#field_large_gray, #label_large_gray { + font : 26px; + color : rgb(54, 54, 54); +} + +#field_large_blue, #label_large_blue { + font : 26px; + color : rgb(0, 30, 80); +} + +#field_large_orange, #label_large_orange { + font : 26px; + color : rgb(235, 160, 15); +} + +#label_qty, #spin_box_qty, #row_field_price, +#label_price, #row_field_note, #label_fraction, #row_field_qty, +#field_fraction { + font : 26px; + alignment : center; +} + +#row_field_description { + font : 26px; + color : #242424; +} + +#label_product { + font : bold 24px; + alignment : center; + color : #242424; +} + +QSpinBox { + padding-right: 15px; + border-width: 3; + height: 40px; +} + +QSpinBox::up-button { + subcontrol-position: right; + height: 40px; + width: 40px; +} + +QSpinBox::down-button { + subcontrol-position: left; + height: 40px; + width: 40px; +} + +#field_invoice { + min-width : 140px; + max-width : 190px; +} + +#label_position, #label_salesman, #label_payment_term, + #label_party, #label_global_discount, #field_invoice, #label_invoice, + #label_agent, #label_date, #label_order_number, #label_default { + font : 26px; + color : rgb(150, 150, 150); + min-height : 10px; + min-width : 10px; +} + +#field_amount, #field_sign { + font : bold 48px; + color : rgb(43, 60, 77); + min-height : 40px; + max-height : 100px; +} + +#field_amount { + max-width : 260px; + min-width : 180px; +} + +#field_total_amount, #field_change, #label_total_amount, #label_change { + color : rgb(50, 65, 75); + min-width : 200px; +} + +#field_paid, #label_paid { + color : rgb(80, 190, 220); +} + +#table_payment { + color : rgb(70, 70, 70); + font : 14pt; + max-height : 80px; +} + +#table_sale_lines { + color : rgb(70, 70, 70); + font : 14pt; +} + +#label_paid, #label_change, #label_discount, #label_total_amount, +#label_discount, #label_change, #label_paid, #label_amount, #label_untaxed_amount, #label_taxes_amount, +#field_total_amount, #field_change, #field_discount, #field_paid, +#field_untaxed_amount, #field_taxes_amount { + min-height : 10px; + font: bold 20pt; +} + +#img_pixmap_pos { + min-width: 200pt; + min-height: 100pt; +} diff --git a/app/localdb.py b/app/localdb.py new file mode 100644 index 0000000..fa96a81 --- /dev/null +++ b/app/localdb.py @@ -0,0 +1,143 @@ + +import os +import sqlite3 + +file_db = 'app.db' + +if os.name == 'posix': + home = 'HOME' + directory = '.tryton' +elif os.name == 'nt': + home = 'USERPROFILE' + directory = 'AppData/Local/tryton' + +_directory = os.path.join(os.getenv(home), directory) +localfile = os.path.join(_directory, file_db) + + +class LocalStore(object): + def __init__(self, params=None): + self.conn = sqlite3.connect(localfile) + + def create_table_config(self): + cursorObj = self.conn.cursor() + cursorObj.execute("CREATE TABLE IF NOT EXISTS configuration( \ + 'id' INTEGER PRIMARY KEY, \ + 'synchronized_date' DATETIME, \ + 'company_id' INTEGER, \ + 'company_name' TEXT, \ + 'defualt_party_id' INTEGER, \ + 'defualt_party_name' TEXT \ + )") + self.conn.commit() + + def create_table_product(self): + cur = self.conn.cursor() + cur.execute("CREATE TABLE IF NOT EXISTS product( \ + 'id' INTEGER PRIMARY KEY, \ + 'code' TEXT, \ + 'write_date' DATE, \ + 'barcode' TEXT, \ + 'template.name' TEXT, \ + 'description' TEXT, \ + 'template.account_category' INTEGER, \ + 'template.sale_price_w_tax' REAL \ + )") + self.conn.commit() + + def get_config(self): + cur = self.conn.cursor() + cur.execute("SELECT id, synchronized_date FROM configuration") + config_ = cur.fetchall() + if config_: + return config_[0] + + def set_config(self, config): + cur = self.conn.cursor() + cur.execute("INSERT INTO configuration(id, synchronized_date, company_id) \ + VALUES(1, '2010-01-01 00:00:00', ?)", config) + self.conn.commit() + + def set_config_sync(self, config_dt): + cur = self.conn.cursor() + cur.execute("UPDATE configuration SET synchronized_date=?", [config_dt]) + self.conn.commit() + + def get_local_products(self): + cur = self.conn.cursor() + cur.execute("SELECT id FROM product") + rows = cur.fetchall() + rows_ = [r[0] for r in rows] + return rows_ + + def update_products(self, products, local_products): + len_products = len(products) + print('------- Creating %d products -------' % len_products) + to_create = [] + to_update = [] + for p in products: + if p[0] not in local_products: + to_create.append(p) + else: + to_update.append([p[1], p[4], float(p[7]), p[6], p[0]]) + + if to_create: + cur = self.conn.cursor() + cur.executemany("INSERT INTO product(id, write_date, code, \ + barcode, 'template.name', description, \ + 'template.account_category', 'template.sale_price_w_tax') \ + VALUES(?, ?, ?, ?, ?, ?, ?, ?)", to_create) + self.conn.commit() + + if to_update: + cur = self.conn.cursor() + for rec in to_update: + cur.execute('UPDATE product SET write_date=?, "template.name"=?,\ + "template.sale_price_w_tax"=?, "template.account_category"=? \ + WHERE id=?', rec) + self.conn.commit() + + def find_product_by_id(self, product_id): + cur = self.conn.cursor() + query = "SELECT * FROM product WHERE id=?" + cur.execute(query, (product_id,)) + res = cur.fetchall() + return res[0] + + def find_product_elastic(self, domain, limit): + self.conn.row_factory = lambda c, r: dict( + zip([col[0] for col in c.description], r) + ) + cur = self.conn.cursor() + clauses = [] + + for d in domain: + clause1 = '"{}" LIKE "{}"'.format(d[1][0], d[1][2]) + clause2 = '"{}" LIKE "{}"'.format(d[2][0], d[2][2]) + clause3 = '(' + clause1 + ' OR ' + clause2 + ')' + clauses.append(clause3.upper()) + + if len(clauses) > 1: + _clause = ' AND '.join(clauses) + else: + _clause = clauses[0] + query = "SELECT * FROM product WHERE {}".format(_clause) + + cur.execute(query) + rows = cur.fetchall() + return rows + + # def do_query(self): + # c = self.conn.cursor() + # c.execute(query) + # rows = c.fetchall() + # self.conn.commit() + # self.conn.close() + # for row in rows: + # print(row) + + +if __name__ == "__main__": + store = LocalStore() + store.create_table() + store.get_local_products() diff --git a/app/mainwindow.py b/app/mainwindow.py new file mode 100644 index 0000000..fc58c96 --- /dev/null +++ b/app/mainwindow.py @@ -0,0 +1,2637 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +import sys +import os +import logging +from decimal import Decimal + +from datetime import datetime, timedelta, date +from collections import OrderedDict +from PyQt5.QtCore import Qt, QThread, pyqtSignal +from PyQt5.QtGui import QTouchEvent +from PyQt5.QtWidgets import (QLabel, QTextEdit, QHBoxLayout, QVBoxLayout, + QWidget, QGridLayout, QLineEdit, QDoubleSpinBox) + +from neox.commons.action import Action +from neox.commons.forms import GridForm, FieldMoney, ComboBox +from neox.commons.messages import MessageBar +from neox.commons.image import Image +from neox.commons.dialogs import QuickDialog +from neox.commons.table import TableView +from neox.commons.model import TableModel, Modules +from neox.commons.search_window import SearchWindow +from neox.commons.frontwindow import FrontWindow +from neox.commons.menu_buttons import MenuDash + +from .proxy import FastModel +from .localdb import LocalStore +from .reporting import Receipt +from .buttonpad import Buttonpad +from .manage_tables import ManageTables +from .states import STATES, RE_SIGN +from .common import get_icon, to_float, to_numeric +from .constants import (PATH_PRINTERS, DELTA_LOCALE, STRETCH, alignRight, + alignLeft, alignCenter, alignHCenter, alignVCenter, DIALOG_REPLY_NO, + DIALOG_REPLY_YES, ZERO, FRACTIONS, RATE_CREDIT_LIMIT, SCREENS, FILE_BANNER, + CONVERSION_DIGITS) + + +class MainWindow(FrontWindow): + + def __init__(self, connection, params): + title = "PRESIK | SMART POS" + global CONNECTION + self.conn = connection + CONNECTION = connection + super(MainWindow, self).__init__(connection, params, title) + print('Screen Size: > ', self.screen_size) + self.set_style(SCREENS[self.screen_size]) + + self.is_clear_right_panel = True + self.payment_ctx = {} + self.set_keys() + self.stock_context = None + + self.ctx = self._context + self.ctx['params'] = params + # self.ctx['params'] = params + response = self.load_modules() + if response is not True: + d = self.dialog(response) + d.exec_() + super(MainWindow, self).close() + return + self.setup_sale_line() + self.setup_payment() + self.set_domains() + self.create_gui() + self.message_bar.load_stack(self.stack_msg) + + if not hasattr(self, 'auto_print_commission'): + self.auto_print_commission = False + + self.active_usb_printers = [] + + if os.name == 'posix' and os.path.exists(PATH_PRINTERS): + self.set_printers_usb(PATH_PRINTERS) + + self.set_printing_context() + if not self.tablet_mode: + self.create_statusbar() + + self.window().showMaximized() + self.create_dialog_search_products() + + if not self.tablet_mode: + self.grabKeyboard() + self.reader_thread = None + self._current_line_id = None + self._amount_text = '' + self._sign = None + self.create_dialogs() + self.createNewSale() + if not hasattr(self, 'active_weighing'): + self.active_weighing = False + elif self.active_weighing is True: + from .electronic_scale import ScaleReader + self.reader_thread = ScaleReader() + self.reader_thread.sigSetWeight.connect(self.set_weight_readed) + + self.do_invoice = DoInvoice(self, self._context) + self.do_invoice.sigDoInvoice.connect(self.__do_invoice_thread) + self.set_cache_company() + self.set_cache_products() + + def set_domains(self): + self.domain_search_product = [ + ('code', '!=', None), + ('active', '=', True), + ('template.salable', '=', True), + ('template.account_category', '!=', None), + ] + + if self.shop['product_categories']: + self.domain_search_product.append( + ('account_category', 'in', self.shop['product_categories']) + ) + + def filter_cache(self, data, filter, target): + res = [] + for d in data: + for t in target: + if t in d[filter]: + res.append(d) + return res + + def set_cache_company(self): + self.store = LocalStore() + self.store.create_table_config() + self._local_config = self.store.get_config() + if not self._local_config: + company_id = self.device['shop.company'] + self._local_config = self.store.set_config([company_id]) + + def set_cache_products(self): + self.store.create_table_product() + local_products = self.store.get_local_products() + + config = self.store.get_config() + _sync_date = config[1] + products = self.Product.sync_get_products({'write_date': _sync_date, 'shop_id': self.ctx['shop']}) + # print('products------->', products) + self.store.update_products(products, local_products) + now = datetime.now() + self.store.set_config_sync(str(now)) + + def event(self, evento): + event_type = super(MainWindow, self).event(evento) + touch = QTouchEvent(event_type) + # logging.warning('Device:', touch.device()) + return event_type + + def set_printers_usb(self, PATH_PRINTERS): + for usb_dev in os.listdir(PATH_PRINTERS): + if 'lp' not in usb_dev: + continue + path_device = os.path.join(PATH_PRINTERS, usb_dev) + self.active_usb_printers.append(['usb', path_device]) + + def get_current_sale(self): + if hasattr(self, '_sale') and self._sale['id']: + sales = self.ModSale.find([ + ('id', '=', self._sale['id']) + ]) + if not sales: + return + return sales[0] + + def check_empty_sale(self): + sale = self.get_current_sale() + if sale and self.model_sale_lines.rowCount() == 0 \ + and sale['state'] == 'draft' and not sale['number']: + self.delete_current_sale() + + def close(self): + dialog = self.dialog('confirm_exit', response=True) + response = dialog.exec_() + if response == DIALOG_REPLY_YES: + self.check_empty_sale() + if self.active_weighing and self.reader_thread: + self.reader_thread.onClose() + super(MainWindow, self).close() + + def delete_current_sale(self): + if self._sale['id']: + self._PosSale.cancel_sale(self._sale['id'], self._context) + + def resize_window_tablet_dev(self): + self.resize(690, self.get_geometry()[1]) + + def set_stack_messages(self): + super(MainWindow, self).set_stack_messages() + self.stack_msg.update({ + 'system_ready': ('info', self.tr('SYSTEM READY...')), + 'confirm_exit': ('warning', self.tr('DO YOU WANT TO EXIT?')), + 'confirm_credit': ('question', self.tr('PLEASE CONFIRM YOUR PAYMENT TERM AS CREDIT?')), + 'sale_number_not_found': ('warning', self.tr('SALE ORDER / INVOICE NUMBER NOT FOUND!')), + 'sale_closed': ('error', self.tr('THIS SALE IS CLOSED, YOU CAN NOT TO MODIFY!')), + 'discount_not_valid': ('warning', self.tr('DISCOUNT VALUE IS NOT VALID!')), + 'add_payment_sale_draft': ('info', self.tr('YOU CAN NOT ADD PAYMENTS TO SALE ON DRAFT STATE!')), + 'enter_quantity': ('question', self.tr('ENTER QUANTITY...')), + 'enter_discount': ('question', self.tr('ENTER DISCOUNT...')), + 'enter_payment': ('question', self.tr('ENTER PAYMENT AMOUNT BY: %s')), + 'enter_new_price': ('question', self.tr('ENTER NEW PRICE...')), + 'order_successfully': ('info', self.tr('ORDER SUCCESUFULLY SENT.')), + 'order_failed': ('warning', self.tr('FAILED SEND ORDER!')), + 'missing_agent': ('warning', self.tr('MISSING AGENT!')), + 'missing_salesman': ('warning', + self.tr('THERE IS NOT SALESMAN FOR THE SALE!')), + 'sale_without_products': ('warning', self.tr('YOU CAN NOT CONFIRM A SALE WITHOUT PRODUCTS!')), + 'user_without_permission': ('error', self.tr('USER WITHOUT PERMISSION FOR SALE POS!')), + 'quantity_not_valid': ('error', self.tr('THE QUANTITY IS NOT VALID...!')), + 'user_not_permissions_device': ('error', self.tr('THE USER HAVE NOT PERMISSIONS FOR ACCESS' \ + ' TO DEVICE!')), + 'missing_party_configuration': ('warning', + self.tr('MISSING THE DEFAULT PARTY ON SHOP CONFIGURATION!')), + 'missing_journal_device': ('error', self.tr('MISSING SET THE JOURNAL ON DEVICE!')), + 'statement_closed': ('error', self.tr('THERE IS NOT A STATEMENT OPEN FOR THIS DEVICE!')), + 'product_not_found': ('warning', self.tr('PRODUCT NOT FOUND!')), + 'must_load_or_create_sale': ('warning', self.tr('FIRST YOU MUST CREATE/LOAD A SALE!')), + 'new_sale': ('warning', self.tr('DO YOU WANT CREATE NEW SALE?')), + 'cancel_sale': ('question', self.tr('ARE YOU WANT TO CANCEL SALE?')), + 'not_permission_delete_sale': ('info', self.tr('YOU HAVE NOT PERMISSIONS FOR DELETE THIS SALE!')), + 'not_permission_for_cancel': ('info', self.tr('YOU HAVE NOT PERMISSIONS FOR CANCEL THIS SALE!')), + 'customer_not_credit': ('info', self.tr('THE CUSTOMER HAS NOT CREDIT!')), + 'agent_not_found': ('warning', self.tr('AGENT NOT FOUND!')), + 'invalid_commission': ('warning', self.tr('COMMISSION NOT VALID!')), + 'credit_limit_exceed': ('info', self.tr('CREDIT LIMIT FOR CUSTOMER EXCEED!')), + 'credit_limit_capacity': ('info', self.tr('THE CUSTOMER CREDIT CAPACITY IS ABOVE 80%')), + 'not_can_force_assign': ('warning', self.tr('YOU CAN NOT FORCE ASSIGN!')), + }) + + def load_modules(self): + modules = Modules(self, self.conn) + self._sale_pos_restaurant = None + self.Module = FastModel('ir.module', self.ctx) + self.Config = FastModel('sale.configuration', self.ctx) + self._config, = self.Config.find([('id', '=', 1)]) + + self.discount_method = self._config.get('discount_pos_method') + + self._commission_activated = self.Module.find([ + ('name', '=', 'commission'), + ('state', '=', 'activated'), + ]) + self._credit_limit_activated = self.Module.find([ + ('name', '=', 'account_credit_limit'), + ('state', '=', 'activated'), + ]) + + _product = { + 'name': 'product.product', + 'fields': [ + 'template.name', 'code', 'barcode', 'write_date', + 'description', 'template.sale_price_w_tax', + 'template.account_category' + ] + } + self.cache_local = self._config.get('cache_products_local') + + if self._config['show_location_pos']: + _product['fields'].append('location_') + + if self._config['show_stock_pos'] in ('value', 'icon'): + if self._config['show_stock_pos'] == 'value': + _product['fields'].append('quantity') + if self._config['show_brand']: + _product['fields'].append('brand.name') + + if self._config['encoded_sale_price']: + _product['fields'].extend(['image', 'image_icon', 'encoded_sale_price']) + + _PosSale = { + 'name': '_PosSale', + 'model': 'sale.sale', + 'fields': ['number', 'party', 'party.name', 'salesman', 'lines', + 'position', 'total_amount_cache', 'salesman.party.name', + 'payment_term', 'payment_term.name', 'invoices', + 'payments', 'untaxed_amount', 'state', 'tax_amount', + 'total_amount', 'residual_amount', 'paid_amount', 'invoice', + 'invoice.state', 'invoice_number', 'invoice.number', 'invoices', + 'delivery_charge', 'sale_date', 'invoice_type'], + 'methods': ( + 'get_printing_context', 'cancel_sale', 'get_amounts', + 'get_discount_total', 'process_sale', 'reconcile_invoice', + 'post_invoice', 'get_data', 'add_value', 'faster_add_product', + 'get_product_prices', 'add_payment', 'get_order2print', + 'get_sale_from_invoice', 'add_tax', 'check_state', 'to_quote', + 'to_draft', 'new_sale', 'on_change', 'get_salesman_in_party', + ) + } + + _Agent = { + 'name': '_Agent', + 'model': 'commission.agent', + 'fields': ('id', 'party.name', 'party.id_number', 'plan.percentage', + 'active'), + } + _Commission = { + 'name': '_Commission', + 'model': 'commission', + 'fields': ('id', 'origin', 'invoice_line', 'invoice_line.invoice'), + } + + _Tables = self._Tables = None + if self.enviroment == 'restaurant': + self._sale_pos_restaurant = self.Module.find([ + ('name', '=', 'sale_pos_frontend_rest'), + ('state', '=', 'activated'), + ]) + if self._sale_pos_restaurant: + _Tables = { + 'name': '_Tables', + 'model': 'sale.shop.table', + 'fields': ('name', 'shop', 'capacity', 'state') + } + _PosSale['fields'].extend(['table_assigned', + 'table_assigned.name', 'table_assigned.state']) + + if self._commission_activated: + _PosSale['fields'].extend(['agent', 'agent.party.name', 'commission']) + modules.set_models([_Agent, _Commission]) + + self.User = FastModel('res.user', self.ctx) + self._user, = self.User.find([('login', '=', self.user)]) + + if not self._user['sale_device']: + return 'user_not_permissions_device' + + self.ctx['user'] = self._user['id'] + + self.ModSale = FastModel('sale.sale', self.ctx) + self.ModSaleLine = FastModel('sale.line', self.ctx) + self.Product = FastModel('product.product', self.ctx) + self.Journal = FastModel('account.statement.journal', self.ctx) + self.Employee = FastModel('company.employee', self.ctx) + self.Device = FastModel('sale.device', self.ctx) + self.Category = FastModel('product.category', self.ctx) + self.PaymentTerm = FastModel('account.invoice.payment_term', self.ctx) + self.Party = FastModel('party.party', self.ctx) + self.Taxes = FastModel('account.tax', self.ctx) + self.ActionReport = FastModel('ir.action.report', self.ctx) + + models_to_work = [_PosSale] + + if _Tables: + models_to_work.append(_Tables) + + modules.set_models(models_to_work) + self.device, = self.Device.find([ + ('id', '=', self._user['sale_device']['id']), + ]) + + self.shop = self.device['shop'] + self.company = self.shop['company'] + + self.shop_taxes = self.shop['taxes'] + self._journals = dict([(j['id'], j) for j in self.device['journals']]) + + self.employees = self.Employee.find([ + ('company', '=', self.company['id']), + ]) + self._payment_terms = self.PaymentTerm.get_payment_term_pos() + + self.type_pos_user = self._context.get('type_pos_user') + + if not self.type_pos_user: + return 'user_without_permission' + self.user_can_delete = self.type_pos_user in ('frontend_admin', 'cashier') + + self.product_categories = self.device['shop']['product_categories'] + self.salesman_required = self.device['shop']['salesman_pos_required'] + + self.default_party = self.shop['party'] + if not self.default_party: + return 'missing_party_configuration' + + self.default_journal = self.device['journal'] + if not self.default_journal: + return 'missing_journal_device' + + self.default_payment_term = self.shop['payment_term'] + self._password_admin = self._config.get('password_admin_pos') + + self._action_report, = self.ActionReport.find([ + ('report_name', '=', 'account.invoice'), + ]) + + if self._config['show_stock_pos'] in ('value', 'icon'): + self.stock_context = { + 'stock_date_end': date.today(), + 'locations': [self.shop['warehouse']], + } + return True + + def create_dialogs(self): + self.create_dialog_position() + self.create_dialog_comment() + self.create_dialog_search_party() + self.create_dialog_payment() + self.create_dialog_salesman() + self.create_dialog_voucher() + self.create_dialog_print_invoice() + self.create_dialog_global_discount() + self.create_dialog_payment_term() + self.create_dialog_search_sales() + self.create_wizard_new_sale() + self.create_dialog_stock() + self.create_dialog_order() + self.create_dialog_force_assign() + self.create_dialog_taxes() + self.create_dialog_cancel_invoice() + self.create_dialog_sale_line() + if self._commission_activated: + self.create_dialog_agent() + if self.enviroment == 'restaurant' and self._sale_pos_restaurant: + self.create_dialog_manage_tables() + + def set_printing_context(self): + # Printing invoice context + if self.printer_sale_name: + if ">" in self.printer_sale_name: + self.printer_sale_name = str(str("\\") + self.printer_sale_name.replace('>', str('\\'))) + + ctx_printing = self._PosSale.get_printing_context( + [self.device['id']], self.user, self._context) + ctx_printing['row_characters'] = self.row_characters + ctx_printing['delta_locale'] = DELTA_LOCALE + + self.receipt_sale = Receipt(ctx_printing) + + # Printing order context + if self.print_order: + self.receipt_order = Receipt(ctx_printing) + self.set_default_printer() + + def set_default_printer(self, printer=None): + if self.active_usb_printers: + self.printer_sale_name = self.active_usb_printers[0] + if not printer and self.printer_sale_name: + printer = { + 'interface': self.printer_sale_name[0], + 'device': self.printer_sale_name[1], + } + if printer: + self.receipt_sale.set_printer(printer) + + def button_new_sale_pressed(self): + self.createNewSale() + + def button_send_to_pay_pressed(self): + # Return sale to draft state + self._PosSale.to_quote(self._sale['id'], self._context) + if self.model_sale_lines.rowCount() > 0: + if self.check_salesman(): + self.state_disabled() + + def button_to_draft_pressed(self): + # Return sale to draft state + if hasattr(self, '_sale'): + self._PosSale.to_draft(self._sale['id'], self._context) + self.state_disabled() + + def create_gui(self): + panels = QHBoxLayout() + panel_left = QVBoxLayout() + panel_right = QVBoxLayout() + + left_head = QHBoxLayout() + left_table = None + left_bottom = QHBoxLayout() + + self.message_bar = MessageBar() + self.label_input = QLabel() + self.label_input.setFocus() + self.label_input.setObjectName('label_input') + + if self.enviroment == 'restaurant': + values = self.get_product_by_categories() + menu_dash = MenuDash(self, values, 'on_selected_item') + + if not self.tablet_mode: + _label_invoice = QLabel(self.tr('INVOICE:')) + _label_invoice.setObjectName('label_invoice') + _label_invoice.setAlignment(alignRight | alignVCenter) + + self.field_invoice = QLineEdit() + self.field_invoice.setReadOnly(True) + self.field_invoice.setObjectName('field_invoice') + if self.tablet_mode: + self.field_invoice.setPlaceholderText(self.tr('INVOICE')) + + self.field_amount = FieldMoney(self, 'amount', {}) + self.field_amount.setObjectName('field_amount') + self.field_sign = QLabel(' ') + self.field_sign.setObjectName('field_sign') + + layout_message = QGridLayout() + layout_message.addLayout(self.message_bar, 1, 0, 1, 4) + + if not self.tablet_mode: + layout_message.addWidget(self.label_input, 2, 0, 1, 2) + layout_message.addWidget(_label_invoice, 2, 2) + layout_message.addWidget(self.field_invoice, 2, 3) + else: + layout_message.addWidget(self.label_input, 2, 0, 1, 2) + layout_message.addWidget(self.field_invoice, 2, 3) + + left_head.addLayout(layout_message, 0) + left_head.addWidget(self.field_sign, 0) + left_head.addWidget(self.field_amount, 0) + + info_fields = [ + ('party', { + 'name': self.tr('CUSTOMER'), + 'readonly': True, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + }), + ('date', { + 'name': self.tr('DATE'), + 'readonly': True, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + }), + ('salesman', { + 'name': self.tr('SALESMAN'), + 'readonly': True, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + }), + ('payment_term', { + 'name': self.tr('PAYMENT TERM'), + 'readonly': True, + 'invisible': self.tablet_mode, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + }), + ('order_number', { + 'name': self.tr('No ORDER'), + 'placeholder': False, + 'readonly': True, + 'size': self.screen_size, + 'color': 'gray' + }), + ('invoice_type', { + 'name': self.tr('INVOICE TYPE'), + 'placeholder': False, + 'type': 'selection', + 'on_change': 'action_invoice_type_selection_changed', + 'values': [ + ('', ''), + ('C', self.tr('COMPUTADOR')), + ('M', self.tr('MANUAL')), + ('P', self.tr('POS')), + ('1', self.tr('VENTA ELECTRONICA')), + ('2', self.tr('VENTA DE EXPORTACION')), + ('3', self.tr('FACTURA POR CONTINGENCIA FACTURADOR')), + ('4', self.tr('FACTURA POR CONTINGENCIA DIAN')), + ('91', self.tr('NOTA CREDITO ELECTRONICA')), + ('92', self.tr('NOTA DEBITO ELECTRONICA')), + ], + 'size': self.screen_size, + 'color': 'gray' + }), + ] + self.field_invoice_type = None + + if self.tablet_mode or self._config['show_position_pos']: + info_fields.append(('position', { + 'name': self.tr('POSITION'), + 'readonly': True, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + })) + if self._commission_activated and not self.tablet_mode \ + and self._config['show_agent_pos']: + info_fields.append(('agent', { + 'name': self.tr('AGENT'), + 'placeholder': self.tablet_mode, + 'readonly': True, + 'size': self.screen_size, + 'color': 'gray' + })) + + _cols = 2 + if self.enviroment == 'restaurant': + _cols = 1 + + self.field_delivery_charge = None + if self._config['show_delivery_charge']: + info_fields.append( + ('delivery_charge', { + 'name': self.tr('DELIVERY CHARGE'), + 'placeholder': False, + 'type': 'selection', + 'on_change': 'action_delivery_charge_selection_changed', + 'values': [ + ('', ''), + ('customer', self.tr('CUSTOMER')), + ('company', self.tr('COMPANY')), + ], + 'size': self.screen_size, + 'color': 'gray' + })) + + self.field_table_assigned = None + if self.enviroment == 'restaurant' and self._sale_pos_restaurant: + info_fields.append( + ('table_assigned', { + 'name': self.tr('ASSIGNED TABLE'), + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + })) + + self.grid_info = GridForm(self, OrderedDict(info_fields), col=_cols) + + col_sizes_tlines = [field['width'] for field in self.fields_sale_line] + left_table = TableView('model_sale_lines', self.model_sale_lines, + col_sizes_tlines, method_selected_row=self.sale_line_selected) + self.table_sale_lines = left_table + + for i, f in enumerate(self.model_sale_lines._fields, 0): + if f.get('invisible'): + self.table_sale_lines.hideColumn(i) + + _fields_amounts = [ + ('untaxed_amount', { + 'name': self.tr('SUBTOTAL'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'gray' + + }), + ('taxes_amount', { + 'name': self.tr('TAXES'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'gray' + }), + ('discount', { + 'name': self.tr('DISCOUNT'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'gray' + }), + ('total_amount', { + 'name': self.tr('TOTAL'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'blue' + }), + ('paid', { + 'name': self.tr('PAID'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'gray' + }), + ('pending', { + 'name': self.tr('PENDIENTE'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'blue' + }), + ('change', { + 'name': self.tr('CHANGE'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'orange' + }) + ] + + fields_amounts = OrderedDict(_fields_amounts) + self.grid_amounts = GridForm(self, fields_amounts, col=1) + + self.buttonpad = Buttonpad(self) + self.pixmap_pos = Image(self, 'pixmap_pos', FILE_BANNER) + self.table_payment = TableView('table_payment', self.table_payment_lines, + [250, STRETCH]) + + panel_left.addLayout(left_head, 0) + panel_left.addWidget(left_table, 1) + panel_right.addWidget(self.pixmap_pos, 0) + + left_bottom.addLayout(self.grid_info, 1) + if self.enviroment == 'restaurant': + panel_left.addLayout(self.buttonpad.functions, 1) + left_bottom.addLayout(self.grid_amounts, 0) + panel_right.addLayout(menu_dash, 1) + panel_right.addLayout(self.buttonpad.stacked, 0) + else: + panel_right.addLayout(self.grid_amounts, 1) + panel_right.addLayout(self.buttonpad.functions, 1) + panel_right.addLayout(self.buttonpad.stacked, 0) + panel_right.addWidget(self.table_payment) + + panel_left.addLayout(left_bottom, 0) + + panels.addLayout(panel_left, 1) + panels.addLayout(panel_right, 0) + + widget = QWidget() + widget.setLayout(panels) + self.setCentralWidget(widget) + + def create_statusbar(self): + values = OrderedDict([ + ('stb_shop', {'name': self.tr('SHOP'), 'value': self.shop['name']}), + ('stb_device', {'name': self.tr('DEVICE'), 'value': self.device['name']}), + ('stb_database', {'name': self.tr('DATABASE'), 'value': self.database}), + ('stb_user', {'name': self.tr('USER'), 'value': self.user}), + ('stb_printer', {'name': self.tr('PRINTER'), 'value': self.printer_sale_name}) + ]) + self.set_statusbar(values) + + def button_plus_pressed(self): + error = False + if self._input_text == '' and self._amount_text == '0': + return + if self._state in ('paid', 'disabled'): + return + if self._sign in ('*', '-', '/'): + if hasattr(self, '_sale_line') and self._sale_line \ + and self._sale_line.get('type') and self._state == 'add' \ + and self.model_sale_lines.rowCount() > 0: + if self._sign == '*': + self._process_quantity(self._amount_text) + else: + error = not(self._process_price(self._amount_text)) + elif self._state in ['add', 'cancel', 'accept']: + self.clear_right_panel() + self.add_product(code=self._input_text) + elif self._state == 'cash': + is_paid = self._process_pay(self.field_amount.text()) + if not is_paid: + self.clear_input_text() + self.clear_amount_text() + return + else: + logging.warning('Unknown command/text') + self._clear_context(error) + + def action_read_weight(self): + self.reader_thread.start() + + def set_weight_readed(self): + if not self.reader_thread or not self.reader_thread.best_weight: + return + + if self.reader_thread.best_weight: + self.amount_text_changed(self.reader_thread.best_weight) + self._process_quantity(self._amount_text) + self._clear_context(False) + self.reader_thread.fZERO() + + def _clear_context(self, error=False): + self.clear_input_text() + self.clear_amount_text() + self.clear_sign() + + if self._state not in ('warning', 'cash') and not error: + self.message_bar.set('system_ready') + else: + self.set_state('add') + + def _process_quantity(self, text): + eval_value = text.replace(',', '.') + rec = {} + try: + quantity = Decimal(eval_value) + if self._sale_line['product'].get('quantity'): + if not self._check_stock_quantity(self._sale_line['product'], quantity): + return + if self._current_line_id: + rec = self.ModSaleLine.faster_set_quantity({ + 'id': self._current_line_id, + 'quantity': float(quantity) + }) + except: + return self.message_bar.set('quantity_not_valid') + + self.message_bar.set('system_ready') + self.model_sale_lines.update_record(rec) + self.update_total_amount() + + def _process_price(self, text): + discount_valid = True + eval_value = text.replace(',', '') + + value = float(eval_value) + if self._sign == '-': + if self.discount_method == 'percentage' and value > 90: + discount_valid = False + # Do discount + else: + discount_valid = self.set_discount(eval_value) + elif self._sign == '/': + if value <= 0: + return + sale_line, = self.ModSaleLine.find([ + ('id', '=', self._current_line_id) + ]) + + price_w_tax = sale_line['product.template.sale_price_w_tax'] + if price_w_tax <= Decimal(value): + # Change unit price + discount_valid = self.set_unit_price(value) + else: + eval_value = (1 - (value / price_w_tax)) * 100 + if self.discount_method == 'fixed': + eval_value = price_w_tax - value + discount_valid = self.set_discount(eval_value) + if not discount_valid: + self.message_bar.set('discount_not_valid') + return False + + self.message_bar.set('system_ready') + self.update_total_amount() + return True + + def _process_pay(self, text): + if not self.validate_done_sale(): + return + val = Decimal(text.replace(',', '')) + + if self._commission_activated: + if self._journals[self.field_journal_id]['kind'] == 'payment': + agent_id = None + if self.field_agent_id: + agent_id = self.field_agent_id + else: + agent_id = self.field_agent_ask.get_id() + + if not agent_id: + self.message_bar.set('missing_agent') + return False + + cash_received = Decimal(val) + self.set_amounts() + residual_amount = self._sale['residual_amount'] + if residual_amount < 0: + # The sale is paid + self._done_sale() + return True + + change = cash_received - residual_amount + + if residual_amount >= cash_received: + amount_to_add = cash_received + else: + amount_to_add = residual_amount + + all_money = cash_received + self._sale['paid_amount'] + # self.field_change.setText(change) + if change < ZERO: + self.field_pending.setText(abs(change)) + else: + self.field_pending.setText(str(ZERO)) + + self.set_amount_received(all_money) + res = self.add_payment(amount_to_add, all_money, change) + if res['residual_amount'] < 0: + # The sale is paid + self._done_sale() + return True + + self.field_journal_id = self._default_journal_id + + if res['msg'] == 'missing_money': + self.message_bar.set('enter_payment', self.default_journal['name']) + return False + + if change < ZERO: + self.message_bar.set('enter_payment', self.default_journal['name']) + + self.field_change.setText(change) + # self.set_amount_received(all_money) + + self._sale.update({ + 'residual_amount': res['residual_amount'] + }) + residual_amount = self._sale['residual_amount'] + + if self._sale['residual_amount'] <= 0: + # The sale is paid + self._done_sale() + return True + + def validate_done_sale(self): + if self.model_sale_lines.rowCount() == 0: + self.dialog('sale_without_products') + self.set_state('add') + self.message_bar.set('system_ready') + return + return True + + def _get_total_amount(self): + return self.model_sale_lines.get_sum('amount_w_tax') + + def set_discount_amount(self): + res = 0 + if self._sale['id']: + res = self.ModSale.get_discount_total({ + 'sale_id': self._sale['id'] + }) + self.field_discount.setText(res) + + def amount_text_changed(self, text=None): + if text: + self._amount_text += text + self.field_amount.setText(self._amount_text) + + def input_text_changed(self, text=None): + if text: + self._input_text += text + elif text == '': + self._input_text = '' + self.label_input.setText(self._input_text) + + def __do_invoice_thread(self): + self.ModSale.faster_post_invoice({ + 'sale_id': self.sale_to_post['id'] + }) + if self.sale_to_post['is_credit']: + return + + self.ModSale.reconcile_invoice({ + 'sale_id': self.sale_to_post['id'] + }) + + def _done_sale(self, is_credit=False): + self._sale['is_credit'] = is_credit + self.sale_to_post = self._sale + self.do_invoice.start() + if self._Tables and self._sale.get('table_assigned'): + self._Tables.write([self._sale['table_assigned']], + {'state': 'available'} + ) + + try: + if self.print_receipt == 'automatic': + _copies = self.device['shop.invoice_copies'] + if not is_credit: + self.print_invoice(copies=_copies) + if self.print_order and self.print_auto_order: + self.action_print_order() + except: + logging.error(sys.exc_info()[0]) + + if not is_credit and self._commission_activated: + agent_id = self.field_agent_ask.get_id() + if self.auto_print_commission and agent_id: + self.print_equivalent_invoice(self._sale['id']) + + if self.type_pos_user not in ('cashier', 'order') \ + and self._config.get('new_sale_automatic'): + self.createNewSale() + else: + self.state_disabled() + return True + + def print_invoice(self, sale_id=None, copies=1): + if not sale_id: + sale_id = self._sale['id'] + data = self._PosSale.get_data(sale_id, self._context) + for i in range(copies): + self.receipt_sale.print_sale(data) + + def button_accept_pressed(self): + if not self._sale['id'] or not self.model_sale_lines.rowCount() > 0: + return + self.set_state('accept') + + def button_cash_pressed(self): + if not self.check_salesman(): + return + if not self.validate_payment_term(): + return + sale_id = self._sale['id'] + res, msg = self.ModSale.faster_process({'sale_id': sale_id}) + # Remove deprecation res['res'] + if msg: + # msg = Mesagge error + self.message_bar.set(msg) + return + + self.set_amounts(res) + + self.field_invoice.setText(res['invoice_number']) + self.field_amount.zero() + if self.type_pos_user == 'salesman': + self.print_invoice(sale_id) + res = self._print_order(sale_id, 'delivery') + self.createNewSale() + return + + self.message_bar.set('enter_payment', self.default_journal['name']) + self.set_state('cash') + self.buttonpad.setFocus() + + def action_reservations(self): + logging.info('Buscando reservas.....') + + def action_tables(self): + self.dialog_manage_tables.exec_() + + def action_table_assigned(self, id, name, prev_state, new_state): + table_assigned = id + + if self._sale.get('table_assigned') != id and prev_state == 'occupied': + return False + + if self._sale.get('table_assigned') == id and new_state == 'available': + name = '' + table_assigned = None + + self._sale['table_assigned'] = table_assigned + self._sale['table_assigned.state'] = new_state + + self._Tables.write([id], {'state': new_state}) + + self._PosSale.write([self._sale['id']], {'table_assigned': table_assigned}) + self.field_table_assigned.setText(name) + return True + + def action_tip(self): + if self._config['tip_product.code'] and self._config['tip_rate']: + total_amount = int(self._get_total_amount()) + self.add_product(code=self._config['tip_product.code']) + self.button_plus_pressed() + eval_value = int((self._config['tip_rate'] / 100) * total_amount) + self.ModSaleLine.write( + [self._current_line_id], {'unit_price': Decimal(eval_value)} + ) + self._PosSale.write([self._sale['id']], {'tip': Decimal(eval_value)}) + self.update_total_amount() + + def action_salesman(self): + self.dialog_salesman.exec_() + + def action_tax(self): + self.dialog_tax.exec_() + + def action_payment(self): + if self._state != 'cash': + self.dialog('add_payment_sale_draft') + return + self.dialog_payment.exec_() + + def _check_credit_capacity(self, party): + if party['credit_limit_amount']: + if (party['credit_limit_amount'] * Decimal(RATE_CREDIT_LIMIT)) < (party['credit_amount'] + self._get_total_amount()): + self.dialog('credit_limit_capacity') + return True + + def validate_payment_term(self): + is_credit = self._payment_terms[str(self.field_payment_term_id)]['is_credit'] + party = self.Party.find([('id', '=', self.party_id)])[0] + + if is_credit: + if self.party_id == self.default_party['id']: + self.dialog('customer_not_credit') + return False + if self._credit_limit_activated: + if is_credit and not party['credit_limit_amount']: + self.dialog('customer_not_credit') + return False + + self._credit_amount = self.ModSale.get_credit_amount_party({'party_id': self.party_id}) + self._credit_limit_amount = party['credit_limit_amount'] + + if is_credit and self._credit_limit_amount and \ + self._credit_limit_amount < (self._credit_amount + self._get_total_amount()): + self.dialog('credit_limit_exceed') + return False + return True + + def action_payment_term_selection_changed(self): + is_credit = self._payment_terms[str(self.field_payment_term_id)]['is_credit'] + self._PosSale.write([self._sale['id']], {'payment_term': self.field_payment_term_id}) + if is_credit and self.type_pos_user != 'salesman': + if self.validate_credit_limit(): + self._done_sale(is_credit=True) + + def validate_credit_limit(self): + if self._credit_limit_amount and self._credit_limit_amount < (self._credit_amount + self._get_total_amount()): + self.dialog('credit_limit_exceed') + return False + else: + return True + + def action_journal_selection_changed(self): + self.message_bar.set('enter_payment', self.field_journal_name) + + def action_salesman_selection_changed(self): + self._PosSale.write([self._sale['id']], {'salesman': self.field_salesman_id}) + + def action_delivery_charge_selection_changed(self, index): + val = self.field_delivery_charge.get_id() + if val: + self._PosSale.write([self._sale['id']], {'delivery_charge': val}) + + def action_invoice_type_selection_changed(self, index): + val = self.field_invoice_type.get_id() + if val: + self._PosSale.write([self._sale['id']], {'invoice_type': val}) + + def action_tax_selection_changed(self): + res = self._PosSale.add_tax(self._sale['id'], self.field_tax_id, self._context) + self._sale.update(res) + self.set_amounts() + + def action_print_sale(self): + number = self.field_invoice.text() + if not number: + number = self.field_order_number.text() + if number: + self.field_invoice_number_ask.setText(number) + res = self.dialog_print_invoice.exec_() + if res == DIALOG_REPLY_NO: + return + number = self.field_invoice_number_ask.text() + printer_id = self.field_printer_ask.get_id() + type_doc = self.field_type_ask.get_id() + sale = {} + if number: + if type_doc == 'order': + sales = self.ModSale.find([('number', '=', number)]) + if sales: + sale = sales[0] + else: + sale = self._PosSale.get_sale_from_invoice([1], number, self._context) + + if not sale: + return self.message_bar.set('sale_number_not_found') + sale_id = sale['id'] + else: + sale_id = self._sale['id'] + + if printer_id == '1': + self.print_invoice(sale_id) + else: + self.print_odt_invoice(sale) + + def print_odt_invoice(self, sale): + if not sale.get('invoices'): + return + invoice_id = sale['invoices'][0] + model = u'account.invoice' + data = { + 'model': model, + 'action_id': self._action_report['id'], + 'id': invoice_id, + 'ids': [invoice_id], + } + ctx = {'date_format': u'%d/%m/%Y'} + ctx.update(self._context) + Action.exec_report(self.conn, u'account.invoice', + data, direct_print=True, context=ctx) + + def print_equivalent_invoice(self, sale_id): + sale, = self.ModSale.find([('id', '=', sale_id)]) + + # if not sale['invoices']: + # return + # + # invoice_id = sale['invoices'][0] + # commissions = self._Commission.find([ + # ('origin', '=', 'account.invoice,' + str(invoice_id)) + # ]) + # + # # if not commissions: + # # return + # # commission = commissions[0] + # # if not commission['invoice_line.invoice']: + # # return + # # + # # model = u'account.invoice' + # # data = { + # # 'model': model, + # # 'action_id': self._action_report_equivalent['id'], + # # 'id': commission['invoice_line.invoice'], + # # 'ids': [commission['invoice_line.invoice']], + # # } + # # ctx = {'date_format': u'%d/%m/%Y'} + # # ctx.update(self._context) + # # Action.exec_report(self.conn, u'account.invoice', + # # data, direct_print=True, context=ctx) + + def action_comment(self): + self.dialog_comment.exec_() + comment = self.field_comment_ask.text() + if comment: + self._PosSale.write([self._sale['id']], {'comment': comment}) + + def action_position(self): + self.dialog_position.exec_() + position = self.field_position_ask.text() + if hasattr(self, 'field_position') and position: + self.field_position.setText(position) + self._PosSale.write([self._sale['id']], {'position': position}) + + def action_agent(self): + self.dialog_agent.exec_() + res = self.field_commission_ask.text() + if not res: + return + commission = float(res) + sale, = self.ModSale.find([ + ('id', '=', self._sale['id']), + ]) + self.field_agent_id = self.field_agent_ask.get_id() + + if self.field_agent_id and commission: + agent, = self._Agent.find([ + ('id', '=', self.field_agent_id), + ]) + if commission <= agent['plan.percentage']: + self._PosSale.write([self._sale['id']], { + 'agent': self.field_agent_id, + 'commission': int(commission), + }) + else: + self.message_bar.set('invalid_commission') + return + self.message_bar.set('system_ready') + comm_string = str('[' + str(commission) + ']' + ' ') + (str(self.field_agent_ask.text())) + self.field_agent.setText(comm_string) + self._set_commission_amount(sale['untaxed_amount'], commission) + + def _set_commission_amount(self, untaxed_amount, commission): + untaxed_amount = int(untaxed_amount) + commission = int(commission) + total = ((untaxed_amount * commission) / 100) + self.field_commission_amount.setText(str(total)) + + def action_party(self): + self.dialog_search_parties.clear_rows() + self.dialog_search_parties.execute() + + def action_global_discount(self, sale_id=None): + self.dialog_global_discount.exec_() + discount = self.field_global_discount_ask.text() + if discount and discount.isdigit(): + if self.model_sale_lines.rowCount() > 0: + lines = [line['id'] for line in self.model_sale_lines._data] + self.set_discount(int(discount), lines) + + def _print_order(self, sale_id, kind, reversion=False): + result = False + try: + orders = self._PosSale.get_order2print(sale_id, reversion, False, self._context) + result = self.receipt_order.print_orders(orders, reversion, kind) + except: + logging.error('Printing order fail!') + return result + + def action_print_order(self, sale_id=None, reversion=False): + res = self.dialog_order.exec_() + if res == DIALOG_REPLY_NO: + return + # order_number = self.field_order_ask.text() + kind = 'delivery' + if self.enviroment == 'restaurant': + kind = 'command' + if not self.print_order: + return + if not sale_id and self._sale['id']: + sale_id = self._sale['id'] + + result = self._print_order(sale_id, kind) + # try: + # orders = self._PosSale.get_order2print(sale_id, reversion, + # False, self._context) + # result = self.receipt_order.print_orders(orders, reversion, kind) + # print(result, orders) + # except: + # print('Error: printing order fail!') + if result: + # Show dialog if send sale order was successful + self.dialog('order_successfully') + else: + self.dialog('order_failed') + + def action_payment_term(self): + if self._state == 'cash' or self.type_pos_user == 'salesman': + self.dialog_payment_term.exec_() + + def action_new_sale(self): + if not self._sale['id']: + return + if self._ask_new_sale(): + self.createNewSale() + if self.enviroment == 'restaurant': + self.wizard_new_sale() + + def wizard_new_sale(self): + # self.action_position() + # self.action_salesman() + pass + + def numpad_price_clicked(self): + code = self.label_input.text() + product = self._search_product(code) + if not product: + return + + def _ask_new_sale(self): + dialog = self.dialog('new_sale', response=True) + res = dialog.exec_() + if res == DIALOG_REPLY_NO: + return False + return True + + def action_cancel(self): + if self.type_pos_user == 'cashier': + self.dialog_cancel_invoice.exec_() + password = self.field_password_for_cancel_ask.text() + if password != self._password_admin: + return self.dialog('not_permission_for_cancel') + + if not self._sale['id']: + return + if self._state == 'cash' and not self.user_can_delete: + return self.dialog('not_permission_delete_sale') + if self.type_pos_user in ('order', 'salesman') and \ + self._sale.get('invoice_number'): + return self.dialog('not_permission_delete_sale') + dialog = self.dialog('cancel_sale', response=True) + response = dialog.exec_() + if response == DIALOG_REPLY_NO: + return + self._PosSale.cancel_sale(self._sale['id'], self._context) + self.field_password_for_cancel_ask.setText('') + self.set_state('cancel') + self.clear_right_panel() + self.createNewSale() + + def action_search_product(self): + if self._state == 'cash': + return + self.dialog_search_products.show() + + def action_search_sale(self): + delta = str(datetime.now() - timedelta(4)) + if self.type_pos_user == 'cashier': + dom = ['OR', [ + ('create_date', '>=', delta), + ('state', 'in', ['quotation', 'confirmed']), + ], [ + ('state', '=', 'processing'), + ('invoice.state', '=', 'draft'), + ('invoice.type', '=', 'out'), + ]] + elif self.type_pos_user in ('order', 'salesman'): + dom = [ + ('state', '=', 'draft'), + ('create_date', '>=', delta), + ] + else: + dom = [ + ('state', 'in', ['draft', 'quotation', 'confirmed']), + ('create_date', '>=', delta), + ] + + sales = self.ModSale.find(dom, order=[('id', 'DESC')]) + self.dialog_search_sales.set_from_values(sales) + + if self.enviroment == 'retail': + dom_draft = [ + ('create_date', '>=', delta), + ('state', '=', 'draft'), + ('invoice_number', '!=', None), + ] + sales_draft = self.ModSale.find(dom_draft) + self.dialog_search_sales.set_counter_control(sales_draft) + response = self.dialog_search_sales.execute() + if response == DIALOG_REPLY_NO: + return + + def on_selected_sale(self): + sale_id = self.dialog_search_sales.get_id() + if not sale_id: + return + self.load_sale(sale_id) + self.setFocus() + self.label_input.setFocus() + self.grabKeyboard() + + def on_selected_party(self): + party_id = self.dialog_search_parties.get_id() + party, = self.Party.find([ + ('id', '=', party_id) + ]) + if not party_id: + return + + values = { + 'party': party_id, + 'invoice_address': party['addresses'][0]['id'], + 'shipment_address': party['addresses'][0]['id'], + } + + if party.get('customer_payment_term'): + values['payment_term'] = party.get('customer_payment_term') + self.field_payment_term.setText(str(party['customer_payment_term.name'])) + self.field_payment_term_id = party.get('customer_payment_term') + else: + values['payment_term'] = self.default_payment_term['id'] + self.field_payment_term_id = self.default_payment_term['id'] + self.field_payment_term.setText(self.default_payment_term['name']) + + self._PosSale.write([self._sale['id']], values) + + self.party_id = party_id + self.field_party.setText(party['name']) + + res = self.ModSale.get_salesman_in_party({ + 'party_id': party_id, + }) + if res: + self.field_salesman_id = res['id'] + self._PosSale.write([self._sale['id']], {'salesman': self.field_salesman_id}) + self.field_salesman.setText(res['name']) + + # if self._credit_limit_activated: + # self._check_credit_capacity(party) + + self.message_bar.set('system_ready') + self.setFocus() + self.label_input.setFocus() + self.grabKeyboard() + + def load_sale(self, sale_id): + # loads only draft sales + self.is_clear_right_panel = True + self.clear_data() + self.clear_left_panel() + self.clear_right_panel() + sale, = self.ModSale.find([('id', '=', sale_id)]) + self._sale.update(sale) + self.table_payment_lines.reset() + self.field_order_number.setText(sale['number'] or '') + self._set_sale_date() + if hasattr(self, 'field_position'): + self.field_position.setText(sale['position'] or '') + if sale.get('payment_term.name'): + self.field_payment_term_id = sale['payment_term'] + self.field_payment_term.setText(sale['payment_term.name'] or '') + if sale.get('salesman'): + self.field_salesman.setText(sale['salesman.rec_name'] or '') + self.field_salesman_id = sale['salesman'] + + res = self.ModSale.get_invoice_type({'sale_id': sale_id}) + if res: + self.field_invoice_type.set_from_id(res['invoice_type']) + + if sale.get('invoice_number'): + self.field_invoice.setText(sale['invoice_number'] or '') + else: + self.field_invoice.setText('') + if self.field_delivery_charge: + self.field_delivery_charge.set_enabled(True) + if self._sale.get('delivery_charge'): + self.field_delivery_charge.set_from_id(self._sale['delivery_charge']) + if sale.get('table_assigned'): + self.field_table_assigned.setText(sale['table_assigned.name'] or '') + + self.field_change.zero() + if self._commission_activated: + if hasattr(self, 'field_agent') and sale.get('agent.party.name') \ + and sale.get('commission'): + commission = sale.get('commission') + self.field_agent.setText('[' + str(int(commission)) + ']' + ' ' + sale.get('agent.party.name')) + self.field_agent_id = sale.get('agent') + self.field_agent_ask.setText(sale.get('agent.party.name')) + self.field_commission_ask.setText(str(commission)) + self._set_commission_amount(sale['untaxed_amount'], commission) + + if sale.get('lines'): + lines = self.ModSaleLine.find([ + ('id', 'in', sale.get('lines')) + ]) + sale['lines'] = lines + for line in lines: + self.add_sale_line(line) + + if sale.get('payments'): + for payment in sale['payments']: + self.table_payment_lines.record_add(payment) + + self.set_state('add') + self.party_id = sale['party'] + self.field_party.setText(sale['party.name']) + self.set_amounts(sale) + self.set_amount_received() + self.field_amount.zero() + if self.type_pos_user in ('cashier', 'order', 'salesman'): + self.table_sale_lines.setEnabled(True) + + def set_change_amount(self): + amount_paid = self.table_payment_lines.get_sum('amount') + res = amount_paid - self._get_total_amount() + self.field_change.setText(res) + + def set_amount_received(self, cash_received=ZERO): + residual_amount = self._sale['residual_amount'] + if cash_received: + amount = cash_received + else: + amount = self._sale['paid_amount'] + self.field_pending.setText(residual_amount) + self.field_paid.setText(amount) + + def update_total_amount(self): + self.set_amounts() + # res = self.model_sale_lines.get_sum('amount_w_tax') + # self.field_total_amount.setText(res) + + def set_amounts(self, res=None): + if not res: + res = self._PosSale.get_amounts([self._sale['id']], self._context) + + self._sale.update(res) + self.field_untaxed_amount.setText(res['untaxed_amount']) + self.field_taxes_amount.setText(res['tax_amount']) + self.field_total_amount.setText(res['total_amount']) + self.set_discount_amount() + + def _get_products_by_category(self, cat_id): + records = self.Product.find([ + ('code', '!=', None), + ('template.salable', '=', True), + ('template.categories', '=', cat_id), + ]) + return [ + [r['id'], r['code'], r['template.name'], r['template.sale_price_w_tax']] + for r in records] + + def get_product_by_categories(self): + domain = [ + ('parent', '=', None), + ('accounting', '=', False), + ('id', 'in', self.product_categories) + ] + self.allow_categories = self.Category.find(domain) + + for cat in self.allow_categories: + cat['icon'] = get_icon(cat['name_icon']) + cat['items'] = self._get_products_by_category(cat['id']) + return self.allow_categories + + def _get_childs(self, parent_cat): + res = {} + for cat_id in parent_cat['childs']: + sub_categories = self.Category.find([ + ('parent', '=', parent_cat['id']) + ]) + + for sub_cat in sub_categories: + res.update(self._get_childs(sub_cat)) + res = { + 'id': parent_cat['id'], + 'name': parent_cat['name'], + 'childs': parent_cat['childs'], + 'records': [], + 'obj': parent_cat, + } + return res + + def get_category_items(self, records): + records_by_category = {} + + def _get_tree_categories(cat): + sub_categories = {} + if not cat['childs']: + sub_categories[cat['name']] = records_by_category.get(cat['id']) or [] + else: + for child in cat['childs']: + sub_categories.update(_get_tree_categories( + self.target_categories[child]['obj'])) + return sub_categories + + for record in records: + cat_id = record.get('template.account_category') + if cat_id not in records_by_category.keys(): + records_by_category[cat_id] = [] + + records_by_category[cat_id].append(record) + + res = {} + for ac in self.allow_categories: + res[ac['name']] = _get_tree_categories(ac) + return res + + def on_selected_product(self): + if self.dialog_search_products.current_row: + self._current_line_id = None + self.clear_right_panel() + self.add_product(product=self.dialog_search_products.current_row) + + def on_selected_icon_product(self): + if self.dialog_search_products.current_row: + code = self.dialog_search_products.current_row['code'] + products = self.Product.find([ + ('code', '=', code) + ]) + if not products: + return + + product = products[0] + image = Image(name='product_icon') + if not product['image']: + return + + b64image = product['image'].encode() + image.set_image(b64image, kind='bytes') + image.activate() + + def on_selected_stock_product(self): + if self.dialog_search_products.current_row: + code = self.dialog_search_products.current_row['code'] + res = self.Product.get_stock_by_locations({'code': code}) + self.dialog_product_stock.update_values(res) + self.dialog_product_stock.show() + + def on_selected_item(self, product_id): + if product_id: + self.clear_right_panel() + self.add_product(id=product_id) + + def create_dialog_manage_tables(self): + if not self._Tables: + return + tables = self._Tables.find([ + ('shop', '=', self.shop['id']) + ]) + self.tables = ManageTables(self, tables, self.action_table_assigned) + self.dialog_manage_tables = QuickDialog(self, 'action', widgets=[self.tables]) + + def create_dialog_search_sales(self): + headers = [ + ('id', self.tr('ID')), + ('number', self.tr('NUMBER')), + ('invoice_number', self.tr('INVOICE')), + ('sale_date', self.tr('DATE')), + ('salesman.party.name', self.tr('SALESMAN')), + ('position', self.tr('POSITION')), + ('total_amount_cache', self.tr('TOTAL AMOUNT')), + ] + widths = [20, 100, 100, 90, 150, 100, 100] + if self.enviroment == 'retail': + headers.insert(2, ('party.name', self.tr('PARTY'))) + widths.insert(2, 150) + title = self.tr('SEARCH SALES...') + methods = { + 'on_selected_method': 'on_selected_sale', + 'on_return_method': 'on_selected_sale' + } + self.dialog_search_sales = SearchWindow(self, headers, None, methods, + filter_column=[1, 2, 3, 4], cols_width=widths, title=title, fill=True) + + self.dialog_search_sales.activate_counter() + + def create_dialog_search_products(self): + _cols_width = [10, 80] + headers = [ + ('id', self.tr('ID')), + ('code', self.tr('CODE')), + ] + if self._config.get('show_stock_pos') in ['icon', 'value']: + if self._config['show_stock_pos'] == 'icon': + headers.append(('icon_stock', self.tr('STOCK'))) + else: + headers.append(('quantity', self.tr('STOCK'))) + _cols_width.append(80) + + headers.append( + ('template.name', self.tr('NAME')), + ) + _cols_width.append(350) + + if self._config.get('show_description_pos'): + headers.append(('description', self.tr('DESCRIPTION'))) + _cols_width.append(300) + + if self._config.get('show_brand'): + headers.append(('brand.name', self.tr('BRAND'))) + _cols_width.append(100) + + if not self._config.get('encoded_sale_price'): + sale_price = ('template.sale_price_w_tax', self.tr('PRICE')) + else: + sale_price = ('encoded_sale_price', self.tr('PRICE')) + headers.append(sale_price) + _cols_width.append(90) + + if self._config.get('show_location_pos'): + headers.append(('location.name', self.tr('LOCATION'))) + _cols_width.append(100) + if self._config['show_product_image']: + headers.append(('icon_camera', self.tr('IMAGE'))) + _cols_width.append(30) + + methods = { + 'on_selected_method': 'on_selected_product', + 'on_return_method': 'on_search_product', + 'icon_camera': self.on_selected_icon_product, + 'icon_stock': self.on_selected_stock_product, + 'quantity': self.on_selected_stock_product + } + + self.dialog_search_products = SearchWindow(self, headers, None, + methods, cols_width=_cols_width, fill=True) + + def create_dialog_search_party(self): + headers = [ + ('id', self.tr('ID')), + ('id_number', self.tr('ID NUMBER')), + ('name', self.tr('NAME')), + ('street', self.tr('ADDRESS')), + ('phone', self.tr('PHONE')), + ] + title = self.tr('SEARCH CUSTOMER') + methods = { + 'on_selected_method': 'on_selected_party', + 'on_return_method': 'on_search_party', + } + self.dialog_search_parties = SearchWindow(self, headers, None, + methods, filter_column=[], cols_width=[60, 120, 270, 190, 90], + title=title, fill=True) + + def create_dialog_payment(self): + data = { + 'name': 'journal', + 'values': sorted([(str(j), self._journals[j]['name']) + for j in self._journals]), + 'heads': [self.tr('ID'), self.tr('PAYMENT MODE:')], + } + string = self.tr('SELECT PAYMENT MODE:') + self.dialog_payment = QuickDialog(self, 'selection', string, data) + + def create_dialog_stock(self): + data = { + 'name': 'stock', + 'values': [], + 'heads': [self.tr('WAREHOUSE'), self.tr('QUANTITY')], + } + label = self.tr('STOCK BY PRODUCT:') + self.dialog_product_stock = QuickDialog(self.dialog_search_products, + 'selection', label, data, readonly=True) + + def create_dialog_salesman(self): + print(self.employees) + data = { + 'name': 'salesman', + 'values': [(str(e['id']), e['party']['name']) + for e in self.employees], + 'heads': [self.tr('Id'), self.tr('Salesman')], + } + string = self.tr('CHOOSE SALESMAN') + self.dialog_salesman = QuickDialog(self, 'selection', string, data) + + def create_dialog_taxes(self): + if self.shop_taxes: + taxes = [(str(e['id']), e['name']) for e in self.shop_taxes] + else: + taxes = [] + data = { + 'name': 'tax', + 'values': taxes, + 'heads': [self.tr('Id'), self.tr('Salesman')], + } + string = self.tr('CHOOSE TAX') + self.dialog_tax = QuickDialog(self, 'selection', string, data) + + def create_dialog_payment_term(self): + data = { + 'name': 'payment_term', + 'values': [(p_id, self._payment_terms[p_id]['name']) + for p_id in self._payment_terms], + 'heads': [self.tr('ID'), self.tr('PAYMENT TERM')], + } + string = self.tr('SELECT PAYMENT TERM') + self.dialog_payment_term = QuickDialog(self, 'selection', string, data) + + def on_search_product(self): + target = self.dialog_search_products.filter_field.text() + if not target: + return + target_words = target.split(' ') + domain = [] + + for tw in target_words: + if len(tw) <= 1: + continue + clause = ['OR', + ('template.name', 'ilike', '%{:}%'.format(tw)), + ('description', 'ilike', '%{:}%'.format(tw)), + ] + domain.append(clause) + + if not domain: + return + if self.cache_local: + products = self.store.find_product_elastic(domain, limit=100) + else: + products = self.Product.find(domain, limit=100, ctx=self.stock_context) + self.dialog_search_products.set_from_values(products) + + def on_search_party(self): + target = self.dialog_search_parties.filter_field.text() + if not target: + return + target_words = target.split(' ') + domain = [('id_number', '!=', None)] + for tw in target_words: + if len(tw) <= 2: + continue + or_clause = ['OR', + ('name', 'ilike', '%' + tw + '%'), + ('contact_mechanisms.value', 'like', tw + '%'), + ('id_number', 'like', tw + '%'), + ] + domain.append(or_clause) + + parties = self.Party.find(domain) + self.dialog_search_parties.set_from_values(parties) + + def create_dialog_print_invoice(self): + view = [ + ('invoice_number_ask', {'name': self.tr('INVOICE NUMBER')}), + ('printer_ask', { + 'name': self.tr('PRINTER'), + 'type': 'selection', + 'values': [ + (1, 'POS'), + (2, 'LASER') + ], + }), + ('type_ask', { + 'name': self.tr('TYPE'), + 'type': 'selection', + 'values': [ + ('invoice', self.tr('INVOICE')), + ('order', self.tr('ORDER')) + ], + }), + ] + self.dialog_print_invoice = QuickDialog(self, 'action', data=view) + + def create_dialog_cancel_invoice(self): + view = [ + ('password_for_cancel_ask', { + 'name': self.tr('INSERT PASSWORD FOR CANCEL'), + 'password': True + }), + ] + self.dialog_cancel_invoice = QuickDialog(self, 'action', data=view) + + def create_dialog_global_discount(self): + field = 'global_discount_ask' + data = {'name': self.tr('GLOBAL DISCOUNT')} + self.dialog_global_discount = QuickDialog(self, 'action', data=[(field, data)]) + + def create_dialog_force_assign(self): + field = 'password_force_assign_ask' + data = {'name': self.tr('PASSWORD FORCE ASSIGN')} + self.dialog_force_assign = QuickDialog(self, 'action', data=[(field, data)]) + self.field_password_force_assign_ask.setEchoMode(QLineEdit.Password) + + def create_dialog_voucher(self): + field = 'voucher_ask' + data = {'name': self.tr('VOUCHER NUMBER')} + self.dialog_voucher = QuickDialog(self, 'action', data=[(field, data)]) + + def create_dialog_order(self): + # field = 'Send Order' + # data = {'name': self.tr('COPY NUMBER')} + string = self.tr('DO YOU WANT TO CONFIRM THE SEND ORDER?') + self.dialog_order = QuickDialog(self, 'action', string, data=[]) + + def create_dialog_position(self): + field = 'position_ask' + data = {'name': self.tr('POSITION')} + self.dialog_position = QuickDialog(self, 'action', data=[(field, data)]) + + def create_dialog_agent(self): + view = [ + ('agent_ask', { + 'name': self.tr('AGENT'), + 'type': 'relation', + 'model': self._Agent, + 'domain': [('active', '=', True)], + 'fields': [ + ('id', self.tr('ID')), + ('party.name', self.tr('NAME')), + ('party.id_number', self.tr('ID NUMBER')), + ]}), + ('commission_ask', {'name': self.tr('COMMISSION')}), + ('commission_amount', {'name': self.tr('AMOUNT'), + 'readonly': True}), + ] + self.dialog_agent = QuickDialog(self, 'action', data=view, size=(600, 400)) + + def create_wizard_new_sale(self): + pass + + def create_dialog_comment(self): + field = 'comment_ask' + data = {'name': self.tr('COMMENTS'), 'widget': 'text'} + self.dialog_comment = QuickDialog(self, 'action', data=[(field, data)]) + + def clear_data(self): + self._sale = { + 'total_amount': 0 + } + self.party_name = None + self._sale_line = {'id': None} + self._total_amount = {} + self._sale_lines_taxes = {} + self.field_journal_id = self.default_journal['id'] + + def clear_left_panel(self): + self.message_bar.set('system_ready') + self.field_party.setText('') + self.field_salesman.setText('') + self.field_salesman_id = None + self.field_invoice_type.set_from_id('') + self.field_party_id = None + self.field_agent_id = None + self.field_payment_term_id = self.default_payment_term['id'] + self.field_payment_term.setText(self.default_payment_term['name']) + self.field_date.setText('') + self.field_global_discount_ask.setText('') + self.field_amount.zero() + self.field_order_number.setText('') + self.current_comment = '' + if self.field_delivery_charge: + self.field_delivery_charge.set_from_id('') + if self.field_table_assigned: + self.field_table_assigned.setText('') + if hasattr(self, 'field_comment_ask'): + # self.field_comment_ask.document().clear() + pass + if hasattr(self, 'field_points'): + self.field_points.setText('') + if self._commission_activated and hasattr(self, 'field_agent'): + self.field_agent.setText('') + self.field_agent_ask.setText('') + self.field_commission_ask.setText('') + self.field_commission_amount.setText('') + if hasattr(self, 'field_position'): + self.field_position.setText('') + self.field_position_ask.setText('') + self.model_sale_lines.reset() + self.clear_input_text() + self.clear_amount_text() + + def clear_right_panel(self): + if self.is_clear_right_panel: + return + self.field_invoice.setText('') + self.field_untaxed_amount.zero() + self.field_taxes_amount.zero() + self.field_total_amount.zero() + self.field_change.zero() + self.field_paid.zero() + self.field_discount.zero() + self.table_payment_lines.reset() + self.is_clear_right_panel = True + + def state_enabled(self): + pass + + def state_disabled(self): + self.payment_ctx = {} + self.clear_left_panel() + self.table_sale_lines.setDisabled(True) + self.set_state('disabled') + if self.field_delivery_charge: + self.field_delivery_charge.set_enabled(False) + + def createNewSale(self): + self.check_empty_sale() + if self.type_pos_user == 'cashier': + self.state_disabled() + return + self.set_state('add') + self.input_text_changed('') + self.amount_text_changed('0') + self.clear_sign() + self.global_timer = 0 + self.payment_ctx = {} + self.is_clear_right_panel = False + self.table_sale_lines.setEnabled(True) + self.clear_data() + self.clear_left_panel() + self._sale = self._PosSale.new_sale([], { + 'shop': self.shop['id'], + 'invoice_type': 'P', + 'company': self.company['id'], + 'party': self.default_party['id'], + 'sale_device': self.device['id'], + 'payment_term': self.default_payment_term['id'] + }, self._context) + + self.field_invoice_type.set_from_id(self._sale['invoice_type']) + # FIXME ADD MORE + self._sale.update({ + 'total_amount': 0 + }) + self.party_id = self.default_party['id'] + if self._sale.get('id'): + self._set_sale_date() + self.field_order_number.setText(self._sale['number']) + if self.field_delivery_charge: + self.field_delivery_charge.set_enabled(True) + + def _set_sale_date(self): + if self._sale.get('sale_date'): + local_date = self._sale['sale_date'] + if isinstance(local_date, date): + local_date = local_date.isoformat() + self.field_date.setText(local_date) + + def _search_product(self, code): + domain = [ + ('template.salable', '=', True), + ('template.account_category', '!=', None), + ] + domain.append(['OR', + ('barcode', '=', code), + ('code', '=', code), + ]) + products = self.Product.find(domain) + if not products or len(products) > 1: + self.message_bar.set('product_not_found') + return False + else: + product = products[0] + return product + + def check_salesman(self): + if self.salesman_required and not self.field_salesman_id: + dialog = self.dialog('missing_salesman') + dialog.exec_() + return False + return True + + def add_product(self, id=None, code=None, product=None): + if self._state == 'disabled': + self.message_bar.set('must_load_or_create_sale') + return + + product_id = None + if id: + product_id = id + elif code: + # REMOVE ME + product = self._search_product(code) + if product: + product_id = product['id'] + elif product: + product_id = product['id'] + + if not product_id: + self._state = 'warning' + return + + data = { + 'sale_id': self._sale['id'], + 'product_id': product_id, + 'qty': 1 + } + + res = self.ModSale.faster_add_product(data) + self._sale_line = res + self._current_line_id = res['id'] + self.add_sale_line(res) + self._sale_line['product'] = product + + self.update_total_amount() + self.set_state('add') + + def _check_stock_quantity(self, product, request_qty): + if self._password_admin and product['quantity'] < request_qty: + self.dialog_force_assign.exec_() + password = self.field_password_force_assign_ask.text() + self.field_password_force_assign_ask.setText('') + if password != self._password_admin: + self.message_bar.set('not_can_force_assign') + return False + return True + + def add_sale_line(self, record): + rec = self.model_sale_lines.add_record(record) + self.field_amount.setText(rec['amount_w_tax']) + + def sale_line_selected(self, product): + if self._state == 'cash': + return + self._current_line_id = product['id'] + self.label_product.setText(product['product.template.name']) + self.row_field_description.setText(product['description']) + self.row_field_qty.setValue(float(product['quantity'])) + self.row_field_price.setText(str(product['unit_price_w_tax'])) + # self.row_field_note.setText(str(product['note'])) + + self.dialog_product_edit.show() + self.row_field_note.setFocus() + + def create_dialog_sale_line(self): + self.state_line = {} + + vbox_product = QVBoxLayout() + grid = QGridLayout() + qty = 2 + + self.label_product = QLabel() + self.label_product.setAlignment(alignCenter) + self.label_product.setObjectName('label_product') + vbox_product.addWidget(self.label_product) + self.row_field_description = QLineEdit() + self.row_field_description.setObjectName('row_field_description') + self.row_field_description.textChanged.connect( + lambda: self.update_sale_line('description') + ) + grid.addWidget(self.row_field_description, 1, 1, 1, 2) + + if self._config.get('show_fractions'): + label_fraction = QLabel(self.tr('FRACTION:')) + label_fraction.setObjectName('label_fraction') + grid.addWidget(label_fraction, 2, 1) + self.field_combobox_fraction = ComboBox(self, 'fraction', + {'values': FRACTIONS}) + grid.addWidget(self.field_combobox_fraction, 2, 2) + self.field_combobox_fraction.currentIndexChanged.connect( + lambda: self.update_sale_line('qty_fraction') + ) + + label_qty = QLabel(self.tr('QUANTITY:')) + label_qty.setObjectName('label_qty') + grid.addWidget(label_qty, 3, 1) + self.row_field_qty = QDoubleSpinBox() + self.row_field_qty.setObjectName('row_field_qty') + self.row_field_qty.setMinimum(0) + self.row_field_qty.setMaximum(100000) + if self._config.get('decimals_digits_quantity'): + qty = self._config['decimals_digits_quantity'] + + self.row_field_qty.setDecimals(qty) + self.row_field_qty.setAlignment(alignCenter) + grid.addWidget(self.row_field_qty, 3, 2) + self.row_field_qty.valueChanged.connect( + lambda: self.update_sale_line('quantity') + ) + + label_price = QLabel(self.tr('UNIT PRICE:')) + label_price.setObjectName('label_price') + grid.addWidget(label_price, 4, 1) + self.row_field_price = FieldMoney(self, 'row_field_price', {}, readonly=False) + self.row_field_price.setObjectName('row_field_price') + grid.addWidget(self.row_field_price, 4, 2) + self.row_field_price.textChanged.connect( + lambda: self.update_sale_line('unit_price') + ) + + self.row_field_note = QTextEdit('') + self.row_field_note.setObjectName('row_field_note') + grid.addWidget(self.row_field_note, 5, 1, 5, 2) + self.row_field_note.textChanged.connect( + lambda: self.update_sale_line('note') + ) + + vbox_product.addLayout(grid) + self.dialog_product_edit = QuickDialog(self, 'action', widgets=[vbox_product]) + self.dialog_product_edit.accepted.connect(self.dialog_product_edit_accepted) + + def update_sale_line(self, field): + value = None + self.state_line['id'] = self._current_line_id + if field == 'quantity': + value = Decimal(self.row_field_qty.value()) + if field == 'unit_price': + value = self.row_field_price.text() + if field == 'qty_fraction': + qty = self.field_combobox_fraction.get_id() + self.row_field_qty.setValue(float(qty)) + value = self.field_combobox_fraction.get_label() + self.state_line['quantity'] = qty + + price_ = self.ModSale.get_product_prices({ + 'ids': [self._sale_line['product']['id']], + 'quantity': float(qty), + 'sale_id': self._sale['id'], + }) + if price_ and price_.get('unit_price_w_tax'): + price_list = str(price_['unit_price_w_tax']) + self.row_field_price.setText(price_list) + self.state_line['unit_price'] = price_list + + if field == 'description': + value = self.row_field_description.text() + if field == 'note': + value = self.row_field_note.toPlainText() + + if value: + self.state_line[field] = value + + + def dialog_product_edit_accepted(self): + if not self.state_line: + return + + _record = None + + if self.state_line.get('quantity'): + quantity = self.state_line.pop('quantity') + _record = self.ModSaleLine.faster_set_quantity({ + 'id': self._current_line_id, + 'quantity': to_float(quantity, 2) + }) + + if self.state_line.get('unit_price'): + unit_price = self.state_line.pop('unit_price') + self._sign = '/' + self._process_price(unit_price) + self._sign = None + _record = self.ModSaleLine.write([self._current_line_id], {}) + + if self.state_line.get('description'): + _record = self.ModSaleLine.write([self._current_line_id], { + 'description': self.state_line['description'] + }) + + if self.state_line.get('note'): + _record = self.ModSaleLine.write([self._current_line_id], { + 'note': self.state_line['note'] + }) + + if _record: + _record['product.template.name'] = _record['product']['name'] + _record['product.code'] = _record['product']['code'] + self.model_sale_lines.update_record(_record) + + self.update_total_amount() + self.state_line = {} + # self.field_combobox_fraction.set_from_id(1) + + def setup_sale_line(self): + product_code = { + 'name': 'product.code', + 'align': alignRight, + 'description': self.tr('COD'), + 'width': 80 + } + product = { + 'name': 'product.template.name', + 'align': alignLeft, + 'description': self.tr('NAME'), + 'width': STRETCH + } + description = { + 'name': 'description', + 'align': alignLeft, + 'description': self.tr('DESCRIPTION'), + 'width': 180 + } + uom = { + 'name': 'unit.symbol', + 'align': alignHCenter, + 'description': self.tr('UNIT'), + 'width': 50 + } + qty = { + 'name': 'quantity', + 'format': '{:3,.%sf}', + 'align': alignRight, + 'description': self.tr('QTY'), + 'digits': ('unit.symbol', CONVERSION_DIGITS), + 'width': 80 + } + discount = { + 'name': 'discount', + 'format': '{0:.0%}', + 'align': alignRight, + 'description': self.tr('DISC'), + 'width': 50 + } + amount = { + 'name': 'amount_w_tax', + 'format': '{:5,.1f}', + 'align': alignRight, + 'description': self.tr('SUBTOTAL'), + 'width': 100 + } + note = { + 'name': 'note', + 'align': alignLeft, + 'description': self.tr('NOTE'), + 'invisible': True, + 'width': 100 + } + unit_price = { + 'name': 'unit_price_w_tax', + 'format': '{:5,.1f}', + 'align': alignLeft, + 'description': self.tr('UNIT PRICE W TAX'), + 'invisible': True, + 'width': 100 + } + + qty_fraction = { + 'name': 'qty_fraction', + 'align': alignHCenter, + 'description': self.tr('FRAC'), + 'width': 50 + } + + self.fields_sale_line = [product, uom, qty, discount, + amount, note, unit_price] + + if self.enviroment == 'retail': + self.fields_sale_line.insert(0, product_code) + + if self._config.get('show_description_pos'): + self.fields_sale_line.insert(2, description) + + if self._config.get('show_fractions'): + self.fields_sale_line.insert(4, qty_fraction) + + self.model_sale_lines = TableModel('sale.line', self.fields_sale_line) + + def setup_payment(self): + pay_fields = [{ + 'name': 'statement', + 'align': alignLeft, + 'description': self.tr('STATEMENT JOURNAL'), + }, { + 'name': 'amount', + 'align': alignRight, + 'format': '{:5,.1f}', + 'description': self.tr('AMOUNT'), + }, { + 'name': 'voucher', + 'align': alignCenter, + 'description': self.tr('VOUCHER'), + }] + self.table_payment_lines = TableModel('account.statement.line', pay_fields) + + def action_table(self): + self.table_sale_lines.setFocus() + + def on_change_line_selected(self, key): + self.table_sale_lines.moved_selection(key) + + def action_delete_line(self): + if self.model_sale_lines.rowCount() <= 0 or self._state == 'cash': + return + + self.table_sale_lines.setFocus() + + removed_item = self.table_sale_lines.delete_item() + self.ModSaleLine.delete([removed_item['id']]) + self.set_amounts() + self.update_total_amount() + # self.clear_right_panel() + + self._current_line_id = None + self.setFocus() + self.label_input.setFocus() + + if self.enviroment == 'restaurant': + if removed_item and self.print_order: + self.action_print_order(self._sale['id'], removed_item) + if self._config['tip_product.code'] == removed_item['product.code']: + self._PosSale.write([self._sale['id']], {'tip': None}) + + def set_discount(self, eval_value, lines_ids=[]): + res = False + try: + value = round(float(str(eval_value)), 6) + except ValueError: + logging.warning('ValueError > ', ValueError) + return + + if float(value) <= 0: + return + + if not lines_ids: + target_lines = [self._current_line_id] + else: + target_lines = lines_ids + + records = self.ModSaleLine.faster_set_discount({ + 'line_ids': target_lines, + 'value': value + }) + + for rec in records: + self.model_sale_lines.update_record(rec) + + if records: + res = True + self.set_amounts() + return res + + def set_unit_price(self, value): + rec = self.ModSaleLine.set_faster_unit_price({ + 'id': self._current_line_id, + 'value': value, + }) + + if rec: + self.model_sale_lines.update_record(rec) + self.update_total_amount() + return True + return False + + def add_payment(self, amount, cash_received=0, change=0): + voucher_number = None + if self._journals[self.field_journal_id]['require_voucher']: + self.dialog_voucher.exec_() + voucher_number = self.field_voucher_ask.text() + if voucher_number is None or voucher_number == '': + return self.add_payment(amount) + + res = self.ModSale.faster_add_payment({ + 'sale_id': self._sale['id'], + 'journal_id': self.field_journal_id, + 'amount': to_numeric(amount), + 'voucher_number': voucher_number, + 'cash_received': to_numeric(cash_received), + 'change': to_numeric(change) + }) + + if res.get('msg') not in ('missing_money', 'ok'): + self.dialog(res['msg']) + return res + + self.table_payment_lines.add_record(res) + return res + + def create_dialog_help(self): + from .help import Help + help = Help(self) + help.show() + + def set_keys(self): + self.keys_numbers = list(range(Qt.Key_0, Qt.Key_9 + 1)) + self.keys_alpha = list(range(Qt.Key_A, Qt.Key_Z + 1)) + self.keys_period = [Qt.Key_Period] + self.show_keys = self.keys_numbers + self.keys_alpha + self.keys_period + + self.keys_special = [Qt.Key_Asterisk, Qt.Key_Comma, + Qt.Key_Minus, Qt.Key_Slash] + self.keys_input = [Qt.Key_Backspace] + self.keys_input.extend(self.keys_special) + self.keys_input.extend(self.show_keys) + self.keys_input.extend(self.keys_numbers) + self.keys_input.extend([Qt.Key_Return, Qt.Key_Plus]) + + def set_state(self, state='add'): + self._state = state + state = STATES[state] + self._re = state['re'] + if not self.type_pos_user == 'order': + if not self.buttonpad.stacked.stacked.currentWidget(): + return + if state['button']: + self.buttonpad.stacked.stacked.setCurrentWidget( + getattr(self.buttonpad.stacked, state['button']) + ) + if not self.tablet_mode: + self.buttonpad.stacked.stacked.currentWidget().setVisible(True) + else: + self.buttonpad.stacked.stacked.currentWidget().setVisible(False) + + def key_pressed(self, text): + if not self._sign and self._state != 'cash': + if self._re.match(self._input_text + text): + self.input_text_changed(text) + else: + if RE_SIGN['quantity'].match(self._amount_text + text): + self.amount_text_changed(text) + + def clear_sign(self): + self._sign = None + self.field_sign.setText(' {0} '.format(' ')) + + def sign_text_changed(self, sign): + self._sign = sign + self.field_sign.setText(' {0} '.format(sign)) + if hasattr(self, '_sale_line') and self._sale_line: + if sign == '-': + self.message_bar.set('enter_discount') + elif sign == '/': + self.message_bar.set('enter_new_price') + elif sign == '*': + self.message_bar.set('enter_quantity') + if self.active_weighing and self._sale_line['unit_symbol'] != 'u': + self.action_read_weight() + + def key_special_pressed(self, value): + self.clear_amount_text() + self.clear_input_text() + if value not in ['-', '/', '*']: + return + self.sign_text_changed(value) + + def key_backspace_pressed(self): + if self._sign or self._state == 'cash': + self._amount_text = self._amount_text[:-1] + self.amount_text_changed() + else: + self._input_text = self._input_text[:-1] + self.input_text_changed() + + def set_text(self, text): + if not self._state == 'cash': + self.input_text_changed(text) + else: + self.amount_text_changed(text) + + def clear_input_text(self): + self.input_text_changed('') + + def clear_amount_text(self): + self._amount_text = '0' + self.amount_text_changed() + + def keyPressEvent(self, event): + self._keyStates[event.key()] = True + key = event.key() + + if self._state == 'add' and key not in self.keys_input and \ + key not in (Qt.Key_Enter, Qt.Key_End): + # Clear ui context when keys function are pressed + self._clear_context() + + if key in (Qt.Key_Return, Qt.Key_Plus): + self.button_plus_pressed() + elif key in self.show_keys: + # No allow change quantity o discount in state == cash + if self._state == 'cash' and key not in self.keys_numbers: + return + self.key_pressed(event.text()) + elif key in self.keys_special: + if self._state == 'cash' or not self._current_line_id: + return + self.key_special_pressed(event.text()) + elif key == Qt.Key_Backspace: + self.key_backspace_pressed() + elif key == Qt.Key_Escape: + self.close() + elif key == Qt.Key_F1: + self.create_dialog_help() + elif key == Qt.Key_F9: + self.action_search_sale() + elif key == Qt.Key_F11: + self.action_new_sale() + elif key == Qt.Key_F7: + self.action_print_sale() + elif self._state == 'disabled': + self.message_bar.set('must_load_or_create_sale') + return + elif key in (Qt.Key_Enter, Qt.Key_End): + if self.type_pos_user in ['order', 'salesman']: + return + if self._state == 'add': + self.button_accept_pressed() + elif self._state in ['accept', 'cash']: + self.button_cash_pressed() + elif key == Qt.Key_F2: + self.action_search_product() + elif key == Qt.Key_F3: + self.action_payment() + elif key == Qt.Key_F4: + self.action_party() + elif key == Qt.Key_F5: + self.action_global_discount() + elif key == Qt.Key_F6: + self.action_print_order() + elif key == Qt.Key_F8: + self.action_payment_term() + elif key == Qt.Key_F10: + self.action_table() + elif key == Qt.Key_F12: + self.action_cancel() + elif key == Qt.Key_Home: + self.action_salesman() + elif key == Qt.Key_Down or key == Qt.Key_Up: + self.on_change_line_selected(key) + elif key == Qt.Key_Delete: + self.action_delete_line() + elif key == Qt.Key_Insert: + self.action_position() + elif key == Qt.Key_Semicolon and self._commission_activated: + sale = self.get_current_sale() + if sale['state'] == 'draft' and self._state not in ['accept', 'cash']: + self.action_agent() + elif key == Qt.Key_QuoteDbl: + self.action_comment() + elif key == Qt.Key_Question: + self.action_tax() + else: + pass + + @property + def state(self): + return self._state + + +class DoInvoice(QThread): + """ + Process invoices using a thread + """ + sigDoInvoice = pyqtSignal() + + def __init__(self, main, context): + QThread.__init__(self) + + def run(self): + self.sigDoInvoice.emit() diff --git a/app/manage_tables.py b/app/manage_tables.py new file mode 100644 index 0000000..560659d --- /dev/null +++ b/app/manage_tables.py @@ -0,0 +1,64 @@ +import os + +from PyQt5.QtWidgets import QGridLayout, QPushButton + +DIR_SHARE = os.path.abspath( + os.path.normpath(os.path.join(__file__, '..', '..', 'share'))) + +__all__ = ['ManageTables'] + +STATES = { + 'available': 'rgb(180, 180, 180)', + 'occupied': 'rgb(255, 210, 30)', + 'reserved': 'rgb(150, 30, 0)' +} + + +class CallButton(QPushButton): + + def __init__(self, value, method): + super(CallButton, self).__init__(value['name']) + self.setAutoFillBackground(True) + self.name = value['name'] + self.id = value['id'] + self.state = value['state'] + self.method = method + self.set_state(value['state']) + self.clicked.connect(self.handle_click) + + def handle_click(self): + if self.state == 'available': + state = 'occupied' + else: + state = 'available' + + res = self.method(self.id, self.name, self.state, state) + if not res: + return + + self.set_state(state) + + def set_state(self, state): + self.state = state + color = STATES[self.state] + self.setStyleSheet('background-color: {}; border:none;'.format(color)) + + +class ManageTables(QGridLayout): + + def __init__(self, parent, tables, method): + super(ManageTables, self).__init__() + self.setHorizontalSpacing(1) + self.setVerticalSpacing(1) + columns = 6 + rows = int(len(tables) / columns) + 1 + self.buttons = {} + positions = [(i, j) for i in range(rows) for j in range(columns)] + for position, value in zip(positions, tables): + button = CallButton(value, method) + self.buttons[button.id] = button + self.addWidget(button, *position) + + def update_table(self, button_id, state): + button = self.buttons[button_id] + button.set_state(state) diff --git a/app/medium_screen.css b/app/medium_screen.css new file mode 100644 index 0000000..00184bd --- /dev/null +++ b/app/medium_screen.css @@ -0,0 +1,137 @@ + +QAbstractButton { + font-family: "DejaVu Sans"; + border-style: groove; + font: 20pt; + color: rgb(102, 102, 102); + background-color: rgb(242, 242, 242); + min-height: 45px; + min-width : 100px; + border-color: rgb(208, 208, 208); + border-width: 0.5px; +} + +#label_input, +#field_default, +#field_medium_gray, +#field_medium_blue, +#field_medium_orange { + background-color : white; + border-style : groove; + border-width : 0.5px; + border-color : rgb(208, 208, 208); +} + +#field_default { + font : 22px; +} + +#field_medium_gray, #label_medium_gray { + font : 22px; + color : rgb(54, 54, 54); +} + +#field_medium_blue, #label_medium_blue { + font : 22px; + color : rgb(0, 30, 80); +} + +#field_medium_orange, #label_medium_orange { + font : 22px; + color : rgb(235, 160, 15); +} + +#label_qty, #spin_box_qty, #row_field_price, +#label_price, #row_field_note, #label_fraction, #row_field_qty, +#field_fraction { + font : 22px; + alignment : center; +} + +#row_field_description { + font : 22px; + color : #242424; +} + +#label_product { + font : bold 24px; + alignment : center; + color : #242424; +} + +QSpinBox { + padding-right: 15px; + border-width: 3; + height: 40px; +} + +QSpinBox::up-button { + subcontrol-position: right; + height: 40px; + width: 40px; +} + +QSpinBox::down-button { + subcontrol-position: left; + height: 40px; + width: 40px; +} + +#field_invoice { + min-width : 120px; + max-width : 140px; +} + +#label_position, #label_salesman, #label_payment_term, + #label_party, #label_global_discount, #field_invoice, #label_invoice, + #label_agent, #label_date, #label_order_number, #label_default { + font : 20px; + color : rgb(150, 150, 150); + min-height : 10px; + min-width : 10px; +} + +#field_amount, #field_sign { + font : bold 42px; + color : rgb(43, 60, 77); + min-height : 40px; + max-height : 100px; +} + +#field_amount { + max-width : 260px; + min-width : 180px; +} + +#field_total_amount, #field_change, #label_total_amount, #label_change { + color : rgb(50, 65, 75); + min-width : 200px; +} + +#field_paid, #label_paid { + color : rgb(80, 190, 220); +} + +#table_payment { + color : rgb(70, 70, 70); + font : 14pt; + max-height : 80px; +} + +#table_sale_lines { + color : rgb(70, 70, 70); + font : 14pt; +} + +#label_paid, #label_change, #label_discount, #label_total_amount, +#label_discount, #label_change, #label_paid, #label_amount, #label_untaxed_amount, #label_taxes_amount, +#field_total_amount, #field_change, #field_discount, #field_paid, +#field_untaxed_amount, #field_taxes_amount { + min-height : 10px; + font: bold 20pt; +} + +#img_pixmap_pos { + min-width: 200pt; + min-height: 100pt; +} diff --git a/app/proxy.py b/app/proxy.py new file mode 100644 index 0000000..42017cd --- /dev/null +++ b/app/proxy.py @@ -0,0 +1,103 @@ + +import requests +from datetime import date +import simplejson as json + + +def encoder(obj): + # FIXME: add datetime, buffer, bytes + if isinstance(obj, date): + return { + '__class__': 'date', + 'year': obj.year, + 'month': obj.month, + 'day': obj.day, + } + raise TypeError(repr(obj) + " is not JSON serializable") + + +class FastModel(object): + + def __init__(self, model, ctx): + # params = parent.params + # self.model = model['name'] + self.model = model + # self.fields = model['fields'] + self.ctx = ctx + self.api = '/'.join( + ['http:/', ctx['params']['api_url'], ctx['params']['database']] + ) + + def __getattr__(self, name, *args): + 'Return attribute value' + self.method = name + return self + + def find(self, domain, order=None, limit=100, ctx=None): + if ctx: + self.ctx.update(ctx) + route = self.get_route('search') + args_ = { + 'model': self.model, + 'domain': domain, + 'order': order, + 'limit': limit, + 'context': self.ctx, + } + data = json.dumps(args_, default=encoder) + res = requests.get(route, data=data) + print('res ', res) + return res.json() + + def write(self, ids, values): + route = self.get_route('save') + args_ = { + 'model': self.model, + 'id': ids[0], + 'record_data': values, + 'context': self.ctx, + } + data = json.dumps(args_, default=encoder) + res = requests.put(route, data=data) + return res.json() + + def delete(self, ids): + route = self.get_route('delete') + args_ = { + 'model': self.model, + 'ids': ids, + 'context': self.ctx, + } + data = json.dumps(args_, default=encoder) + res = requests.delete(route, data=data) + return res.json() + + def get_route(self, target): + route = self.api + '/' + target + return route + + def __call__(self, args=None): + args_ = { + 'model': self.model, + 'method': self.method, + 'args': args, + 'context': self.ctx, + } + route = self.get_route('model_method') + data = json.dumps(args_, default=encoder) + res = requests.post(route, data=data) + data = res.json() + return data + + +if __name__ == "__main__": + params = {'api_url': 'localhost:5070', 'database': 'DEMORD'} + model = {'model': 'sale.sale'} + test_model = FastModel(params, model) + id = 180 + data = { + 'reference': 'OC-02874' + } + # res = test_model.find(dom) + res = test_model.write([id], data) + print(res) diff --git a/app/reporting.py b/app/reporting.py new file mode 100755 index 0000000..01273fe --- /dev/null +++ b/app/reporting.py @@ -0,0 +1,648 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +import os +from io import StringIO +import logging +from datetime import datetime +from decimal import Decimal + +pyudev = None + +try: + import pyudev +except: + logging.warning("Pyudev module not found!") + +try: + from escpos import printer +except: + logging.warning("Escpos module not found!") + +try: + import cups +except: + logging.warning("Cups module not found!") + +__all__ = ['Receipt'] + +_ROW_CHARACTERS = 48 + +_DIGITS = 9 +_PRINT_TAX_ID = False +_DIGITS_CODE_RECEIPT = 4 + +# ------------------- Type Font Escpos ----------------- +_FONT_A = 'a' # Normal Font +_FONT_B = 'b' # Condensed Font +# ------------------------------------------------------ + +if os.name == 'posix': + homex = 'HOME' + dirconfig = '.tryton/temp' +elif os.name == 'nt': + homex = 'USERPROFILE' + dirconfig = 'AppData/Local/tryton/temp' + +HOME_DIR = os.getenv(homex) +directory = os.path.join(HOME_DIR, dirconfig) + + +if not os.path.exists(directory): + os.makedirs(directory) + +TEMP_INVOICE_FILE = os.path.join(directory, 'invoice.txt') +SSH_PORT = 23 + + +def money(value): + if type(value) != int: + value = int(value) + return '{:,}'.format(value) + + +dev_printers = {} +if pyudev: + context = pyudev.Context() +# for device in context.list_devices(): +# if device.subsystem == 'usbmisc': +# print(device.subsystem, device.sys_path.split('2-1/')[1][0:5], device.device_node) +# dev_printers[str(device.sys_path.split('2-1/')[1][0:5])] = device.device_node + + +class Receipt(object): + __name__ = 'frontend_pos.ticket' + + def __init__(self, context, row_characters=None, logo=None, environment='retail'): + self.logger = logging.getLogger('reporting') + self._company = context.get('company') + self._sale_device = context.get('sale_device') + self._shop = context.get('shop') + self._street = context.get('street') + self._city = context.get('city') + self._phone = context.get('phone') + self._id_number = context.get('id_number') + self._regime_tax = context.get('regime_tax') + self._gta_info = context.get('gta_info') + self._user = context.get('user') + self._footer = context.get('footer') + self._header = context.get('header') + self._printing_taxes = context.get('printing_taxes') + self._delta_locale = context.get('delta_locale') + self._environment = environment + + self._row_characters = _ROW_CHARACTERS + if context.get('row_characters'): + self._row_characters = int(context.get('row_characters')) + + self.taxes_col_width = int(self._row_characters / 3) + + order_col_width = int(self._row_characters / 3) + self.order_col_1 = order_col_width - 6 + self.order_col_2 = order_col_width + 11 + self.order_col_3 = order_col_width - 5 + + self._show_position = context.get('show_position') + self._show_discount = context.get('show_discount') + self._img_logo = None + + if logo: + self._img_logo = StringIO(logo) + + def printer_found(self): + return self._printer + + def printing(f): + def p(self, *p, **kw): + self._open_device() + try: + res = f(self, *p, **kw) + finally: + pass + return res + return p + + def test_printer(self): + if self._interface == 'usb': + if os.name == 'posix': + self._printer = printer.File(self._device) + elif os.name == 'nt': + self._printer = printer.UsbWin(self._device) + self._printer.open() + elif self._interface == 'network': + self._printer = printer.Network(self._device) + elif self._interface == 'ssh': + self._printer = printer.FileSSH(*self._device.split('@')) + self._printer.open() + if not self._printer: + return + if self._img_logo: + self.print_logo() + self.print_enter() + self.print_enter() + self.print_header() + self.print_enter() + self.print_enter() + self._printer.cut() + self._printer.cashdraw(2) + self._printer.close() + + def set_printer(self, printer): + if dev_printers.get(printer['device']): + device = dev_printers[printer['device']] + else: + device = printer['device'] + + self._interface = printer['interface'] + self._device = device + + def print_sale(self, sale): + try: + if self._interface == 'usb': + if os.name == 'posix': + self._printer = printer.File(self._device) + elif os.name == 'nt': + self._printer = printer.UsbWin(self._device) + self._printer.open() + elif self._interface == 'network': + self._printer = printer.Network(self._device) + elif self._interface == 'ssh': + self._printer = printer.FileSSH(*self._device.split('@')) + self._printer.open() + elif self._interface == 'cups': + self.conn = cups.Connection() + self._file = open(TEMP_INVOICE_FILE, 'w') + self._printer = CupsPrinter(self._file, self._row_characters) + if not self._printer: + self.logger.info("Warning: Can not found Printer!") + return + self.logger.info("Info: Printer is OK!") + self._print_sale(sale) + except: + self.logger.info("Warning: Printer error or device not found!") + + def _print_sale(self, sale): + self.print_header() + self.print_body(sale) + self.print_footer() + # self.print_extra_info(sale) + if self._interface in ['usb', 'ssh', 'network']: + self._printer.close() + elif self._interface == 'cups': + self._file.close() + self.conn.printFile(self._printer_name, TEMP_INVOICE_FILE, + 'POS Invoice', {}) + + def print_logo(self): + self._printer.set(align='center') + self._printer.image(self._img_logo) + self.print_enter() + + def print_header(self): + if self._img_logo: + self.print_logo() + self._printer.set(align='center') + if self._header != '' and self._header is not None: + self._printer.text(self._header) + self.print_enter() + self._printer.text(self._company) + self.print_enter() + self._printer.text(self._shop) + self.print_enter() + if self._id_number: + self._printer.text('NIT:' + self._id_number) + if self._regime_tax: + self._printer.text(' ' + self._regime_tax) + self.print_enter() + if self._street: + self._printer.text(self._street) + self.print_enter() + if self._city: + self._printer.text(self._city) + if self._phone: + if self._city: + self._printer.text(' ') + self._printer.text('Telefono:' + self._phone) + if self._city or self._phone: + self.print_enter() + self.print_enter() + self.print_enter() + + def print_horinzontal_line(self): + self._printer.text('-' * self._row_characters) + + def print_horinzontal_double_line(self): + self._printer.text('=' * self._row_characters) + + def print_enter(self): + self._printer.text('\n') + + def print_split(self, left, right): + len_left = self._row_characters - len(right) - 1 + left = left[:len_left] + if type(left) == bytes: + left = left.decode("utf-8") + if type(right) == bytes: + right = right.decode("utf-8") + left += (len_left - len(left) + 1) * ' ' + self._printer.text(left) + self._printer.text(right + '\n') + + def print_body(self, sale): + self._cashdraw = True + self._printer.set(font=_FONT_B) + self._printer.set(align='left') + if sale['number'] and sale['state'] in ['processing', 'done', 'cancel']: + if sale['total_amount'] >= 0: + self._printer.text('FACTURA DE VENTA No. ' + sale['number']) + else: + self._printer.text('NOTA CREDITO No. ' + sale['number']) + else: + self._cashdraw = False + self._printer.text('Pedido: ' + sale['order']) + self.print_enter() + #mod_hours = sale["create_date"] + timedelta(hours=self._delta_locale) + #time_ = mod_hours.strftime('%I:%M %p') + self._printer.text('Fecha:%s' % sale['date']) + if sale.get('turn') and sale['turn'] != 0: + self._printer.text('Turno: %s - ' % str(sale['turn'])) + self.print_enter() + self.print_horinzontal_line() + party_name = 'Cliente: %s ' % sale['party'] + party_id_number = 'Id: %s' % sale.get('party_id_number', '') + if len(party_name + party_id_number) > self._row_characters: + self._printer.text(party_name) + self.print_enter() + self._printer.text(party_id_number) + else: + self._printer.text(party_name + party_id_number) + if sale.get('party_address'): + self.print_enter() + self._printer.text('Direccion: %s' % sale['party_address']) + if sale.get('party_phone'): + self.print_enter() + self._printer.text('Telefono: %s' % sale['party_phone']) + + self.print_enter() + self.print_horinzontal_line() + self.print_split(' Articulo ', 'Subtotal ') + self.print_horinzontal_line() + + len_row = self._row_characters - (_DIGITS_CODE_RECEIPT + 1) - (_DIGITS + 1) + for line in sale['lines']: + if line['taxes'] and _PRINT_TAX_ID: + tax_id = ' ' + str(line['taxes'][0].id) + else: + tax_id = '' + line_total = money(line['amount_w_tax']) + tax_id + + if line['quantity'] != 1: + length_name = self._row_characters - 11 + first_line = line['code'] + ' ' + line['name'][:length_name] + + if type(first_line) == bytes: + first_line = first_line.decode('utf-8') + + self._printer.text(first_line + '\n') + + unit_price_w_tax = str(round(line['unit_price_w_tax'], 2)) + + second_line = ' %s x %s' % (line['quantity'], unit_price_w_tax) + second_line = second_line.encode('utf-8') + self.print_split(second_line, line_total) + else: + if self._environment == 'retail': + line_pt = line['code'] + ' ' + line['name'][:len_row] + else: + line_pt = line['name'][:len_row] + + self.print_split(line_pt, line_total) + + untaxed_amount = sale['untaxed_amount'] + total_amount = sale['total_amount'] + total_string = 'Total:' + + tip = None + if sale.get('tip') and sale['tip'] > 0: + untaxed_amount = untaxed_amount - sale['tip'] + total_amount = untaxed_amount + sale['tax_amount'] + tip = sale['tip'] + total_string = 'Total Sin Propina:' + + self.print_split('', '----------------') + self.print_split('Subtotal Base:', money(untaxed_amount)) + self.print_split('Impuesto:', money(sale['tax_amount'])) + self.print_split('', '----------------') + self.print_split(total_string, money(total_amount)) + self.print_enter() + + if tip: + self.print_split('Propina:', money(tip)) + self.print_split('Total con Propina:', money(sale['total_amount'])) + + if self._show_discount: + self.print_split('Descuento:', money(sale['discount'])) + self.print_enter() + if sale['cash_received']: + self.print_split('Recibido:', money(sale['cash_received'])) + else: + self.print_split('Recibido:', money(sale['paid_amount'])) + self.print_split('Saldo Pendiente:', money(sale['total_amount'] - sale['paid_amount'])) + self.print_split('Cambio:', money(sale['change'])) + self.print_horinzontal_line() + self.print_enter() + if self._printing_taxes: + self.print_col('Tipo', self.taxes_col_width + 2) + self.print_col('Base', self.taxes_col_width) + self.print_col('Imp.', self.taxes_col_width) + taxes = sale['taxes'] + for tax in taxes: + self.print_col(str(taxes[tax]['name']) + ' ', self.taxes_col_width) + self.print_col(str(int(taxes[tax]['base'])), self.taxes_col_width) + self.print_col(str(int(taxes[tax]['tax'])), self.taxes_col_width) + self.print_enter() + + self.print_horinzontal_line() + self.print_enter() + no_products = 'No Items: %s' % str(sale['num_products']) + self._printer.text(no_products) + self.print_enter() + + if self._gta_info and sale['state'] not in ['draft']: + self._printer.text(self._gta_info) + self.print_enter() + if sale['state'] in ['processing', 'done']: + self._printer.text('Pedido: ' + sale['order']) + self.print_enter() + + register = 'Caja No. %s' % self._sale_device + self._printer.text(register) + self.print_enter() + + self._printer.text('Cajero: %s' % self._user) + self.print_enter() + if sale.get('salesman'): + self._printer.text('Vendedor: %s' % sale['salesman']) + self.print_enter() + if sale.get('comment'): + self._printer.text('Notas: %s' % sale['comment']) + self.print_enter() + + if self._show_position: + self._printer.text('Posicion: %s' % str(sale['position'])) + self.print_enter() + + self._printer.set(align='center') + #self.print_split('Puntos Acumulados:', sale['points']) + #self.print_enter() + #printer.barcode(sale.receipt_code, 'CODE128B', 3, 50,'','') + self.print_enter() + self.print_enter() + + def print_extra_info(self, sale): + if sale.get('pos_notes'): + self.print_enter() + self.print_header() + self.print_horinzontal_line() + self.print_enter() + party_name = 'Cliente: %s ' % sale['party'] + self._printer.text(party_name) + self.print_enter() + if self._show_position: + self._printer.text('Posicion: %s' % str(sale['position'])) + self.print_enter() + + if sale['state'] in ['draft']: + self._printer.text('Cotizacion: ', sale['order']) + else: + self._printer.text('Factura No. ' + sale['number']) + self.print_enter() + self._printer.text(str(sale.get('pos_notes'))) + self.print_enter() + self.print_horinzontal_line() + self.print_enter() + self._printer.cut() + + def print_col(self, x, l): + self._printer.text(x[:l] + (l - len(x)) * ' ') + + def print_footer(self, ): + if self._footer: + self._printer.text(self._footer) + self.print_enter() + self._printer.text('SOFTWARE POS TRYTON - www.presik.com') + self.print_enter() + self._printer.cut() + if self._cashdraw: + self._printer.cashdraw(2) + self.print_enter() + + def print_orders(self, orders, reversion=None, kind='command'): + res = [] + self.order_kind = kind + for order in orders.values(): + try: + if dev_printers.get(order['host']): + host = dev_printers[order['host']] + else: + host = order['host'] + if order['interface'] == 'usb': + self._printer = printer.File(host) + elif order['interface'] == 'network': + self._printer = printer.Network(host) + elif order['interface'] == 'ssh': + self._printer = printer.FileSSH(*host.split('@')) + if self._printer: + self._printer.open() + elif order['interface'] == 'cups': + pass + if not self._printer: + self.logger.info("Warning: Interface not found for printer!") + res.append(None) + continue + + self.logger.info("Info: Printer is OK!") + res.append(self._print_order(order, reversion)) + except: + self.logger.info("Warning: Can not found Printer!") + res.append(None) + return all(res) + + def _print_order(self, order, reversion): + self.print_body_order(order, reversion) + self._printer.cut() + self._row_characters = order['row_characters'] + if order['interface'] in ('network', 'usb', 'ssh'): + self._printer.close() + return True + + def print_body_order(self, order, reversion): + self._printer.set(font=_FONT_B) + self._printer.set(align='center') + self._printer.text('TURNO: %s' % str(order['turn'])) + self.print_enter() + self.print_enter() + kind = 'COMANDA' + if self.order_kind == 'delivery': + kind = 'PEDIDO' + title = '+ + + + + %s + + + + +' % kind + self._printer.text(title) + self.print_enter() + self._printer.set(align='left') + self.print_enter() + date_ = datetime.now().strftime("%Y-%m-%d %H:%M %p") + self._printer.text('FECHA: ' + date_) + self.print_enter() + + if self.order_kind == 'delivery': + self._printer.text('FACTURA: ' + order['number']) + self.print_enter() + delivery_charge = 'Cliente' + if order['delivery_charge'] == 'company': + delivery_charge = 'Empresa' + self._printer.text('CARGO DEL DOMICILIO: ' + delivery_charge) + self.print_enter() + if order.get('payment_term'): + self._printer.text('FORMA DE PAGO: ' + order['payment_term']) + self.print_enter() + + if order.get('sale_number'): + self._printer.text('PEDIDO: %s' % str(order['sale_number'])) + self.print_enter() + + self._printer.text('POSICION: %s' % str(order['position'])) + self.print_enter() + self._printer.text('VENDEDOR: %s' % order['salesman']) + self.print_enter() + self._printer.text('AMBIENTE: %s' % order['shop']) + self.print_enter() + self._printer.text('CLIENTE: %s' % order['party']) + self.print_enter() + + if self.order_kind == 'delivery': + self._printer.text('VALOR: ' + str(order['total_amount'])) + self.print_enter() + if order.get('pos_notes'): + self._printer.text(order['pos_notes']) + self.print_enter() + self._printer.text('CAJA No: %s' % self._sale_device or '') + self.print_enter() + self.print_enter() + self._printer.set(align='center') + self.print_horinzontal_line() + if not reversion and self.order_kind == 'command': + self._printer.text('-------- PREPARAR Y SERVIR --------') + elif not reversion and self.order_kind == 'delivery': + self._printer.text('-------- ENTREGAR --------') + else: + self._printer.text('<<<< R E V E R S I O N >>>>') + self.print_enter() + + if self.order_kind != 'delivery': + self.print_horinzontal_line() + self._printer.set(align='left') + self.print_enter() + self._printer.set(align='left') + + self.print_enter() + self.print_horinzontal_line() + self.print_col('CANT', self.order_col_1) + self.print_col('PRODUCTO', self.order_col_2) + self.print_col(' ' + 'PRECIO', self.order_col_3) + + for line in order['lines']: + qty = str(int(Decimal(line['quantity']))) + self.print_col(qty, self.order_col_1) + self.print_col(line['name'], self.order_col_2) + self.print_col(' ' + str(line['unit_price']), self.order_col_3) + if line['note']: + self.print_enter() + self._printer.text(' ----->> NOTA: ' + line['note']) + self.print_enter() + + self.print_enter() + self.print_horinzontal_double_line() + + self.print_enter() + self._printer.text('NOTA:') + self.print_enter() + if order['comment']: + self._printer.text(str(order['comment'])) + self.print_enter() + self.print_horinzontal_line() + self.print_enter() + self.print_enter() + + +class CupsPrinter(object): + "Cups Printer" + __name__ = 'sale_pos_frontend.cups_printer' + + def __init__(self, _file, row_characters): + self._file = _file + self.align = 'left' + self._row_characters = row_characters + + def text(self, text): + self._text(text) + + def set(self, align='left', font=_FONT_A): + if align: + self.align = align + if font: + self.font = font + + def cut(self): + pass + + def cashdraw(number): + pass + + def _text(self, text): + start_spaces = '' + if self.align == 'center': + start_spaces = int((self._row_characters - len(text)) / 2) * ' ' + elif self.align == 'right': + start_spaces = int(self._row_characters - len(text)) * ' ' + else: + pass + text = start_spaces + text + self._file.write(text) + + +if __name__ == '__main__': + + # Test for Escpos interface printer Linux + + # Network example + device = 'network', '192.168.0.32' + + # Unix-like Usb example + # device = 'usb','/dev/usb/lp1' + + # Windows Usb example for printer nameb SATPOS + # device = 'usb', 'SATPOS' + + # SSH example + # device = 'ssh', 'psk@xxxxx@192.168.0.5@23@/dev/usb/lp1' + + example_dev = { + 'interface': device[0], + 'device': device[1], + } + + ctx_printing = {} + ctx_printing['company'] = 'OSCORP INC' + ctx_printing['sale_device'] = 'CAJA-10' + ctx_printing['shop'] = 'Shop Wall Boulevard' + ctx_printing['street'] = 'Cll 21 # 172-81. Central Park' + ctx_printing['user'] = 'Charles Chapplin' + ctx_printing['city'] = 'Dallas' + ctx_printing['zip'] = '0876' + ctx_printing['phone'] = '591 5513 455' + ctx_printing['id_number'] = '123456789-0' + ctx_printing['tax_regime'] = 'none' + + receipt = Receipt(ctx_printing) + receipt.set_printer(example_dev) + receipt.test_printer() diff --git a/app/share/accept.svg b/app/share/accept.svg new file mode 100644 index 0000000..7edc1f1 --- /dev/null +++ b/app/share/accept.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/share/beer.svg b/app/share/beer.svg new file mode 100644 index 0000000..8679548 --- /dev/null +++ b/app/share/beer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/breakfast.svg b/app/share/breakfast.svg new file mode 100644 index 0000000..613b05e --- /dev/null +++ b/app/share/breakfast.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/burger.svg b/app/share/burger.svg new file mode 100644 index 0000000..f406764 --- /dev/null +++ b/app/share/burger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/calendar.svg b/app/share/calendar.svg new file mode 100644 index 0000000..e67e2df --- /dev/null +++ b/app/share/calendar.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/share/cancel.svg b/app/share/cancel.svg new file mode 100644 index 0000000..91ecc55 --- /dev/null +++ b/app/share/cancel.svg @@ -0,0 +1,97 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/app/share/cash.svg b/app/share/cash.svg new file mode 100644 index 0000000..6ee4dba --- /dev/null +++ b/app/share/cash.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/share/cheese.svg b/app/share/cheese.svg new file mode 100644 index 0000000..d6cd000 --- /dev/null +++ b/app/share/cheese.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/chicken-leg.svg b/app/share/chicken-leg.svg new file mode 100644 index 0000000..76d4694 --- /dev/null +++ b/app/share/chicken-leg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/chinese-food.svg b/app/share/chinese-food.svg new file mode 100644 index 0000000..b7ec558 --- /dev/null +++ b/app/share/chinese-food.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/coffee.svg b/app/share/coffee.svg new file mode 100644 index 0000000..0ee7ab2 --- /dev/null +++ b/app/share/coffee.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/comment.svg b/app/share/comment.svg new file mode 100644 index 0000000..dc0c0a0 --- /dev/null +++ b/app/share/comment.svg @@ -0,0 +1,85 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/app/share/croissant.svg b/app/share/croissant.svg new file mode 100644 index 0000000..370900d --- /dev/null +++ b/app/share/croissant.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/delete_line.svg b/app/share/delete_line.svg new file mode 100644 index 0000000..1f1869c --- /dev/null +++ b/app/share/delete_line.svg @@ -0,0 +1,89 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/app/share/donut.svg b/app/share/donut.svg new file mode 100644 index 0000000..afc6051 --- /dev/null +++ b/app/share/donut.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/draft.svg b/app/share/draft.svg new file mode 100644 index 0000000..77877b3 --- /dev/null +++ b/app/share/draft.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/share/fork.svg b/app/share/fork.svg new file mode 100644 index 0000000..cdfbae0 --- /dev/null +++ b/app/share/fork.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/global_discount.svg b/app/share/global_discount.svg new file mode 100644 index 0000000..6a32645 --- /dev/null +++ b/app/share/global_discount.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/hot-dog.svg b/app/share/hot-dog.svg new file mode 100644 index 0000000..d391e2e --- /dev/null +++ b/app/share/hot-dog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/ice-cream.svg b/app/share/ice-cream.svg new file mode 100644 index 0000000..0466ffb --- /dev/null +++ b/app/share/ice-cream.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/kebab.svg b/app/share/kebab.svg new file mode 100644 index 0000000..dcaf9ed --- /dev/null +++ b/app/share/kebab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/menu.svg b/app/share/menu.svg new file mode 100644 index 0000000..f971e76 --- /dev/null +++ b/app/share/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/menu_section.svg b/app/share/menu_section.svg new file mode 100644 index 0000000..5f675ed --- /dev/null +++ b/app/share/menu_section.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + Menu + + + + + + + + + + + + + + + + + + + + + + + + + + Menu + + diff --git a/app/share/milkshake.svg b/app/share/milkshake.svg new file mode 100644 index 0000000..a967b0c --- /dev/null +++ b/app/share/milkshake.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/new_sale.svg b/app/share/new_sale.svg new file mode 100644 index 0000000..043b878 --- /dev/null +++ b/app/share/new_sale.svg @@ -0,0 +1,81 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/app/share/party.svg b/app/share/party.svg new file mode 100644 index 0000000..c106eeb --- /dev/null +++ b/app/share/party.svg @@ -0,0 +1,77 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/app/share/payment.svg b/app/share/payment.svg new file mode 100644 index 0000000..9bb0b7e --- /dev/null +++ b/app/share/payment.svg @@ -0,0 +1,91 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/app/share/payment_term.svg b/app/share/payment_term.svg new file mode 100644 index 0000000..b763967 --- /dev/null +++ b/app/share/payment_term.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/pizza-slice.svg b/app/share/pizza-slice.svg new file mode 100644 index 0000000..c6ad6db --- /dev/null +++ b/app/share/pizza-slice.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/plus.svg b/app/share/plus.svg new file mode 100644 index 0000000..e249e58 --- /dev/null +++ b/app/share/plus.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/share/pos-icon.ico b/app/share/pos-icon.ico new file mode 100644 index 0000000..ab05f7a Binary files /dev/null and b/app/share/pos-icon.ico differ diff --git a/app/share/pos-icon.svg b/app/share/pos-icon.svg new file mode 100644 index 0000000..8a60771 --- /dev/null +++ b/app/share/pos-icon.svg @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/app/share/pos_banner.png b/app/share/pos_banner.png new file mode 100644 index 0000000..4ee72f6 Binary files /dev/null and b/app/share/pos_banner.png differ diff --git a/app/share/position.svg b/app/share/position.svg new file mode 100644 index 0000000..b73a4ab --- /dev/null +++ b/app/share/position.svg @@ -0,0 +1,81 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/app/share/print_order.svg b/app/share/print_order.svg new file mode 100644 index 0000000..4a222de --- /dev/null +++ b/app/share/print_order.svg @@ -0,0 +1,77 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/app/share/print_sale.svg b/app/share/print_sale.svg new file mode 100644 index 0000000..5438f2d --- /dev/null +++ b/app/share/print_sale.svg @@ -0,0 +1,89 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/app/share/reservations.svg b/app/share/reservations.svg new file mode 100644 index 0000000..997fcf9 --- /dev/null +++ b/app/share/reservations.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/share/salesman.svg b/app/share/salesman.svg new file mode 100644 index 0000000..30aeaf5 --- /dev/null +++ b/app/share/salesman.svg @@ -0,0 +1,78 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/app/share/search_product.svg b/app/share/search_product.svg new file mode 100644 index 0000000..8e750fc --- /dev/null +++ b/app/share/search_product.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/app/share/search_sale.svg b/app/share/search_sale.svg new file mode 100644 index 0000000..383603b --- /dev/null +++ b/app/share/search_sale.svg @@ -0,0 +1,81 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/app/share/table.svg b/app/share/table.svg new file mode 100644 index 0000000..5354cfe --- /dev/null +++ b/app/share/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/tables.svg b/app/share/tables.svg new file mode 100644 index 0000000..4605144 --- /dev/null +++ b/app/share/tables.svg @@ -0,0 +1,152 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/share/taco.svg b/app/share/taco.svg new file mode 100644 index 0000000..7838eaa --- /dev/null +++ b/app/share/taco.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/tea.svg b/app/share/tea.svg new file mode 100644 index 0000000..5e5e7c6 --- /dev/null +++ b/app/share/tea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/tip.svg b/app/share/tip.svg new file mode 100644 index 0000000..2651fe0 --- /dev/null +++ b/app/share/tip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/waffle.svg b/app/share/waffle.svg new file mode 100644 index 0000000..35953f3 --- /dev/null +++ b/app/share/waffle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/share/waiter.svg b/app/share/waiter.svg new file mode 100644 index 0000000..82235f3 --- /dev/null +++ b/app/share/waiter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/small_screen.css b/app/small_screen.css new file mode 100644 index 0000000..6ed94b1 --- /dev/null +++ b/app/small_screen.css @@ -0,0 +1,164 @@ + +QAbstractButton { + font-family: "DejaVu Sans"; + border-style: groove; + font: 12pt; + color: rgb(102, 102, 102); + background-color: rgb(242, 242, 242); + min-height: 50px; + min-width : 110px; + border-color: rgb(208, 208, 208); + border-width: 0px; +} + +#field_total_amount, #field_amount, #label_input, + #field_default, + #field_small_blue, + #field_small_gray, + #field_invoice { + background-color : white; + border-style : groove; + border-width : 0.5px; + border-color : rgb(208, 208, 208); +} + +#label_default { + font : 14pt; + color : rgb(102, 102, 102); + min-height : 10px; + min-width : 10px; +} + +#field_default { + font: bold 14pt; + min-height : 10px; +} + +#field_small_gray, #label_small_gray { + font : 16px; + color : rgb(54, 54, 54); +} + +#field_medium_gray, #label_medium_gray { + font : 24px; + color : rgb(54, 54, 54); +} + +#field_big_gray, #label_big_gray { + font : 32px; + color : rgb(54, 54, 54); +} + +#field_small_blue, #label_small_blue { + font : 16px; + color : rgb(0, 30, 80); +} + +#field_medium_blue, #label_medium_blue { + font : 24px; + color : rgb(0, 30, 80); +} + +#field_big_blue, #label_big_blue { + font : bold 32px; + color : rgb(0, 30, 80); +} + +#field_small_orange, #label_small_orange { + font : 16px; + color : rgb(235, 160, 15); +} + +#field_medium_orange, #label_medium_orange { + font : 24px; + color : rgb(235, 160, 15); +} + +#field_big_orange, #label_big_orange { + font : bold 32px; + color : rgb(235, 160, 15); +} + +QDialog { + min-height : 300px; + max-height : 400px; + min-width : 500px; +} + +#label_gray { + font: bold 14pt; + min-height : 10px; + min-width : 10px; + color: rgb(102, 102, 102); +} + +#label_blue { + font: bold 14pt; + color: rgb(17, 84, 102); + min-height : 10px; + min-width : 10px; +} + +#label_message { + font : 14pt; + min-height : 45px; + min-width : 10px; +} + +#field_invoice { + font : 15pt; + min-height : 45px; + min-width : 110px; +} + +#label_input { + font : 15pt; + min-height : 45px; + min-width : 200px; +} + +#field_sign { + font: bold 26pt; + min-height : 70px; + max-width: 100px; +} + +#field_amount { + font: 24pt; + min-height : 60px; + max-width: 310px; +} + +#field_total_amount, #label_total_amount { + color : rgb(50, 65, 75); +} + +#table_sale_lines { + color : rgb(70, 70, 70); + font : 11pt; + max-height: 380px; +} + +#label_product, #label_qty, #spin_box_qty, #row_field_price, + #label_price, #row_field_note { + font : 24px; + alignment : center; +} + +QSpinBox { + padding-right: 15px; + border-width: 3; + height: 40px; +} + +QSpinBox::up-button { + subcontrol-position: right; + height: 38px; + width: 38px; +} + +QSpinBox::down-button { + subcontrol-position: left; + height: 38px; + width: 38px; +} diff --git a/app/states.py b/app/states.py new file mode 100644 index 0000000..ea8b57f --- /dev/null +++ b/app/states.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +from re import compile + +# States of mainwindow +STATES = { + 'add': { + 'button': 'button_accept', + 're': compile(r'^(\*[0-9]*|[0-9]+)$|-|/|\*[0-9]*[.]*'), + }, + 'accept': { + 'button': 'button_cash', + 're': compile(r'^[0-9]+$'), + }, + 'cash': { + 'button': None, + 're': compile(r'^[0-9]+(,[0-9]{,2})?$') + }, + 'paid': { + 'button': None, + 're': compile(r'^(\*[0-9]*|[0-9]+)$'), + }, + 'cancel': { + 'button': None, + 're': compile(r'^(\*[0-9]*|[0-9]+)$'), + }, + 'disabled': { + 'button': None, + 're': compile(r'^(\*[0-9]*|[0-9]+)$'), + }, +} + +RE_SIGN = { + 'quantity': compile(r'\d+|\.\d+|\d+\.'), +} diff --git a/app/translations/i18n_es.qm b/app/translations/i18n_es.qm new file mode 100644 index 0000000..420adf2 Binary files /dev/null and b/app/translations/i18n_es.qm differ diff --git a/app/translations/i18n_es.ts b/app/translations/i18n_es.ts new file mode 100644 index 0000000..b4991a6 --- /dev/null +++ b/app/translations/i18n_es.ts @@ -0,0 +1,823 @@ + + + + + ButtonsFunction + + + SEARCH + BUSCAR + + + + CUSTOMER + CLIENTE + + + + CANCEL + CANCELAR + + + + PRINT + IMPRIMIR + + + + SALESMAN + VENDEDOR + + + + GLOBAL DISCOUNT + DESCUENTO GLOBAL + + + + ORDER + ENV. ORDEN + + + + NEW SALE + NUEVA VENTA + + + + PAY MODE + MEDIO DE PAGO + + + + PAY TERM + PLAZO DE PAGO + + + + POSITION + POSICION + + + + NOTE + NOTA + + + + TIP + PROPINA + + + + TABLES + MESAS + + + + RESERVATIONS + RESERVACIONES + + + + S. SALE + B. VENTA + + + + WAITER + MESERO + + + + MainWindow + + + SYSTEM READY... + SISTEMA LISTO... + + + + DO YOU WANT TO EXIT? + DESEA SALIR? + + + + PLEASE CONFIRM YOUR PAYMENT TERM AS CREDIT? + POR FAVOR CONFIRMAR SI SU PLAZO DE PAGO ES CREDITO? + + + + SALE ORDER / INVOICE NUMBER NOT FOUND! + ORDER / FACTURA DE VENTA NO ENCONTRADA! + + + + THIS SALE IS CLOSED, YOU CAN NOT TO MODIFY! + ESTA VENTA ESTA CERRADA, Y USTED NO PUEDE MODIFICARLA! + + + + DISCOUNT VALUE IS NOT VALID! + EL DESCUENTO NO ES VALIDO! + + + + YOU CAN NOT ADD PAYMENTS TO SALE ON DRAFT STATE! + NO PUEDE AGREGAR PAGOS A UNA VENTA EN BORRADOR! + + + + ENTER QUANTITY... + INGRESE LA CANTIDAD... + + + + ENTER DISCOUNT... + INGRESE EL DESCUENTO... + + + + ENTER PAYMENT AMOUNT BY: %s + INGRESE EL VALOR DEL PAGO EN: %s + + + + ENTER NEW PRICE... + INGRESE EL NUEVO PRECIO... + + + + ORDER SUCCESUFULLY SENT. + ORDEN ENVIADA EXITOSAMENTE. + + + + FAILED SEND ORDER! + FALLO EL ENVIO DE LA ORDEN! + + + + MISSING AGENT! + FALTA EL AGENTE! + + + + THERE IS NOT SALESMAN FOR THE SALE! + NO SE DEFINIDO EL VENDEDOR EN LA VENTA! + + + + YOU CAN NOT CONFIRM A SALE WITHOUT PRODUCTS! + NO PUEDE CONFIRMAR UNA VENTA SIN PRODUCTOS! + + + + USER WITHOUT PERMISSION FOR SALE POS! + USUARIO SIN PERMISOS PARA VENTA POS! + + + + THE QUANTITY IS NOT VALID...! + LA CANTIDAD NO ES VALIDAD...! + + + + MISSING THE DEFAULT PARTY ON SHOP CONFIGURATION! + FALTA CONFIGURAR EL TERCERO EN LA TIENDA! + + + + MISSING SET THE JOURNAL ON DEVICE! + FALTA EL ESTADO DE CUENTA PARA LA CAJA! + + + + PRODUCT NOT FOUND! + PRODUCTO NO ENCONTRADO! + + + + DO YOU WANT CREATE NEW SALE? + DESEA CREAR UNA NUEVA VENTA? + + + + ARE YOU WANT TO CANCEL SALE? + DESEA CANCELAR LA VENTA? + + + + AGENT NOT FOUND! + AGENTE NO ENCONTRADO! + + + + COMMISSION NOT VALID! + LA COMISIÓN NO ES VÁLIDA! + + + + CREDIT LIMIT FOR CUSTOMER EXCEED! + EL CLIENTE SUPERA SU CUPO DE CREDITO! + + + + THE CUSTOMER CREDIT CAPACITY IS ABOVE 80% + EL CUPO DE CREDITO DEL CLIENTE ESTA SOBRE EL 80% + + + + YOU CAN NOT FORCE ASSIGN! + NO PUEDE FORZAR UNA ASIGACIÓN! + + + + INVOICE: + FACTURA: + + + + INVOICE + FACTURA + + + + PARTY + CLIENTE + + + + DATE + FECHA + + + + SALESMAN + VENDEDOR + + + + PAYMENT TERM + PLAZO DE PAGO + + + + No ORDER + No PEDIDO + + + + POSITION + POSICION + + + + AGENT + AGENTE + + + + DELIVERY CHARGE + CARGO DOMICILIO + + + + SUBTOTAL + SUBTOTAL + + + + TAXES + IMPUESTOS + + + + DISCOUNT + DESCUENTO + + + + TOTAL + TOTAL + + + + PAID + PAGADO + + + + CHANGE + CAMBIO + + + + SHOP + TIENDA + + + + DEVICE + CAJA + + + + DATABASE + BD + + + + USER + USUARIO + + + + PRINTER + IMPRESORA + + + + ID + ID + + + + NUMBER + NUMERO + + + + TOTAL AMOUNT + VALOR TOTAL + + + + SEARCH SALES... + BUSCAR VENTAS... + + + + CODE + CÓDIGO + + + + STOCK + INVENTARIO + + + + NAME + NOMBRE + + + + DESCRIPTION + DESCRIPCIÓN + + + + BRAND + MARCA + + + + PRICE + PRECIO + + + + LOCATION + LOCACIÓN + + + + IMAGE + IMAGEN + + + + ID NUMBER + NUMERO ID + + + + PHONE + TELÉFONO + + + + PAYMENT MODE: + MEDIO DE PAGO: + + + + SELECT PAYMENT MODE: + SELECCIONE EL MEDIO DE PAGO: + + + + WAREHOUSE + BODEGA + + + + QUANTITY + CANTIDAD + + + + STOCK BY PRODUCT: + INVENTARIO POR PRODUCTO: + + + + Id + Id + + + + Salesman + Vendedor + + + + CHOOSE SALESMAN + ESCOGE EL VENDEDOR + + + + CHOOSE TAX + ESCOJA EL IMPUESTO + + + + SELECT PAYMENT TERM + SELECCIONE EL MODO DE PAGO + + + + INVOICE NUMBER + NUMERO DE FACTURA + + + + TYPE + TIPO + + + + ORDER + PEDIDO + + + + INSERT PASSWORD FOR CANCEL + INGRESE LA CONTRASEÑA PARA CANCELAR + + + + GLOBAL DISCOUNT + DESCUENTO GLOBAL + + + + PASSWORD FORCE ASSIGN + CONTRASEÑA PARA FORZAR ASIGNACIÓN + + + + VOUCHER NUMBER + NÚMERO DE VOUCHER + + + + COMMISSION + COMISIÓN + + + + AMOUNT + VALOR + + + + COMMENTS + COMENTARIOS + + + + QUANTITY: + CANTIDAD: + + + + UNIT PRICE: + PRECIO UNITARIO: + + + + COD + COD + + + + UNIT + UND + + + + QTY + CANT + + + + DISC + DESC + + + + NOTE + NOTA + + + + UNIT PRICE W TAX + PRECIO UNIT CON IMP + + + + STATEMENT JOURNAL + ESTADO DE CUENTA + + + + THE USER HAVE NOT PERMISSIONS FOR ACCESS TO DEVICE! + EL USUARIO NO TIENE PERMISOS PARA ACCEDER A CAJA! + + + + THERE IS NOT A STATEMENT OPEN FOR THIS DEVICE! + NO HAY ESTADO DE CUENTA ABIERTOS POR ESTA CAJA! + + + + YOU HAVE NOT PERMISSIONS FOR DELETE THIS SALE! + NO TIENE PERMISOS PARA BORRAR ESTA VENTA! + + + + YOU HAVE NOT PERMISSIONS FOR CANCEL THIS SALE! + NO TIENE PERMISOS PARA CANCELAR LA VENTA! + + + + THE CUSTOMER HAS NOT CREDIT! + EL CLIENTE NO TIENE CREDITO! + + + + CUSTOMER + CLIENTE + + + + COMPANY + COMPAÑIA + + + + ADDRESS + DIRECCION + + + + SEARCH CUSTOMER + BUSCAR CLIENTE + + + + ASSIGNED TABLE + MESA ASIGNADA + + + + FIRST YOU MUST CREATE/LOAD A SALE! + PRIMERO DEBE AGREGAR/CARGAR UNA VENTA! + + + + FRACTION: + FRACCIÓN: + + + + FRAC + FRAC + + + + DO YOU WANT TO CONFIRM THE SEND ORDER? + DESEAS CONFIRMAR EL ENVIO DE LA ORDEN? + + + + ActionButton + + + &ACCEPT + &ACEPTAR + + + + &CANCEL + &CANCELAR + + + + FrontWindow + + + APPLICATION + APLICACION + + + + HelpDialog + + + Keys Shortcuts... + Atajos de Teclado... + + + + Action + Acción + + + + Shortcut + Atajo + + + + Login + + + HOST + SERVIDOR + + + + DATABASE + BASE DE DATOS + + + + USER + USUARIO + + + + PASSWORD + CONTRASEÑA + + + + C&ANCEL + C&ANCELAR + + + + &CONNECT + &CONECTAR + + + + Error: username or password invalid...! + Error: nombre de usuario o contraseña inválido! + + + + MenuButtons + + + Menu... + Menu... + + + + &ACCEPT + &ACCEPT + + + + &BACK + &BACK + + + + QuickDialog + + + Warning... + Advertencia... + + + + Information... + Información... + + + + Action... + Acción... + + + + Help... + Ayuda... + + + + Error... + Error... + + + + Question... + Pregunta... + + + + Selection... + Selección... + + + + Dialog... + Dialogo... + + + + SQLModel + + + Name + + + + + Salary + + + + + SearchDialog + + + Search Products... + Buscar Productos... + + + + SearchWindow + + + SEARCH... + BUSCAR... + + + + FILTER: + FILTRO: + + + + SelectionWindow + + + SEARCH... + BUSCAR... + + + + &ACCEPT + &ACEPTAR + + + + &RETURN + &VOLVER + + + + main + + + Enter your password: + Ingrese su password: + + + diff --git a/config_pos.ini b/config_pos.ini new file mode 100644 index 0000000..236dd16 --- /dev/null +++ b/config_pos.ini @@ -0,0 +1,59 @@ +[General] +#Valid protocols: xml, json, local +protocol=xml +server=127.0.0.1 +api_url=localhost:5070 +port=8000 +database=DEMO +user=admin + +######################################### +# Printer Interface and Printer name +# For Unix use eg: +# usb,/dev/usb/lp0 +# network,192.168.0.36 +# cups,EPSON-TM-T20 +# +# For Windows use just namep printer eg: +# usb,SATPOS +# +########################################## +printer_sale_name=usb,/dev/usb/lp0 + +# ROW CHARACTERS: 33 / 48 / 28 +# --- EPSON TM-T20 = 33 +# --- TALLY DT-230 = 48 +# --- SAT = 42 +row_characters=48 + +# Define mode to print receipt: automatic or manually +print_receipt=automatic + +# Define electronic scale is used for set quantity +active_weighing=False + +# True / False +print_order=False + +print_auto_order=False + +auto_print_commission=False + +active_timeout=True + +timeout=10000 + +tablet_mode=False + +# Mode of window: fullscreen / maximized +mode_window=maximized + +locale=es_CO.UTF-8 + +language=es_CO + +# Options: retail / restaurant +enviroment=retail + + +profile_name=MY DEMO diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..f0eb28b --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,33 @@ +Tryton POS Client (Using Qt5 and Python3) +========================================= + +# FOR WINDOWS + +Download and install python-3.5.2-amd64 + +Download and install Visual C++ Build Tools 2013 + +Download and install swigwin-3.0.12 + +Add swig.exe to Windows PATH + + +> python -m pip install -U pip + +> pip install -U setuptools + +> pip install -U virtualenv + +> pip install PyQt5 + +> downlod TortoiseHG instalador mercurial + +Copy config_pos.ini on user AppData/Local/tryton + +Modify config_pos.ini + +Put launcher on desktop and set icon change +name "pospro.pyw" + + + diff --git a/doc/translation_guide.rst b/doc/translation_guide.rst new file mode 100644 index 0000000..8ff897f --- /dev/null +++ b/doc/translation_guide.rst @@ -0,0 +1,19 @@ +# Three steps for translating Qt App + +From main directory: + +1. Clean obsolete translations: + + pylupdate5 -noobsolete project.pro + +2. Do translate .ts file, opening linguist tool: + + /usr/lib/qt5/bin/linguist + +3. Release translation + + lrelease app/translations/i18n_es.ts + + + + diff --git a/pospro b/pospro new file mode 100755 index 0000000..e724d7a --- /dev/null +++ b/pospro @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +import os +import sys + +from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import QTranslator +from neox.commons.dblogin import Login +from app import mainwindow + +try: + DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', '..'))) + if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) +except NameError: + pass + +locale_app = os.path.join(os.path.abspath( + os.path.dirname(__file__)), 'app', 'translations', 'i18n_es.qm') + + +class Client(object): + + def __init__(self, parent=None): + self.app = QApplication(sys.argv) + self.translator = QTranslator() + self.translator.load(locale_app) + self.app.installTranslator(self.translator) + + def init_login(self): + login = Login(file_config='config_pos.ini') + + while not login.connection: + login.run() + login.exec_() + + return login.connection, login.params + + def main(self, conn, params): + mw = mainwindow.MainWindow(conn, params) + self.app.exec_() + + +client = Client() +conn, params = client.init_login() + +if conn: + client.main(conn, params) +sys.exit() diff --git a/pospro.pyw b/pospro.pyw new file mode 100755 index 0000000..d5856a9 --- /dev/null +++ b/pospro.pyw @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +import os +import sys + +from PyQt5.QtWidgets import QApplication, QStyleFactory +from PyQt5.QtCore import QTranslator + +from neox.commons.dblogin import Login +from app import mainwindow + +try: + DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', '..'))) + if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) +except NameError: + pass + + +class Client(object): + + def __init__(self, parent=None): + self.app = QApplication(sys.argv) + path_trans = os.path.join(os.path.abspath( + os.path.dirname(__file__)), 'app', 'translations', 'i18n_es.qm') + self.translator = QTranslator() + self.translator.load(path_trans) + # This is to make Qt use locale configuration; i.e. Standard Buttons + # in your system's language. + self.app.installTranslator(self.translator) + self.app.setStyle(QStyleFactory.create('Fusion')) + + def init_login(self): + login = Login(file_config='config_pos.ini') + + while not login.connection: + login.run() + login.exec_() + + return login.connection, login.params + + def main(self, conn, params): + mw = mainwindow.MainWindow(conn, params) + self.app.exec_() + + +client = Client() +conn, params = client.init_login() + +if conn: + client.main(conn, params) +sys.exit() diff --git a/posproc b/posproc new file mode 100644 index 0000000..b3166a5 Binary files /dev/null and b/posproc differ diff --git a/project.pro b/project.pro new file mode 100644 index 0000000..bc8ba28 --- /dev/null +++ b/project.pro @@ -0,0 +1,3 @@ +# Execute in terminal $pylupdate5 project.pro +SOURCES = app/mainwindow.py app/reporting.py app/buttonpad.py +TRANSLATIONS = app/translations/i18n_es.ts diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fdcec11 --- /dev/null +++ b/setup.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# This file is part of Presik POS. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from setuptools import setup +import os +import glob +import shutil + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +args = {} +data_files = [ + ('app', glob.glob('*.ini')), + ('app/frontend', glob.glob('pos/share/*.css')), + ('app/share', glob.glob('app/share/*.png')), + ('app/translations', glob.glob('app/translations/*.qm')), + ('neo/share', glob.glob('neo/share/*.css')), +] + +if os.name == 'posix': + HOME_DIR = os.environ['HOME'] + default_dir = os.path.join(HOME_DIR, '.tryton') + + if not os.path.exists(default_dir): + os.mkdir(default_dir, '0777') + default_config = os.path.join(default_dir, 'config_pos.ini') + + if not os.path.exists(default_config): + shutil.copyfile('config_pos.ini', default_config) + path_inifile = os.path.join(default_dir, 'config_pos.ini') + + +setup(name='presik_pos', + version='4.4.0', + description='POS Client for Tryton', + author='Oscar Alvarez', + author_email='gerente@presik.com', + url='www.presik.com', + download_url="http://www.bitbucket.org", + packages=['app/frontend', 'neo/commons', 'client', + 'neo/proteus', 'app/translations'], + data_files=data_files, + scripts=['pospro'], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: X11 Applications :: Qt', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Operating System :: OS Independent', + 'Natural Language :: English', + 'Natural Language :: Spanish', + 'Programming Language :: Python', + 'Topic :: Office/Business', + ], + license='GPL', + install_requires=[] +) diff --git a/update_new_version.sh b/update_new_version.sh new file mode 100644 index 0000000..6a926cd --- /dev/null +++ b/update_new_version.sh @@ -0,0 +1,4 @@ +#!/bin/sh + + +hg pull && hg update