├── LICENSE
├── Descargando
├── java.desktop
├── java_16.png
├── java_48.png
├── ssl
└── empty
├── jconsole.desktop
├── jshell.desktop
├── src
├── fiscalberry
│ ├── ui
│ │ ├── __init__.py
│ │ ├── assets
│ │ │ ├── bg.jpg
│ │ │ ├── lion.ico
│ │ │ ├── lion.jpg
│ │ │ ├── lion.png
│ │ │ ├── play.png
│ │ │ ├── stop.png
│ │ │ ├── connected.png
│ │ │ ├── fiscalberry.ico
│ │ │ ├── fiscalberry.jpg
│ │ │ ├── fiscalberry.png
│ │ │ ├── disconnected.png
│ │ │ ├── stop.svg
│ │ │ └── play.svg
│ │ ├── adopt_screen.py
│ │ ├── main_screen.py
│ │ ├── kv
│ │ │ ├── fiscalberry.kv
│ │ │ └── main.kv
│ │ ├── log_screen.py
│ │ ├── login_screen.py
│ │ ├── old_fiscalberry_app.py
│ │ └── fiscalberry_app.py
│ ├── version.py
│ ├── common
│ │ ├── __init__.py
│ │ ├── rabbitmq
│ │ │ ├── __init__.py
│ │ │ ├── consumer.py
│ │ │ ├── process_handler.py.backup
│ │ │ └── process_handler.py
│ │ ├── afip.bmp
│ │ ├── assets
│ │ │ └── fiscalberry.ico
│ │ ├── printer_detector.py
│ │ ├── FiscalberryComandos.py
│ │ ├── discover.py
│ │ ├── token_manager.py
│ │ ├── fiscalberry_sio.py
│ │ ├── fiscalberry_logger.py
│ │ └── service_controller.py
│ ├── gui.py
│ ├── __init__.py
│ ├── cli.py
│ └── diagnostics
│ │ ├── test_error_handling.py
│ │ └── rabbitmq_check.py
├── .env.example
├── config.ini.install
├── main.py
├── config.ini.sample
├── scripts
│ └── check_internet.py
├── fiscalberryservice
│ └── android.py
├── test.py
└── LICENSE
├── server.bat
├── assets
└── paxapos.ico
├── my_recipes
└── jpeg
│ ├── Application.mk
│ ├── remove-version.patch
│ ├── build-static.patch
│ └── __init__.py
├── requirements.android.txt
├── requirements.txt
├── requirements.cli.txt
├── fiscalberry.service
├── requirements.kivy.txt
├── test_mode_detection.py
├── Dockerfile
├── Dockerfile.standalone
├── Dockerfile.build
├── fiscalberry-cli.spec
├── file_version_info.txt
├── fiscalberry-gui.spec
├── setup.py
├── .devcontainer
└── devcontainer.json
├── venv.cli
└── share
│ └── man
│ └── man1
│ └── qr.1
├── .gitignore
├── .github
└── workflows
│ ├── android.build.yml
│ └── desktop.build.yml
├── demo_priority_logic.py
├── docs
├── logica_prioridad_rabbitmq.md
├── rabbitmq_error_handling.md
└── mejoras_rabbitmq_resumen.md
├── test_config_priority.py
└── readme.md
/LICENSE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Descargando:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/java.desktop:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/java_16.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/java_48.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ssl/empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jconsole.desktop:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jshell.desktop:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/fiscalberry/ui/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/.env.example:
--------------------------------------------------------------------------------
1 | ENVIRONMENT="development"
--------------------------------------------------------------------------------
/src/fiscalberry/version.py:
--------------------------------------------------------------------------------
1 | VERSION = "2.0.1"
--------------------------------------------------------------------------------
/server.bat:
--------------------------------------------------------------------------------
1 | start "" "pythonw" "C:\fiscalberry\server.py"
--------------------------------------------------------------------------------
/src/fiscalberry/common/__init__.py:
--------------------------------------------------------------------------------
1 | # archivo init del modulo common
--------------------------------------------------------------------------------
/src/fiscalberry/common/rabbitmq/__init__.py:
--------------------------------------------------------------------------------
1 | # archivo init del modulo common
--------------------------------------------------------------------------------
/assets/paxapos.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/assets/paxapos.ico
--------------------------------------------------------------------------------
/src/fiscalberry/common/afip.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/common/afip.bmp
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/bg.jpg
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/lion.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/lion.ico
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/lion.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/lion.jpg
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/lion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/lion.png
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/play.png
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/stop.png
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/connected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/connected.png
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/fiscalberry.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/fiscalberry.ico
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/fiscalberry.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/fiscalberry.jpg
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/fiscalberry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/fiscalberry.png
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/disconnected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/disconnected.png
--------------------------------------------------------------------------------
/src/fiscalberry/common/assets/fiscalberry.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/common/assets/fiscalberry.ico
--------------------------------------------------------------------------------
/my_recipes/jpeg/Application.mk:
--------------------------------------------------------------------------------
1 | APP_OPTIM := release
2 | APP_ABI := all # or armeabi
3 | APP_MODULES := libjpeg
4 | APP_ALLOW_MISSING_DEPS := true
5 |
--------------------------------------------------------------------------------
/requirements.android.txt:
--------------------------------------------------------------------------------
1 | python-escpos[image,qrcode,usb,serial]==3.1
2 | python-socketio[client]==5.11.4
3 | requests==2.32.3
4 | platformdirs==4.2.2
5 | pika
6 | kivy[base]==2.3.1
--------------------------------------------------------------------------------
/src/fiscalberry/gui.py:
--------------------------------------------------------------------------------
1 | from fiscalberry.ui.fiscalberry_app import FiscalberryApp
2 |
3 |
4 | def main():
5 | FiscalberryApp().run()
6 |
7 | if __name__ == "__main__":
8 | main()
--------------------------------------------------------------------------------
/src/fiscalberry/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # pylint: disable=invalid-name
3 |
4 | """
5 | Fiscalberry Server Package
6 | """
7 | from fiscalberry.version import VERSION
8 |
9 | __version__ = VERSION
--------------------------------------------------------------------------------
/src/config.ini.install:
--------------------------------------------------------------------------------
1 | [SERVIDOR]
2 | puerto = 12000
3 | discover_url =
4 | ssl_crt_path =
5 | ssl_key_path =
6 | uuid =
7 |
8 | [IMPRESORA_FISCAL]
9 | marca = Hasar
10 | modelo = 715v2
11 | path = /tmp/respuestas.txt
12 | driver = File
13 |
14 | [IMPRESORA_RED]
15 | marca = EscP
16 | host = 127.0.0.1
17 | driver = ReceiptDirectJet
18 |
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | # src/main.py
2 | import runpy
3 | import os
4 |
5 | # Set the current working directory to the script's directory
6 | # This helps ensure relative paths within your app work correctly
7 | os.chdir(os.path.dirname(os.path.abspath(__file__)))
8 |
9 | # Run your actual main script
10 | runpy.run_module('fiscalberry.gui', run_name='__main__')
--------------------------------------------------------------------------------
/src/config.ini.sample:
--------------------------------------------------------------------------------
1 | [SERVIDOR]
2 | uuid = xxxxx-yyyy-zzzz-aaaa-zzzzzzzzzz
3 | sio_host = http://localhost:5000
4 | sio_password =
5 |
6 |
7 |
8 | [IMPRESORA_FISCAL]
9 | marca = Hasar
10 | modelo = 715v2
11 | path = /tmp/respuestas.txt
12 | driver = File
13 |
14 | [IMPRESORA_RED]
15 | marca = EscP
16 | host = 127.0.0.1
17 | driver = ReceiptDirectJet
18 |
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/stop.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Fiscalberry CLI Dependencies
2 | # Última actualización: 2025-07-19
3 | # Usa las últimas versiones estables de todas las dependencias
4 |
5 | # Core dependencies
6 | python-escpos[all]
7 | python-socketio[client]
8 | requests
9 | aiohttp
10 |
11 | # Utility libraries
12 | appdirs
13 | platformdirs
14 |
15 | # Message broker
16 | pika
17 |
18 | # Windows-specific dependencies
19 | pywin32; sys_platform == 'win32'
--------------------------------------------------------------------------------
/requirements.cli.txt:
--------------------------------------------------------------------------------
1 | # Fiscalberry CLI Dependencies
2 | # Última actualización: 2025-07-19
3 | # Usa las últimas versiones estables de todas las dependencias
4 |
5 | # Core dependencies
6 | python-escpos[all]
7 | python-socketio[client]
8 | requests
9 | aiohttp
10 |
11 | # Utility libraries
12 | appdirs
13 | platformdirs
14 |
15 | # Message broker
16 | pika
17 |
18 | # Windows-specific dependencies
19 | pywin32; sys_platform == 'win32'
--------------------------------------------------------------------------------
/fiscalberry.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Fiscalberry Server
3 | Wants=network-online.target
4 | After=network.target network-online.target
5 |
6 | [Service]
7 | User=root
8 | Group=root
9 | Restart=always
10 | RestartSec=5
11 | Type=simple
12 | ExecStart=/usr/bin/fiscalberry
13 | StandardOutput=syslog
14 | StandardError=syslog
15 | SyslogIdentifier=fiscalberry
16 |
17 | [Install]
18 | WantedBy=multi-user.target
19 | Alias=fiscalberry.service
20 |
--------------------------------------------------------------------------------
/requirements.kivy.txt:
--------------------------------------------------------------------------------
1 | # Fiscalberry GUI (Kivy) Dependencies
2 | # Última actualización: 2025-07-19
3 | # Usa las últimas versiones estables de todas las dependencias
4 |
5 | # GUI Framework
6 | kivy[base]
7 |
8 | # Core dependencies (from CLI)
9 | python-escpos[all]
10 | python-socketio[client]
11 | requests
12 | aiohttp
13 |
14 | # Utility libraries
15 | appdirs
16 | platformdirs
17 |
18 | # Message broker
19 | pika
20 |
21 | # Windows-specific dependencies
22 | pywin32; sys_platform == 'win32'
--------------------------------------------------------------------------------
/src/fiscalberry/ui/assets/play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/scripts/check_internet.py:
--------------------------------------------------------------------------------
1 | # /opt/fiscalberryNUEVO/scripts/check_internet.py
2 | import socket
3 | import sys
4 |
5 | def check_internet(host="8.8.8.8", port=53, timeout=5):
6 | try:
7 | socket.setdefaulttimeout(timeout)
8 | socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
9 | return True
10 | except socket.error as ex:
11 | print(f"Error checking internet connection: {ex}")
12 | return False
13 |
14 | if not check_internet():
15 | sys.exit(1) # Exit with non-zero status to prevent the service from starting
--------------------------------------------------------------------------------
/my_recipes/jpeg/remove-version.patch:
--------------------------------------------------------------------------------
1 | --- jpeg/CMakeLists.txt.orig 2018-11-12 20:20:28.000000000 +0100
2 | +++ jpeg/CMakeLists.txt 2018-12-14 12:43:45.338704504 +0100
3 | @@ -573,6 +573,9 @@
4 | add_library(turbojpeg SHARED ${TURBOJPEG_SOURCES})
5 | set_property(TARGET turbojpeg PROPERTY COMPILE_FLAGS
6 | "-DBMP_SUPPORTED -DPPM_SUPPORTED")
7 | + set_property(TARGET jpeg PROPERTY NO_SONAME 1)
8 | + set_property(TARGET turbojpeg PROPERTY NO_SONAME 1)
9 | + set(CMAKE_SHARED_LIBRARY_SONAME_C_FLAG "")
10 | if(WIN32)
11 | set_target_properties(turbojpeg PROPERTIES DEFINE_SYMBOL DLLDEFINE)
12 | endif()
13 |
--------------------------------------------------------------------------------
/src/fiscalberry/common/printer_detector.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | def listar_impresoras():
4 | sistema_operativo = platform.system()
5 | impresoras = []
6 |
7 | if sistema_operativo == "Linux":
8 | import subprocess
9 |
10 | resultado = subprocess.run(['lpstat', '-e'], stdout=subprocess.PIPE)
11 | impresoras = resultado.stdout.decode('utf-8').splitlines()
12 | elif sistema_operativo == "Windows":
13 | import win32print
14 |
15 | impresoras = [printer[2] for printer in win32print.EnumPrinters(2)]
16 | else:
17 | raise NotImplementedError("Sistema operativo no soportado")
18 |
19 | return impresoras
--------------------------------------------------------------------------------
/test_mode_detection.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import sys
3 | import os
4 |
5 | # Agregar el directorio src al path
6 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
7 |
8 | from fiscalberry.common.service_controller import ServiceController
9 |
10 | def test_mode_detection():
11 | controller = ServiceController()
12 |
13 | print("Probando detección de modo...")
14 | is_gui = controller._is_gui_mode()
15 | print(f"¿Es modo GUI? {is_gui}")
16 |
17 | if is_gui:
18 | print("Modo GUI detectado - usará stop_for_gui()")
19 | else:
20 | print("Modo CLI detectado - usará stop_for_cli()")
21 |
22 | if __name__ == "__main__":
23 | test_mode_detection()
24 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3-slim-bullseye
2 |
3 | WORKDIR /usr/src/app
4 | EXPOSE 12000
5 |
6 |
7 | COPY requirements.txt ./
8 | COPY . .
9 | RUN ls
10 | RUN pip install -r requirements.txt
11 |
12 |
13 | CMD [ "python", "server.py" ]
14 |
15 | ENV CONFIG_FILE_PATH="./"
16 |
17 |
18 | # luego buildear la imagen esta
19 | # docker build -t fiscalberry2 .
20 |
21 | #para buildear asw
22 | # docker build -t yoalevilar/fiscalberry:3 . --no-cache
23 |
24 | # y ejecutarlo asi:
25 | # en la carpeta openvpn guardar todos los archivos de config para conectar con openvpn server
26 | # docker run -d --name fiscalberry --privileged --env=CONFIG_FILE_PATH=/fiscalberry --volume=C:\fiscalberry:/fiscalberry -p 12000:12000 --restart=always yoalevilar/fiscalberry:3
--------------------------------------------------------------------------------
/Dockerfile.standalone:
--------------------------------------------------------------------------------
1 | FROM debian:bookworm-slim
2 |
3 | # Instalar dependencias necesarias
4 | RUN apt-get update && apt-get install -y python3 libstdc++6 cups-client
5 |
6 | RUN apt-get update && apt-get install -y wget
7 |
8 | RUN wget https://github.com/paxapos/fiscalberry/releases/download/v1.0.20/fiscalberry-lin -O /usr/local/bin/fiscalberry-lin
9 |
10 | RUN chmod +x /usr/local/bin/fiscalberry-lin
11 |
12 | RUN apt-get clean && rm -rf /var/lib/apt/lists/*
13 | CMD ["/usr/local/bin/fiscalberry-lin"]
14 |
15 |
16 | # rememter to create volume for config file in /root/.config/Fiscalberry/config.ini
17 | # echo docker run -d --name fiscalberry --privileged --volume=/home/$USER/.config/Fiscalberry:/root/.config/Fiscalberry --restart=always fiscalberry-standalone
18 |
--------------------------------------------------------------------------------
/src/fiscalberry/cli.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import sys
3 |
4 | from fiscalberry.common.fiscalberry_logger import getLogger
5 | logger = getLogger()
6 | from fiscalberry.common.service_controller import ServiceController
7 |
8 | def main():
9 | """Función principal que ejecuta el controlador de servicios."""
10 | controller = ServiceController()
11 |
12 | try:
13 | controller.start()
14 | except KeyboardInterrupt:
15 | print("\nInterrupción de teclado detectada. Cerrando servicios...")
16 | controller.stop()
17 | print("Servicio detenido. Saliendo.")
18 | except Exception as e:
19 | print(f"Error inesperado: {e}")
20 | controller.stop()
21 | sys.exit(1)
22 |
23 | if __name__ == "__main__":
24 | main()
25 |
26 |
--------------------------------------------------------------------------------
/Dockerfile.build:
--------------------------------------------------------------------------------
1 | FROM python:3-slim-bullseye
2 |
3 | WORKDIR /usr/src/app
4 | EXPOSE 12000
5 |
6 |
7 | COPY requirements.cli.txt ./
8 | COPY . .
9 | RUN ls
10 |
11 | RUN apt-get update && \
12 | apt-get install -y libcups2-dev && \
13 | python3 -m pip install --upgrade pip && \
14 | pip3 install pyinstaller && \
15 | pip3 install -r requirements.cli.txt
16 |
17 | # luego buildear la imagen esta
18 | # docker build -t fiscalberry2 .
19 |
20 | #para buildear asw
21 | # docker build -t yoalevilar/fiscalberry:3 . --no-cache
22 |
23 | # y ejecutarlo asi:
24 | # en la carpeta openvpn guardar todos los archivos de config para conectar con openvpn server
25 | # docker run -d --name fiscalberry --privileged --env=CONFIG_FILE_PATH=/fiscalberry --volume=C:\fiscalberry:/fiscalberry -p 12000:12000 --restart=always yoalevilar/fiscalberry:3
--------------------------------------------------------------------------------
/src/fiscalberry/ui/adopt_screen.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | import json
4 | from kivy.uix.screenmanager import Screen
5 | from kivy.app import App
6 | import tempfile
7 | from fiscalberry.common.token_manager import save_token, do_login
8 | from fiscalberry.common.Configberry import Configberry
9 | from fiscalberry.common.fiscalberry_logger import getLogger
10 | from kivy.properties import StringProperty
11 |
12 | logger = getLogger()
13 |
14 | configberry = Configberry()
15 | host = configberry.get("SERVIDOR", "sio_host", "https://beta.paxapos.com")
16 | uuid = configberry.get("SERVIDOR", "uuid", fallback="")
17 |
18 | QRGENLINK = "https://codegenerator.paxapos.com/?bcid=qrcode&text="
19 | ADOP_LINK = host + "/adopt/" + uuid
20 | class AdoptScreen(Screen):
21 | adoptarLink = StringProperty(ADOP_LINK)
22 | qrCodeLink = StringProperty(QRGENLINK + ADOP_LINK)
23 |
24 |
25 |
--------------------------------------------------------------------------------
/fiscalberry-cli.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 |
4 | a = Analysis(
5 | ['src/fiscalberry/cli.py'],
6 | pathex=[],
7 | binaries=[],
8 | datas=[('./capabilities.json', 'escpos')],
9 | hiddenimports=[],
10 | hookspath=[],
11 | hooksconfig={},
12 | runtime_hooks=[],
13 | excludes=[],
14 | noarchive=False,
15 | optimize=0,
16 | )
17 | pyz = PYZ(a.pure)
18 |
19 | exe = EXE(
20 | pyz,
21 | a.scripts,
22 | a.binaries,
23 | a.datas,
24 | [],
25 | name='fiscalberry-cli',
26 | debug=False,
27 | bootloader_ignore_signals=False,
28 | strip=False,
29 | upx=True,
30 | upx_exclude=[],
31 | runtime_tmpdir=None,
32 | console=True,
33 | disable_windowed_traceback=False,
34 | argv_emulation=False,
35 | target_arch=None,
36 | codesign_identity=None,
37 | entitlements_file=None,
38 | icon=['src/fiscalberry/ui/assets/fiscalberry.ico'],
39 | )
40 |
--------------------------------------------------------------------------------
/file_version_info.txt:
--------------------------------------------------------------------------------
1 | # file_version_info.txt
2 | VSVersionInfo(
3 | ffi=FixedFileInfo(
4 | filevers=(1, 0, 0, 0),
5 | prodvers=(1, 0, 0, 0),
6 | mask=0x3f,
7 | flags=0x0,
8 | OS=0x40004,
9 | fileType=0x1,
10 | subtype=0x0,
11 | date=(0, 0)
12 | ),
13 | kids=[
14 | StringFileInfo(
15 | [
16 | StringTable(
17 | u'040904B0',
18 | [StringStruct(u'CompanyName', u'Plus Abstracta'),
19 | StringStruct(u'FileDescription', u'Servidor de Impresion GUI Application'),
20 | StringStruct(u'FileVersion', u'2.0.1.0'),
21 | StringStruct(u'InternalName', u'fiscalberry'),
22 | StringStruct(u'LegalCopyright', u'© Plus Abstracta SRL. All rights reserved.'),
23 | StringStruct(u'OriginalFilename', u'fiscalberry-gui.exe'),
24 | StringStruct(u'ProductName', u'Fiscalberry'),
25 | StringStruct(u'ProductVersion', u'1.0.0.0')])
26 | ]),
27 | VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
28 | ]
29 | )
--------------------------------------------------------------------------------
/fiscalberry-gui.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 |
4 | a = Analysis(
5 | ['src/fiscalberry/gui.py'],
6 | pathex=[],
7 | binaries=[],
8 | datas=[
9 | ('./capabilities.json', 'escpos'),
10 | ('src/fiscalberry/ui/kv', 'fiscalberry/ui/kv')
11 | ],
12 | hiddenimports=[],
13 | hookspath=[],
14 | hooksconfig={},
15 | runtime_hooks=[],
16 | excludes=[],
17 | noarchive=False,
18 | optimize=0,
19 | )
20 | pyz = PYZ(a.pure)
21 |
22 | exe = EXE(
23 | pyz,
24 | a.scripts,
25 | a.binaries,
26 | a.datas,
27 | [],
28 | name='fiscalberry-gui',
29 | debug=False,
30 | bootloader_ignore_signals=False,
31 | strip=False,
32 | upx=False,
33 | upx_exclude=[],
34 | runtime_tmpdir=None,
35 | console=False,
36 | disable_windowed_traceback=False,
37 | argv_emulation=False,
38 | target_arch=None,
39 | codesign_identity=None,
40 | entitlements_file=None,
41 | icon=['src/fiscalberry/ui/assets/fiscalberry.ico'],
42 | version='file_version_info.txt', # Add a version file (see below)
43 | uac_admin=False, # Don't request admin privileges unless necessary
44 | )
45 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from setuptools import setup, find_packages
3 |
4 | def load_requirements(filename):
5 | with open(filename, "r") as req_file:
6 | return [line.strip() for line in req_file
7 | if line.strip() and not line.startswith("#")]
8 |
9 |
10 | setup(
11 | name='fiscalberry',
12 | version='2.0.0',
13 | description='Proyecto con interfaz Kivy y consola',
14 | author='Ale Vilar',
15 | author_email='alevilar@gmail.com',
16 | package_dir={'': 'src'}, # Le decimos que los paquetes están en "src"
17 | packages=find_packages(where='src'), # Esto encontrará "fiscalberry"
18 | install_requires=load_requirements("requirements.txt"), # Asume que load_requirements está fijo para tomar un nombre de archivo
19 | extras_require={
20 | 'cli': load_requirements("requirements.cli.txt"), # Assumes load_requirements is fixed to take a filename
21 | 'gui': load_requirements("requirements.kivy.txt"), # Assumes load_requirements is fixed to take a filename
22 | },
23 | entry_points={
24 | 'console_scripts': [
25 | 'fiscalberry_cli=fiscalberry.cli:main',
26 | 'fiscalberry_gui=fiscalberry.gui:main',
27 | ],
28 | },
29 | )
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python
3 | {
4 | "name": "Python 3",
5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6 | "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
7 |
8 | // Features to add to the dev container. More info: https://containers.dev/features.
9 | // "features": {},
10 |
11 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
12 | "forwardPorts": [12000],
13 |
14 | // Use 'postCreateCommand' to run commands after the container is created.
15 | "postCreateCommand": "pip3 install --user -r requirements.txt",
16 |
17 | // Configure tool-specific properties.
18 | // "customizations": {},
19 |
20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
21 | "remoteUser": "root",
22 | "customizations": {
23 | "vscode": {
24 | "extensions": [
25 | "KevinRose.vsc-python-indent",
26 | "mgesbert.python-path",
27 | "demystifying-javascript.python-extensions-pack",
28 | "BattleBas.kivy-vscode"
29 | ]
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/fiscalberry/common/FiscalberryComandos.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import json
3 | from fiscalberry.common.fiscalberry_logger import getLogger
4 |
5 | class PrinterException(Exception):
6 | pass
7 |
8 | logger = getLogger()
9 |
10 | class FiscalberryComandos():
11 |
12 | timeout = 5
13 |
14 | DEFAULT_DRIVER = "Fiscalberry"
15 |
16 | def run(self, host, jsonData):
17 | """Envia comando a impresora"""
18 |
19 | logger.info(f"Conectando a la URL {host}")
20 | headers = {'Content-type': 'application/json'}
21 |
22 | try:
23 | reply = requests.post(host, data=json.dumps(jsonData), headers=headers, timeout=self.timeout)
24 |
25 | if reply.status_code == 200:
26 | logger.info("Conexión exitosa")
27 | return []
28 | except requests.exceptions.Timeout as e:
29 | # Maybe set up for a retry, or continue in a retry loop
30 | # Sin timeout, si NO existe la IP de destino, queda intentando infinitamente.
31 | logger.error(f"Timeout de conexión con {host} después de {self.timeout} segundos")
32 | return []
33 |
34 | except requests.exceptions.RequestException as e:
35 | # catastrophic error. bail.
36 | # Raises en caso de que exista la IP destino pero el puerto esté cerrado
37 | logger.error(f"No se pudo establecer la conexión con {host}")
38 | return []
39 |
--------------------------------------------------------------------------------
/src/fiscalberry/ui/main_screen.py:
--------------------------------------------------------------------------------
1 | from kivy.uix.screenmanager import Screen
2 | from kivy.app import App # Import the App class
3 | import os
4 | from fiscalberry.common.token_manager import delete_token
5 | from kivy.properties import StringProperty
6 | from os.path import join, dirname
7 |
8 | class MainScreen(Screen):
9 | stop_image = StringProperty(join(dirname(__file__), "assets/stop.png"))
10 | play_image = StringProperty(join(dirname(__file__), "assets/play.png"))
11 |
12 | connected_image = StringProperty(join(dirname(__file__), "assets/connected.png"))
13 | disconnected_image = StringProperty(join(dirname(__file__), "assets/disconnected.png"))
14 |
15 | def start_service(self):
16 | """Inicia el servicio desde la GUI."""
17 | app = App.get_running_app() # Use App.get_running_app()
18 | app.on_start_service()
19 |
20 | def toggle_service(self):
21 | """Alterna el estado del servicio desde la GUI."""
22 | app = App.get_running_app()
23 | app.on_toggle_service()
24 |
25 | def stop_service(self):
26 | """Detiene el servicio desde la GUI."""
27 | app = App.get_running_app() # Use App.get_running_app()
28 | app.on_stop_service()
29 |
30 | def logout(self):
31 | """Cierra sesión y elimina el token JWT."""
32 | delete_token()
33 |
34 | app = App.get_running_app() # Use App.get_running_app()
35 | app.root.current = "login"
--------------------------------------------------------------------------------
/venv.cli/share/man/man1/qr.1:
--------------------------------------------------------------------------------
1 | .\" Manpage for qr
2 | .TH QR 1 "6 Feb 2023" "7.4.2" "Python QR tool"
3 | .SH NAME
4 | qr \- script to create QR codes at the command line
5 | .SH SYNOPSIS
6 | qr [\-\-help] [\-\-factory=FACTORY] [\-\-optimize=OPTIMIZE] [\-\-error\-correction=LEVEL] [data]
7 | .SH DESCRIPTION
8 | This script uses the python qrcode module. It can take data from stdin or from the commandline and generate a QR code.
9 | Normally it will output the QR code as ascii art to the terminal. If the output is piped to a file, it will output the image (default type of PNG).
10 | .SH OPTIONS
11 | .PP
12 | \fB\ \-h, \-\-help\fR
13 | .RS 4
14 | Show a help message.
15 | .RE
16 |
17 | .PP
18 | \fB\ \-\-factory=FACTORY\fR
19 | .RS 4
20 | Full python path to the image factory class to create the
21 | image with. You can use the following shortcuts to the
22 | built-in image factory classes: pil (default), png (
23 | default if pillow is not installed), svg, svg-fragment,
24 | svg-path.
25 | .RE
26 |
27 | .PP
28 | \fB\ \-\-optimize=OPTIMIZE\fR
29 | .RS 4
30 | Optimize the data by looking for chunks of at least this
31 | many characters that could use a more efficient encoding
32 | method. Use 0 to turn off chunk optimization.
33 | .RE
34 |
35 | .PP
36 | \fB\ \-\-error\-correction=LEVEL\fR
37 | .RS 4
38 | The error correction level to use. Choices are L (7%),
39 | M (15%, default), Q (25%), and H (30%).
40 | .RE
41 |
42 | .PP
43 | \fB\ data\fR
44 | .RS 4
45 | The data from which the QR code will be generated.
46 | .RE
47 |
48 | .SH SEE ALSO
49 | https://github.com/lincolnloop/python-qrcode/
50 |
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.pyc
6 | .pyc
7 | *.log
8 | *.swp
9 | py3env
10 | bin
11 | lib
12 | lib64
13 | output
14 | .buildozer
15 |
16 | #configuration file for fiscalberry
17 | config.ini
18 | bemafix.py
19 | fiscaltest.py
20 | tornado.*
21 | comanderatest.py
22 | test_scripts/*
23 | env/*
24 | ssl/*
25 | tmp/*
26 | .venv
27 | venv.kivy/
28 | .venv.android/
29 |
30 | # Distribution / packaging
31 | *.pyc
32 | .Python
33 | env/
34 | build/
35 | develop-eggs/
36 | dist/
37 | downloads/
38 | eggs/
39 | .eggs/
40 | lib/
41 | lib64/
42 | parts/
43 | sdist/
44 | var/
45 | *.egg-info/
46 | .installed.cfg
47 | *.egg
48 |
49 | # PyInstaller
50 | # Usually these files are written by a python script from a template
51 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
52 | *.manifest
53 |
54 | # Installer logs
55 | pip-log.txt
56 | pip-delete-this-directory.txt
57 |
58 | # Unit test / coverage reports
59 | htmlcov/
60 | .tox/
61 | .coverage
62 | .coverage.*
63 | .cache
64 | nosetests.xml
65 | coverage.xml
66 | *,cover
67 | .hypothesis/
68 |
69 | # Translations
70 | *.mo
71 | *.pot
72 |
73 | # Django stuff:
74 | *.log
75 | local_settings.py
76 |
77 | # Flask stuff:
78 | instance/
79 | .webassets-cache
80 |
81 | # Scrapy stuff:
82 | .scrapy
83 |
84 | # Sphinx documentation
85 | docs/_build/
86 |
87 | # PyBuilder
88 | target/
89 |
90 | # IPython Notebook
91 | .ipynb_checkpoints
92 |
93 | # pyenv
94 | .python-version
95 |
96 | # celery beat schedule file
97 | celerybeat-schedule
98 |
99 | # dotenv
100 | .env
101 |
102 | # virtualenv
103 | venv/
104 | ENV/
105 |
106 | # Spyder project settings
107 | .spyderproject
108 |
109 | # Rope project settings
110 | .ropeproject
111 |
112 |
113 | .vscode/
114 | fiscalberry3env
115 | pyvenv.cfg
116 |
117 |
118 | kivy_venv
119 | fiscalberry3env
120 | pyManjaroEnv
121 | bin
122 | build
123 |
--------------------------------------------------------------------------------
/src/fiscalberry/common/discover.py:
--------------------------------------------------------------------------------
1 | import threading
2 | import requests
3 | import json
4 | from fiscalberry.common.Configberry import Configberry
5 | from fiscalberry.common.fiscalberry_logger import getLogger
6 | from fiscalberry.common.printer_detector import listar_impresoras
7 |
8 | configberry = Configberry()
9 |
10 | def send_discover():
11 |
12 | logger = getLogger()
13 | discoverUrl = False
14 |
15 | uuidval = configberry.config.get("SERVIDOR", "uuid", fallback="")
16 |
17 | if not uuidval:
18 | logger.error("No se ha configurado el uuid en el archivo de configuracion")
19 | return None
20 |
21 | data = configberry.getJSON()
22 | data["installed_printers"] = listar_impresoras()
23 | senddata = {
24 | "uuid": configberry.config.get("SERVIDOR", "uuid"),
25 | "raw_data": json.dumps(data)
26 | }
27 |
28 | ### Enviar al Discover
29 | host = configberry.config.get("SERVIDOR", "sio_host", fallback="")
30 |
31 | discoverUrl = host + "/discover.json"
32 | logger.info(f"DISCOVER:: URL: {discoverUrl}")
33 | ret = None
34 |
35 | if discoverUrl:
36 | try:
37 | headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
38 | ret = requests.post(discoverUrl, headers=headers, data=json.dumps(senddata))
39 |
40 | if ret.status_code != requests.codes.ok:
41 | raise Exception(f"Error de conexión con {discoverUrl} status_code: {ret.status_code} - {ret.text}")
42 |
43 | except Exception as e:
44 | if str(e.args[0]).startswith("Invalid URL"): logger.error(f"El formato de 'discover_url' es inválido: \033[91m{discoverUrl}\033[0m")
45 |
46 | logger.error(f"No es posible conectarse con el Discover en {discoverUrl}. El error dice: {str(e)}")
47 |
48 | else:
49 | logger.info("No hay sio_host configurado, no tengo el host donde hacer el discover")
50 |
51 | return None
52 |
53 |
54 | def send_discover_in_thread():
55 | thread = threading.Thread(target=send_discover, daemon=True)
56 | return thread
--------------------------------------------------------------------------------
/src/fiscalberry/ui/kv/fiscalberry.kv:
--------------------------------------------------------------------------------
1 | #:kivy 2.3.0
2 |
3 |
4 | :
5 | title: "Fiscalberry App Iupi!"
6 | orientation: 'vertical'
7 |
8 | canvas.before:
9 | Rectangle:
10 | pos: self.pos
11 | size: self.size
12 | source: app.background_image
13 |
14 |
15 | BoxLayout:
16 | orientation: 'vertical'
17 | padding: 10
18 |
19 | Image:
20 | source: app.logo_image
21 | size_hint_y: None
22 | height: 96
23 | allow_stretch: True
24 | keep_ratio: True
25 | color: 1, 1, 1, 1
26 | margin_bottom: 10
27 |
28 | Label:
29 | id: title_label
30 | text: "Print Server"
31 | font_size: 32
32 | bold: True
33 | size_hint_y: None
34 | height: 64
35 | color: 0, 0, 128, 1
36 | margin_bottom: 10
37 |
38 |
39 | Label:
40 | id: conectado_label
41 | text: "Conectado!" if app.connected else "Desconectado"
42 | color: (0, 128, 0, 1) if app.connected else (128, 0, 0, 1)
43 | font_size: 48
44 |
45 | Button:
46 | id: connect_button
47 | text: "Conectar" if not app.connected else "Desconectar"
48 | on_press: app.start_service() if not app.connected else app.stop_service()
49 | size_hint_y: None
50 | height: 64
51 | background_color: (0, 128, 0, 1) if app.connected else (128, 0, 0, 1)
52 | color: 1, 1, 1, 1
53 |
54 |
55 | Label:
56 | id: uuid_label
57 | text: app.uuidSmall
58 | font_size: 16
59 | size_hint_y: None
60 | height: 40
61 | color: 128, 0, 0, 1
62 |
63 |
64 | TextInput:
65 | id: host_input
66 | text: app.sioServerUrl
67 | multiline: False
68 | size_hint_y: None
69 | height: 40
70 | on_text: app.store_new_host(self.text)
71 |
72 |
73 |
74 | :
75 | height: 200
76 |
77 |
78 | :
79 | source: app.connected_image if app.connected else app.disconnected_image
80 | height: 16
81 | keep_ratio: True
--------------------------------------------------------------------------------
/my_recipes/jpeg/build-static.patch:
--------------------------------------------------------------------------------
1 | diff -Naur jpeg/Android.mk b/Android.mk
2 | --- jpeg/Android.mk 2015-12-14 11:37:25.900190235 -0600
3 | +++ b/Android.mk 2015-12-14 11:41:27.532182210 -0600
4 | @@ -54,8 +54,7 @@
5 |
6 | LOCAL_SRC_FILES:= $(libjpeg_SOURCES_DIST)
7 |
8 | -LOCAL_SHARED_LIBRARIES := libcutils
9 | -LOCAL_STATIC_LIBRARIES := libsimd
10 | +LOCAL_STATIC_LIBRARIES := libsimd libcutils
11 |
12 | LOCAL_C_INCLUDES := $(LOCAL_PATH)
13 |
14 | @@ -68,7 +67,7 @@
15 |
16 | LOCAL_MODULE := libjpeg
17 |
18 | -include $(BUILD_SHARED_LIBRARY)
19 | +include $(BUILD_STATIC_LIBRARY)
20 |
21 | ######################################################
22 | ### cjpeg ###
23 | @@ -82,7 +81,7 @@
24 |
25 | LOCAL_SRC_FILES:= $(cjpeg_SOURCES)
26 |
27 | -LOCAL_SHARED_LIBRARIES := libjpeg
28 | +LOCAL_STATIC_LIBRARIES := libjpeg
29 |
30 | LOCAL_C_INCLUDES := $(LOCAL_PATH) \
31 | $(LOCAL_PATH)/android
32 | @@ -110,7 +109,7 @@
33 |
34 | LOCAL_SRC_FILES:= $(djpeg_SOURCES)
35 |
36 | -LOCAL_SHARED_LIBRARIES := libjpeg
37 | +LOCAL_STATIC_LIBRARIES := libjpeg
38 |
39 | LOCAL_C_INCLUDES := $(LOCAL_PATH) \
40 | $(LOCAL_PATH)/android
41 | @@ -137,7 +136,7 @@
42 |
43 | LOCAL_SRC_FILES:= $(jpegtran_SOURCES)
44 |
45 | -LOCAL_SHARED_LIBRARIES := libjpeg
46 | +LOCAL_STATIC_LIBRARIES := libjpeg
47 |
48 | LOCAL_C_INCLUDES := $(LOCAL_PATH) \
49 | $(LOCAL_PATH)/android
50 | @@ -163,7 +162,7 @@
51 |
52 | LOCAL_SRC_FILES:= $(tjunittest_SOURCES)
53 |
54 | -LOCAL_SHARED_LIBRARIES := libjpeg
55 | +LOCAL_STATIC_LIBRARIES := libjpeg
56 |
57 | LOCAL_C_INCLUDES := $(LOCAL_PATH)
58 |
59 | @@ -189,7 +188,7 @@
60 |
61 | LOCAL_SRC_FILES:= $(tjbench_SOURCES)
62 |
63 | -LOCAL_SHARED_LIBRARIES := libjpeg
64 | +LOCAL_STATIC_LIBRARIES := libjpeg
65 |
66 | LOCAL_C_INCLUDES := $(LOCAL_PATH)
67 |
68 | @@ -215,7 +214,7 @@
69 |
70 | LOCAL_SRC_FILES:= $(rdjpgcom_SOURCES)
71 |
72 | -LOCAL_SHARED_LIBRARIES := libjpeg
73 | +LOCAL_STATIC_LIBRARIES := libjpeg
74 |
75 | LOCAL_C_INCLUDES := $(LOCAL_PATH)
76 |
77 | @@ -240,7 +239,7 @@
78 |
79 | LOCAL_SRC_FILES:= $(wrjpgcom_SOURCES)
80 |
81 | -LOCAL_SHARED_LIBRARIES := libjpeg
82 | +LOCAL_STATIC_LIBRARIES := libjpeg
83 |
84 | LOCAL_C_INCLUDES := $(LOCAL_PATH)
85 |
86 |
--------------------------------------------------------------------------------
/my_recipes/jpeg/__init__.py:
--------------------------------------------------------------------------------
1 | from pythonforandroid.recipe import Recipe
2 | from pythonforandroid.logger import shprint
3 | from pythonforandroid.util import current_directory
4 | from os.path import join
5 | import sh
6 |
7 |
8 | class JpegRecipe(Recipe):
9 | '''
10 | .. versionchanged:: 0.6.0
11 | rewrote recipe to be build with clang and updated libraries to latest
12 | version of the official git repo.
13 | '''
14 | name = 'jpeg'
15 | version = '2.0.1'
16 | url = 'https://github.com/libjpeg-turbo/libjpeg-turbo/archive/{version}.tar.gz' # noqa
17 | built_libraries = {'libjpeg.a': '.', 'libturbojpeg.a': '.'}
18 | # we will require this below patch to build the shared library
19 | # patches = ['remove-version.patch']
20 |
21 | def build_arch(self, arch):
22 | build_dir = self.get_build_dir(arch.arch)
23 |
24 | # TODO: Fix simd/neon
25 | with current_directory(build_dir):
26 | env = self.get_recipe_env(arch)
27 | toolchain_file = join(self.ctx.ndk_dir,
28 | 'build/cmake/android.toolchain.cmake')
29 |
30 | shprint(sh.rm, '-rf', 'CMakeCache.txt', 'CMakeFiles/')
31 | shprint(sh.cmake, '-G', 'Unix Makefiles',
32 | '-DCMAKE_SYSTEM_NAME=Android',
33 | '-DCMAKE_POSITION_INDEPENDENT_CODE=1',
34 | '-DCMAKE_ANDROID_ARCH_ABI={arch}'.format(arch=arch.arch),
35 | '-DCMAKE_ANDROID_NDK=' + self.ctx.ndk_dir,
36 | '-DCMAKE_C_COMPILER={cc}'.format(cc=arch.get_clang_exe()),
37 | '-DCMAKE_CXX_COMPILER={cc_plus}'.format(
38 | cc_plus=arch.get_clang_exe(plus_plus=True)),
39 | '-DCMAKE_BUILD_TYPE=Release',
40 | '-DCMAKE_INSTALL_PREFIX=./install',
41 | '-DCMAKE_TOOLCHAIN_FILE=' + toolchain_file,
42 |
43 | '-DANDROID_ABI={arch}'.format(arch=arch.arch),
44 | '-DANDROID_ARM_NEON=ON',
45 | '-DENABLE_NEON=ON',
46 | # '-DREQUIRE_SIMD=1',
47 |
48 | # Force disable shared, with the static ones is enough
49 | '-DENABLE_SHARED=0',
50 | '-DENABLE_STATIC=1',
51 | '-DCMAKE_POLICY_VERSION_MINIMUM=3.5',
52 |
53 | _env=env)
54 | shprint(sh.make, _env=env)
55 |
56 |
57 | recipe = JpegRecipe()
58 |
--------------------------------------------------------------------------------
/src/fiscalberry/ui/log_screen.py:
--------------------------------------------------------------------------------
1 | from kivy.uix.screenmanager import Screen
2 | from kivy.properties import StringProperty
3 | from kivy.clock import Clock
4 | from fiscalberry.common.fiscalberry_logger import getLogFilePath
5 | import platform
6 | import os
7 | import subprocess
8 | class LogScreen(Screen):
9 | logs = StringProperty("") # Propiedad para almacenar los logs
10 | logFilePath = StringProperty("") # Propiedad para almacenar la ruta del log
11 |
12 | def on_kv_post(self, base_widget):
13 | # Se garantiza que los ids están disponibles
14 | Clock.schedule_interval(self.update_logs, 1) # Actualizar cada segundo
15 |
16 |
17 | def open_log_file(self):
18 | """Abre el archivo de log en el editor de texto predeterminado."""
19 | if self.logFilePath:
20 |
21 | try:
22 | system = platform.system()
23 | if system == "Windows":
24 | # os.startfile opens the file with the default application on Windows
25 | os.startfile(self.logFilePath)
26 | elif system == "Darwin": # macOS
27 | subprocess.Popen(["open", self.logFilePath])
28 | else: # Linux and other Unix-like systems
29 | # xdg-open opens the file with the default application on Linux
30 | subprocess.Popen(["xdg-open", self.logFilePath])
31 | except FileNotFoundError:
32 | self.logs = f"Error: No se encontró el archivo de log en {self.logFilePath}"
33 | except OSError as e:
34 | # Catch errors like 'xdg-open' or 'open' not found, or no default app associated
35 | self.logs = f"Error al abrir el log con la aplicación predeterminada: {e}"
36 | except Exception as e:
37 | self.logs = f"Error inesperado al abrir el log: {e}"
38 | else:
39 | self.logs = "No se ha encontrado la ruta del archivo de log."
40 |
41 | def update_logs(self, dt):
42 | """Lee el archivo de logs y actualiza la propiedad `logs`."""
43 | self.logFilePath = getLogFilePath()
44 |
45 | try:
46 | with open(self.logFilePath, "r") as log_file:
47 | log_data = log_file.read() # Leer contenido del log
48 | self.logs = log_data
49 | # Actualizar el scroll si existe el ScrollView
50 | if "scroll_view" in self.ids:
51 | self.ids.scroll_view.scroll_y = 0
52 | except Exception as e:
53 | self.logs = f"Error al leer log: {e}"
--------------------------------------------------------------------------------
/src/fiscalberry/ui/login_screen.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | import json
4 | from kivy.uix.screenmanager import Screen
5 | from kivy.app import App
6 | import tempfile
7 | from fiscalberry.common.token_manager import save_token, do_login
8 | from fiscalberry.common.Configberry import Configberry
9 | from fiscalberry.common.fiscalberry_logger import getLogger
10 | from kivy.properties import StringProperty
11 |
12 | logger = getLogger()
13 |
14 | configberry = Configberry()
15 | host = configberry.get("SERVIDOR", "sio_host", "https://beta.paxapos.com")
16 | uuid = configberry.get("SERVIDOR", "uuid", fallback="")
17 |
18 | QRGENLINK = "https://codegenerator.paxapos.com/?bcid=qrcode&text="
19 | ADOP_LINK = host + "/adopt/" + uuid
20 | class LoginScreen(Screen):
21 | errorMsg = StringProperty("")
22 | adoptarLink = StringProperty(ADOP_LINK)
23 | qrCodeLink = StringProperty(QRGENLINK + ADOP_LINK)
24 |
25 |
26 | def on_leave(self, *args):
27 | """Se llama automáticamente cuando se sale de la pantalla."""
28 | self.errorMsg = ""
29 |
30 | def login(self, username, password):
31 | """Autentica al usuario contra el backend."""
32 | backend_url = host.rstrip("/") + "/login.json"
33 | try:
34 | if do_login(username, password):
35 | # Login exitoso
36 | app = App.get_running_app()
37 | app.root.current = "main"
38 | return True
39 | else:
40 | # Login fallido
41 | logger.error("Error de autenticación.")
42 | return False
43 | except requests.exceptions.RequestException as e:
44 | logger.error(f"Error al conectar con el backend: {e}")
45 | self.errorMsg = f"Error de conexión: {e}"
46 | return False
47 | except Exception as e:
48 | logger.error(f"Error de autenticación: {e}")
49 | self.errorMsg = str(e)
50 | return False
51 |
52 |
53 |
54 |
55 |
56 |
57 | def save_token(self, token):
58 | """Guarda el token JWT en un archivo local."""
59 | return save_token(token)
60 |
61 | def get_sites(self):
62 | """Obtiene los sitios del usuario autenticado."""
63 | backend_url = host.rstrip("/") + "/sites.json"
64 | try:
65 | response = requests.get(backend_url, headers={"Authorization": f"Bearer {self.load_token()}"})
66 | logger.info(f"Response: {response.status_code}, {response.text}")
67 | if response.status_code == 200:
68 | return response.json()
69 | else:
70 | logger.error("Error al obtener los sitios.")
71 | return []
72 | except requests.exceptions.RequestException as e:
73 | logger.error(f"Error al conectar con el backend: {e}")
74 | return []
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/fiscalberry/diagnostics/test_error_handling.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Script de prueba para verificar el comportamiento del manejo de errores mejorado.
4 | """
5 |
6 | import sys
7 | import os
8 | import threading
9 | import time
10 |
11 | # Agregar el directorio src al path para importar los módulos
12 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
13 |
14 | from fiscalberry.common.rabbitmq.process_handler import RabbitMQProcessHandler
15 | from fiscalberry.common.Configberry import Configberry
16 | import queue
17 |
18 | def test_error_handling():
19 | """Prueba el manejo de errores con configuración que causará error DNS."""
20 | print("=== Prueba del manejo de errores mejorado ===\n")
21 |
22 | # Configurar valores que causarán error DNS
23 | config = Configberry()
24 |
25 | # Guardar configuración original
26 | original_config = {
27 | "host": config.get("RabbitMq", "host"),
28 | "port": config.get("RabbitMq", "port"),
29 | "user": config.get("RabbitMq", "user"),
30 | "password": config.get("RabbitMq", "password"),
31 | "vhost": config.get("RabbitMq", "vhost"),
32 | "queue": config.get("RabbitMq", "queue"),
33 | }
34 |
35 | try:
36 | # Configurar para causar error DNS (hostname inexistente)
37 | test_config = {
38 | "host": "rabbitmq", # Este hostname causará error DNS
39 | "port": "5672",
40 | "user": "guest",
41 | "password": "guest",
42 | "vhost": "/",
43 | "queue": "test-queue"
44 | }
45 |
46 | config.set("RabbitMq", test_config)
47 | print(f"Configuración de prueba establecida: {test_config}")
48 |
49 | # Crear queue para mensajes
50 | message_queue = queue.Queue()
51 |
52 | # Inicializar el manejador de proceso
53 | handler = RabbitMQProcessHandler()
54 |
55 | print("\nIniciando RabbitMQ process handler...")
56 | print("Esperando ver errores DNS mejorados y sistema de backoff...\n")
57 |
58 | # Iniciar el handler
59 | handler.start(message_queue)
60 |
61 | # Esperar un poco para ver algunos intentos de conexión
62 | print("Dejando que el sistema intente conectar por 30 segundos...")
63 | print("Deberías ver mensajes de error más descriptivos y reintentos con backoff.\n")
64 |
65 | time.sleep(30)
66 |
67 | print("\nDeteniendo el handler...")
68 | handler.stop(timeout=2)
69 | print("Handler detenido.")
70 |
71 | except KeyboardInterrupt:
72 | print("\nInterrumpido por usuario.")
73 | handler.stop(timeout=2)
74 |
75 | finally:
76 | # Restaurar configuración original
77 | config.set("RabbitMq", original_config)
78 | print("Configuración original restaurada.")
79 |
80 | if __name__ == "__main__":
81 | test_error_handling()
82 |
--------------------------------------------------------------------------------
/src/fiscalberry/common/token_manager.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | import json
4 | import platformdirs
5 |
6 | # Define your app name and author for proper directory structure
7 | APP_NAME = "fiscalberry"
8 | APP_AUTHOR = "fiscalberry"
9 |
10 | # Use platformdirs to get the correct config directory
11 | TOKEN_FILE = os.path.join(platformdirs.user_config_dir(APP_NAME, APP_AUTHOR), "fiscalberry_jwt_token.json")
12 |
13 | # Ensure the directory exists
14 | os.makedirs(os.path.dirname(TOKEN_FILE), exist_ok=True)
15 |
16 | def do_login(username, password):
17 | """Autentica al usuario contra el backend y guarda el token JWT."""
18 |
19 | from fiscalberry.common.Configberry import Configberry
20 |
21 | cfg = Configberry()
22 | host = cfg.get("SERVIDOR","sio_host")
23 | backend_url = host.rstrip("/") + "/login.json"
24 |
25 | response = requests.post(backend_url, json={"email": username, "password": password})
26 | if response.status_code == 200:
27 | # Login exitoso, guardar el token JWT
28 | token = response.json().get("jwt")
29 | if token:
30 | save_token(token)
31 | return token
32 | else:
33 | raise Exception("Error: No se recibió un token.")
34 | elif response.status_code == 401:
35 | error_message = response.json().get("message", f"Error de autenticación 401 (Código {response.status_code}). Verifique sus credenciales.")
36 | raise Exception(error_message)
37 | else:
38 | error_message = response.json().get("message", f"Error de autenticación (Código {response.status_code}). Verifique sus credenciales.")
39 | raise Exception(error_message)
40 | return ""
41 |
42 |
43 | def save_token(token):
44 | """Guarda el token JWT en un archivo local."""
45 | with open(TOKEN_FILE, "w") as f:
46 | json.dump({"token": token}, f)
47 |
48 |
49 | def get_token():
50 | """Carga el token JWT desde un archivo local."""
51 | if os.path.exists(TOKEN_FILE):
52 | with open(TOKEN_FILE, "r") as f:
53 | data = json.load(f)
54 | return data.get("token")
55 | return None
56 |
57 | def delete_token():
58 | """Elimina el token JWT del archivo local."""
59 | if os.path.exists(TOKEN_FILE):
60 | os.remove(TOKEN_FILE)
61 |
62 |
63 |
64 | def make_authenticated_request(url, method="GET", data=None):
65 | """Realiza una solicitud autenticada usando el token JWT."""
66 | token = get_token()
67 | if not token:
68 | raise Exception("No se encontró un token JWT. Inicie sesión primero.")
69 |
70 | headers = {"Authorization": f"Bearer {token}"}
71 | try:
72 | if method == "GET":
73 | response = requests.get(url, headers=headers)
74 | elif method == "POST":
75 | response = requests.post(url, json=data, headers=headers)
76 | else:
77 | raise ValueError("Método HTTP no soportado.")
78 | return response
79 | except requests.exceptions.RequestException as e:
80 | print(f"Error en la solicitud: {e}")
81 | return None
--------------------------------------------------------------------------------
/.github/workflows/android.build.yml:
--------------------------------------------------------------------------------
1 | name: Build Android APK
2 |
3 |
4 | on:
5 | push:
6 | tags: # Trigger on tags like v*.*.*
7 | - 'NEVER-MATCH-v*.*.*'
8 | workflow_dispatch: # Allows manual triggering if needed
9 |
10 |
11 | jobs:
12 | build-android:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v4
18 |
19 | - name: Set up JDK 17
20 | uses: actions/setup-java@v4
21 | with:
22 | distribution: 'temurin' # O 'zulu', 'adopt', etc.
23 | java-version: '17'
24 |
25 | - name: Set up Python 3.11 # O la versión que prefieras/necesites
26 | uses: actions/setup-python@v5
27 | with:
28 | python-version: '3.11'
29 |
30 | - name: Get pip cache dir
31 | id: pip-cache
32 | run: |
33 | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
34 |
35 | - name: Cache pip
36 | uses: actions/cache@v4
37 | with:
38 | path: ${{ steps.pip-cache.outputs.dir }}
39 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} # Cambia si tus requisitos están en buildozer.spec
40 | restore-keys: |
41 | ${{ runner.os }}-pip-
42 |
43 | - name: Cache Buildozer global directory
44 | uses: actions/cache@v4
45 | with:
46 | path: ~/.buildozer
47 | key: ${{ runner.os }}-buildozer-${{ hashFiles('buildozer.spec') }} # Key based on buildozer.spec changes
48 | restore-keys: |
49 | ${{ runner.os }}-buildozer-
50 |
51 | - name: Install Buildozer system dependencies
52 | run: |
53 | sudo apt-get update
54 | sudo apt-get install -y \
55 | git zip unzip build-essential python3-pip python3-dev \
56 | libffi-dev libssl-dev libncurses5-dev zlib1g-dev libsqlite3-dev libbz2-dev \
57 | autoconf automake libtool libltdl-dev pkg-config \
58 | cython3 ccache
59 |
60 | - name: Install Python build tools and Cython
61 | run: |
62 | python -m pip install --upgrade pip wheel setuptools
63 | python -m pip install cython
64 |
65 | - name: Install Buildozer
66 | run: |
67 | python -m pip install buildozer
68 | python -m pip install -r requirements.android.txt
69 |
70 |
71 |
72 | - name: Build APK with Buildozer
73 | # Set ACLOCAL_PATH and PKG_CONFIG_PATH explicitly before running buildozer
74 | # This helps autotools find necessary macro and package config files
75 | run: |
76 | export ACLOCAL_PATH="${ACLOCAL_PATH}:/usr/share/aclocal"
77 | export PKG_CONFIG_PATH="${PKG_CONFIG_PATH}:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig"
78 | rm -rf ~/.buildozer
79 | rm -rf ./.buildozer
80 |
81 | buildozer -v android release
82 |
83 | # --- step for debugging ---
84 | - name: List bin directory contents (after build)
85 | if: always() # Run even if previous steps fail, to aid debugging
86 | run: |
87 | echo "Listing contents of ./bin directory:"
88 | ls -lR ./bin || echo "Bin directory not found or empty."
89 |
90 | - name: Upload APK artifact
91 | uses: actions/upload-artifact@v4
92 | with:
93 | name: fiscalberry-android-apk-${{ github.ref_name }}
94 | path: bin/*.apk
--------------------------------------------------------------------------------
/demo_priority_logic.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Demostración de la nueva lógica de prioridad config.ini vs SocketIO
4 | para configuración de RabbitMQ en Fiscalberry.
5 | """
6 |
7 | import configparser
8 | import os
9 | import sys
10 |
11 | def demonstrate_priority_logic():
12 | """Demuestra cómo funciona la nueva lógica de prioridad"""
13 |
14 | # Leer el config.ini real del usuario
15 | config_path = os.path.expanduser("~/.config/Fiscalberry/config.ini")
16 |
17 | if not os.path.exists(config_path):
18 | print(f"❌ No se encontró {config_path}")
19 | return
20 |
21 | config = configparser.ConfigParser()
22 | config.read(config_path)
23 |
24 | print("🔧 DEMOSTRACIÓN: Lógica de Prioridad RabbitMQ Config.ini vs SocketIO")
25 | print("=" * 70)
26 |
27 | # Mostrar valores actuales del config.ini
28 | print("\n📄 Valores actuales en config.ini:")
29 | if config.has_section('RabbitMq'):
30 | for key in ['host', 'port', 'user', 'password', 'vhost', 'queue']:
31 | value = config.get('RabbitMq', key, fallback='')
32 | status = "✓ Tiene valor" if value and value.strip() else "❌ Vacío"
33 | print(f" {key}: '{value}' {status}")
34 | else:
35 | print(" ❌ No hay sección [RabbitMq]")
36 |
37 | # Simular un mensaje SocketIO con valores diferentes
38 | print("\n📡 Mensaje SocketIO simulado:")
39 | rabbit_cfg = {
40 | 'host': 'rabbitmq.server.socketio.com',
41 | 'port': '5673',
42 | 'user': 'socketio_user',
43 | 'password': 'socketio_password',
44 | 'vhost': '/socketio_vhost',
45 | 'queue': 'socketio_queue'
46 | }
47 |
48 | for key, value in rabbit_cfg.items():
49 | print(f" {key}: '{value}'")
50 |
51 | # Aplicar la nueva lógica
52 | print("\n🎯 APLICANDO NUEVA LÓGICA (config.ini tiene prioridad):")
53 | print("-" * 50)
54 |
55 | def get_config_or_message_value(config_key, message_key, default_value=""):
56 | """Lógica exacta implementada en process_handler.py"""
57 | config_value = config.get("RabbitMq", config_key, fallback="") if config.has_section('RabbitMq') else ""
58 | if config_value and str(config_value).strip():
59 | print(f" {config_key}: 📄 config.ini -> '{config_value}'")
60 | return config_value
61 | else:
62 | message_value = rabbit_cfg.get(message_key, default_value)
63 | print(f" {config_key}: 📡 SocketIO -> '{message_value}' (config vacío)")
64 | return message_value
65 |
66 | # Aplicar la lógica
67 | host = get_config_or_message_value("host", "host")
68 | port_str = get_config_or_message_value("port", "port", "5672")
69 | user = get_config_or_message_value("user", "user", "guest")
70 | pwd = get_config_or_message_value("password", "password", "guest")
71 | vhost = get_config_or_message_value("vhost", "vhost", "/")
72 | queue = get_config_or_message_value("queue", "queue", "")
73 |
74 | # Convertir puerto a entero
75 | try:
76 | port = int(port_str)
77 | except (ValueError, TypeError):
78 | print(f" ⚠️ Puerto inválido '{port_str}', usando 5672 por defecto")
79 | port = 5672
80 |
81 | print("\n🎯 CONFIGURACIÓN FINAL QUE USARÍA LA APLICACIÓN:")
82 | print("=" * 50)
83 | print(f" Host: {host}")
84 | print(f" Port: {port}")
85 | print(f" User: {user}")
86 | print(f" Password: {'*' * len(pwd) if pwd else '(vacío)'}")
87 | print(f" VHost: {vhost}")
88 | print(f" Queue: {queue}")
89 |
90 | print("\n✅ VENTAJAS DE LA NUEVA LÓGICA:")
91 | print(" • Config.ini siempre tiene prioridad (control local)")
92 | print(" • SocketIO solo completa campos vacíos en config.ini")
93 | print(" • Logs claros muestran el origen de cada valor")
94 | print(" • Facilita debugging y configuración manual")
95 |
96 | print("\n💡 PARA PROBAR:")
97 | print(" 1. Modifica valores en ~/.config/Fiscalberry/config.ini")
98 | print(" 2. Esos valores tendrán prioridad sobre cualquier mensaje SocketIO")
99 | print(" 3. Deja campos vacíos en config.ini para usar valores remotos")
100 |
101 | if __name__ == "__main__":
102 | demonstrate_priority_logic()
103 |
--------------------------------------------------------------------------------
/docs/logica_prioridad_rabbitmq.md:
--------------------------------------------------------------------------------
1 | # Lógica de Prioridad RabbitMQ: Config.ini vs SocketIO
2 |
3 | ## Resumen de la Implementación
4 |
5 | Hemos implementado una lógica de prioridad robusta en el archivo `process_handler.py` que garantiza que **cada parámetro** de configuración de RabbitMQ sigue la misma regla de prioridad:
6 |
7 | ### 🎯 Regla de Prioridad (por parámetro individual)
8 |
9 | Para cada campo (`host`, `port`, `user`, `password`, `vhost`, `queue`):
10 |
11 | 1. **Primera prioridad**: Valor en `config.ini` (si existe y no está vacío)
12 | 2. **Segunda prioridad**: Valor del mensaje SocketIO
13 | 3. **Tercera prioridad**: Valor por defecto
14 |
15 | ### 📄 Código Implementado
16 |
17 | ```python
18 | def get_config_or_message_value(config_key, message_key, default_value=""):
19 | """
20 | Obtiene valor del config.ini si existe y no está vacío, sino del mensaje SocketIO.
21 | Prioridad: config.ini -> mensaje SocketIO -> valor por defecto
22 | """
23 | config_value = self.config.get("RabbitMq", config_key, fallback="")
24 | if config_value and str(config_value).strip():
25 | logger.info(f"{config_key}: usando config.ini -> {config_value}")
26 | return config_value
27 | else:
28 | message_value = rabbit_cfg.get(message_key, default_value)
29 | logger.info(f"{config_key}: config.ini vacío, usando SocketIO -> {message_value}")
30 | return message_value
31 |
32 | # Aplicar la lógica a todos los parámetros
33 | host = get_config_or_message_value("host", "host")
34 | port_str = get_config_or_message_value("port", "port", "5672")
35 | user = get_config_or_message_value("user", "user", "guest")
36 | pwd = get_config_or_message_value("password", "password", "guest")
37 | vhost = get_config_or_message_value("vhost", "vhost", "/")
38 | queue = get_config_or_message_value("queue", "queue", "")
39 | ```
40 |
41 | ### ✅ Ventajas de la Nueva Lógica
42 |
43 | 1. **Control Local Total**: Los administradores pueden forzar cualquier valor específico en `config.ini`
44 | 2. **Fallback Inteligente**: Campos vacíos se completan automáticamente desde SocketIO
45 | 3. **Logs Transparentes**: Cada valor muestra claramente su origen
46 | 4. **Configuración Granular**: Cada parámetro puede tener diferente origen sin afectar a los demás
47 | 5. **Backwards Compatible**: Funciona con configuraciones existentes
48 |
49 | ### 🧪 Casos de Prueba Verificados
50 |
51 | #### Caso 1: Config.ini completo
52 | ```ini
53 | [RabbitMq]
54 | host = localhost
55 | port = 5672
56 | user = admin
57 | password = secret
58 | vhost = /prod
59 | queue = my_queue
60 | ```
61 | **Resultado**: Se usan TODOS los valores del `config.ini`, ignorando SocketIO.
62 |
63 | #### Caso 2: Config.ini parcial
64 | ```ini
65 | [RabbitMq]
66 | host = localhost
67 | port =
68 | user = admin
69 | password =
70 | vhost = /prod
71 | queue =
72 | ```
73 | **Resultado**:
74 | - `host`, `user`, `vhost` → del `config.ini`
75 | - `port`, `password`, `queue` → del mensaje SocketIO
76 |
77 | #### Caso 3: Config.ini vacío
78 | ```ini
79 | [RabbitMq]
80 | host =
81 | port =
82 | user =
83 | password =
84 | vhost =
85 | queue =
86 | ```
87 | **Resultado**: Se usan TODOS los valores del mensaje SocketIO.
88 |
89 | ### 🔧 Herramientas de Diagnóstico
90 |
91 | Se han creado scripts de prueba para verificar la lógica:
92 |
93 | 1. **`test_config_priority.py`**: Pruebas unitarias automatizadas
94 | 2. **`demo_priority_logic.py`**: Demostración interactiva con el `config.ini` real
95 | 3. **`rabbitmq_check.py`**: Diagnóstico completo de conectividad RabbitMQ
96 |
97 | ### 💡 Uso Práctico
98 |
99 | **Para forzar un valor específico:**
100 | ```bash
101 | # Editar config.ini
102 | vim ~/.config/Fiscalberry/config.ini
103 |
104 | # Establecer el valor deseado
105 | [RabbitMq]
106 | host = mi-servidor-rabbitmq.com
107 | ```
108 |
109 | **Para permitir configuración remota:**
110 | ```bash
111 | # Dejar campo vacío en config.ini
112 | [RabbitMq]
113 | host =
114 | # Este valor vendrá del mensaje SocketIO
115 | ```
116 |
117 | ### 🚀 Mejoras Incluidas
118 |
119 | 1. **Manejo de Errores Mejorado**: DNS, autenticación, red
120 | 2. **Backoff Exponencial**: Reintentos inteligentes
121 | 3. **Logging Detallado**: Origen claro de cada configuración
122 | 4. **Validación de Tipos**: Puerto convertido a entero con fallback
123 | 5. **Diagnósticos Automáticos**: Scripts para troubleshooting
124 |
125 | Esta implementación proporciona un control granular y transparente sobre la configuración de RabbitMQ, facilitando tanto la administración manual como la configuración automática remota.
126 |
--------------------------------------------------------------------------------
/docs/rabbitmq_error_handling.md:
--------------------------------------------------------------------------------
1 | # Mejoras en el manejo de errores de RabbitMQ
2 |
3 | ## Problemas identificados
4 |
5 | El error original mostraba:
6 | ```
7 | ERROR:pika.adapters.utils.selector_ioloop_adapter:Address resolution failed: gaierror(-2, 'Name or service not known')
8 | socket.gaierror: [Errno -2] Name or service not known
9 | ```
10 |
11 | Este error indicaba que el hostname 'rabbitmq' no se podía resolver, pero el manejo de errores era genérico y no proporcionaba información útil al usuario.
12 |
13 | ## Mejoras implementadas
14 |
15 | ### 1. Manejo específico de errores en `process_handler.py`
16 |
17 | - **Errores DNS**: Se detectan específicamente errores `socket.gaierror` y se proporcionan sugerencias claras
18 | - **Errores de autenticación**: Se capturan errores de `ProbableAuthenticationError` y `ProbableAccessDeniedError`
19 | - **Errores de conectividad**: Se distinguen errores de red vs errores de RabbitMQ
20 | - **Backoff exponencial**: Se implementa un sistema de reintentos inteligente que evita saturar el servidor
21 |
22 | ### 2. Verificación previa de conectividad
23 |
24 | Se añadió `_check_network_connectivity()` que:
25 | - Verifica resolución DNS antes de intentar conectar
26 | - Prueba la conectividad del puerto TCP
27 | - Falla rápido para evitar timeouts largos
28 |
29 | ### 3. Configuración mejorada de timeouts en `consumer.py`
30 |
31 | - `socket_timeout=10`: Timeout más corto para detectar errores de red rápidamente
32 | - `connection_attempts=1`: Solo un intento, el retry lo maneja process_handler
33 | - `retry_delay=1`: Delay corto entre intentos
34 |
35 | ### 4. Herramienta de diagnóstico
36 |
37 | Se creó `rabbitmq_check.py` para:
38 | - Diagnosticar problemas de DNS, red y RabbitMQ por separado
39 | - Proporcionar sugerencias específicas para cada tipo de error
40 | - Permitir pruebas rápidas sin tener que ejecutar toda la aplicación
41 |
42 | ## Uso de la herramienta de diagnóstico
43 |
44 | ```bash
45 | # Diagnóstico básico
46 | python src/fiscalberry/diagnostics/rabbitmq_check.py
47 |
48 | # Usar configuración específica
49 | python src/fiscalberry/diagnostics/rabbitmq_check.py --host localhost --port 5672
50 |
51 | # Usar configuración de Fiscalberry
52 | python src/fiscalberry/diagnostics/rabbitmq_check.py --from-config
53 | ```
54 |
55 | ## Comportamiento del nuevo sistema de reintentos
56 |
57 | 1. **Primeros 3 intentos**: Reintento cada 5 segundos
58 | 2. **Después del intento 3**: Backoff exponencial (10s, 20s, 40s, etc.)
59 | 3. **Máximo delay**: 300 segundos (5 minutos)
60 | 4. **Verificación de conectividad**: Se verifica DNS y puerto antes de cada intento RabbitMQ
61 |
62 | ## Mensajes de error mejorados
63 |
64 | En lugar de:
65 | ```
66 | ERROR:fiscalberry.common.rabbitmq.process_handler:Error en RabbitMQConsumer: [Errno -2] Name or service not known
67 | WARNING:fiscalberry.common.rabbitmq.process_handler:RabbitMQConsumer detenido; reintentando en 5 s...
68 | ```
69 |
70 | Ahora se muestra:
71 | ```
72 | ERROR:fiscalberry.common.rabbitmq.process_handler:Error de resolución DNS para 'rabbitmq': [Errno -2] Name or service not known
73 | ERROR:fiscalberry.common.rabbitmq.process_handler:Posibles soluciones:
74 | ERROR:fiscalberry.common.rabbitmq.process_handler:1. Verificar que el hostname 'rabbitmq' esté configurado correctamente
75 | ERROR:fiscalberry.common.rabbitmq.process_handler:2. Verificar conectividad de red
76 | ERROR:fiscalberry.common.rabbitmq.process_handler:3. Verificar que el servidor RabbitMQ esté ejecutándose
77 | ERROR:fiscalberry.common.rabbitmq.process_handler:4. Considerar usar una IP directa en lugar del hostname
78 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 1/3 - esperando 5s antes del siguiente intento...
79 | ```
80 |
81 | ## Soluciones comunes
82 |
83 | ### Error DNS (Name or service not known)
84 |
85 | 1. **Configurar /etc/hosts**:
86 | ```
87 | 127.0.0.1 rabbitmq
88 | ```
89 |
90 | 2. **Usar IP directa en configuración**:
91 | ```ini
92 | [RabbitMq]
93 | host = 127.0.0.1
94 | ```
95 |
96 | 3. **Verificar que RabbitMQ esté ejecutándose en Docker**:
97 | ```bash
98 | docker ps | grep rabbitmq
99 | ```
100 |
101 | ### Error de conexión (Connection refused)
102 |
103 | 1. Verificar que RabbitMQ esté ejecutándose
104 | 2. Verificar firewall
105 | 3. Verificar puerto correcto (por defecto 5672)
106 |
107 | ### Error de autenticación
108 |
109 | 1. Verificar usuario y contraseña
110 | 2. Verificar permisos en el vhost
111 | 3. Crear usuario si no existe:
112 | ```bash
113 | rabbitmqctl add_user myuser mypassword
114 | rabbitmqctl set_permissions -p / myuser ".*" ".*" ".*"
115 | ```
116 |
--------------------------------------------------------------------------------
/docs/mejoras_rabbitmq_resumen.md:
--------------------------------------------------------------------------------
1 | # Resumen de mejoras en el manejo de errores de RabbitMQ
2 |
3 | ## Archivos modificados
4 |
5 | ### 1. `src/fiscalberry/common/rabbitmq/process_handler.py`
6 | **Mejoras principales:**
7 | - ✅ Manejo específico de errores DNS (`socket.gaierror`)
8 | - ✅ Manejo específico de errores de autenticación y permisos
9 | - ✅ Sistema de backoff exponencial para reintentos
10 | - ✅ Verificación previa de conectividad de red
11 | - ✅ Mensajes de error descriptivos con sugerencias
12 | - ✅ Importación de módulos necesarios (`socket`, `pika.exceptions`)
13 |
14 | ### 2. `src/fiscalberry/common/rabbitmq/consumer.py`
15 | **Mejoras principales:**
16 | - ✅ Timeouts más cortos para detección rápida de errores
17 | - ✅ Configuración optimizada para fallar rápido
18 | - ✅ Reducción de intentos de reconexión automática
19 |
20 | ### 3. `src/fiscalberry/diagnostics/rabbitmq_check.py` (NUEVO)
21 | **Funcionalidades:**
22 | - ✅ Diagnóstico paso a paso (DNS → Red → RabbitMQ)
23 | - ✅ Sugerencias específicas para cada tipo de error
24 | - ✅ Integración con configuración de Fiscalberry
25 | - ✅ Herramienta CLI independiente
26 |
27 | ### 4. `src/fiscalberry/diagnostics/test_error_handling.py` (NUEVO)
28 | **Funcionalidades:**
29 | - ✅ Script de prueba para verificar mejoras
30 | - ✅ Simulación de errores DNS
31 | - ✅ Verificación del sistema de backoff
32 |
33 | ### 5. `docs/rabbitmq_error_handling.md` (NUEVO)
34 | **Contenido:**
35 | - ✅ Documentación completa de las mejoras
36 | - ✅ Guía de solución de problemas comunes
37 | - ✅ Ejemplos de uso de herramientas
38 |
39 | ## Comportamiento antes vs después
40 |
41 | ### ANTES (comportamiento original):
42 | ```
43 | ERROR:fiscalberry.common.rabbitmq.process_handler:Error en RabbitMQConsumer: [Errno -2] Name or service not known
44 | WARNING:fiscalberry.common.rabbitmq.process_handler:RabbitMQConsumer detenido; reintentando en 5 s...
45 | WARNING:fiscalberry.common.rabbitmq.process_handler:RabbitMQConsumer detenido; reintentando en 5 s...
46 | WARNING:fiscalberry.common.rabbitmq.process_handler:RabbitMQConsumer detenido; reintentando en 5 s...
47 | ...
48 | ```
49 | - ❌ Error genérico sin contexto
50 | - ❌ Reintentos cada 5 segundos indefinidamente
51 | - ❌ Sin información sobre posibles soluciones
52 | - ❌ Sin diferenciación entre tipos de error
53 |
54 | ### DESPUÉS (comportamiento mejorado):
55 | ```
56 | ERROR:fiscalberry.common.rabbitmq.process_handler:Error de resolución DNS para 'rabbitmq': [Errno -2] Name or service not known
57 | ERROR:fiscalberry.common.rabbitmq.process_handler:Posibles soluciones:
58 | ERROR:fiscalberry.common.rabbitmq.process_handler:1. Verificar que el hostname 'rabbitmq' esté configurado correctamente
59 | ERROR:fiscalberry.common.rabbitmq.process_handler:2. Verificar conectividad de red
60 | ERROR:fiscalberry.common.rabbitmq.process_handler:3. Verificar que el servidor RabbitMQ esté ejecutándose
61 | ERROR:fiscalberry.common.rabbitmq.process_handler:4. Considerar usar una IP directa en lugar del hostname
62 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 1/3 - esperando 5s antes del siguiente intento...
63 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 2/3 - esperando 5s antes del siguiente intento...
64 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 3/3 - esperando 5s antes del siguiente intento...
65 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 4 con backoff exponencial - esperando 10s antes del siguiente intento...
66 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 5 con backoff exponencial - esperando 20s antes del siguiente intento...
67 | ...
68 | ```
69 | - ✅ Error específico con identificación clara del problema
70 | - ✅ Sugerencias concretas para solucionar el error
71 | - ✅ Sistema de backoff exponencial inteligente
72 | - ✅ Diferenciación entre tipos de error (DNS, red, autenticación, etc.)
73 |
74 | ## Herramientas de diagnóstico
75 |
76 | ### Uso básico:
77 | ```bash
78 | python src/fiscalberry/diagnostics/rabbitmq_check.py
79 | ```
80 |
81 | ### Con configuración específica:
82 | ```bash
83 | python src/fiscalberry/diagnostics/rabbitmq_check.py --host localhost --port 5672 --user myuser --password mypass
84 | ```
85 |
86 | ### Usando configuración de Fiscalberry:
87 | ```bash
88 | python src/fiscalberry/diagnostics/rabbitmq_check.py --from-config
89 | ```
90 |
91 | ## Beneficios principales
92 |
93 | 1. **Diagnóstico más rápido**: Los errores se identifican y categorizan inmediatamente
94 | 2. **Menor carga en el sistema**: Backoff exponencial evita spam de reintentos
95 | 3. **Mejor experiencia del usuario**: Mensajes claros con sugerencias específicas
96 | 4. **Herramientas de depuración**: Scripts independientes para diagnosticar problemas
97 | 5. **Documentación completa**: Guías paso a paso para solucionar problemas
98 |
99 | ## Casos de uso cubiertos
100 |
101 | - ✅ Error DNS (hostname no encontrado)
102 | - ✅ Error de conectividad de red (puerto cerrado/firewall)
103 | - ✅ Error de autenticación (usuario/contraseña incorrectos)
104 | - ✅ Error de permisos (acceso denegado al vhost)
105 | - ✅ Error de configuración de RabbitMQ
106 | - ✅ Timeouts de conexión
107 | - ✅ Servidor RabbitMQ no disponible
108 |
--------------------------------------------------------------------------------
/src/fiscalberry/ui/old_fiscalberry_app.py:
--------------------------------------------------------------------------------
1 | import os
2 | from kivy.app import App
3 | from kivy.uix.label import Label
4 | from kivy.uix.scrollview import ScrollView
5 | from kivy.uix.boxlayout import BoxLayout
6 | from kivy.uix.image import Image
7 | from kivy.clock import Clock
8 | from kivy.properties import StringProperty, BooleanProperty
9 | from jnius import autoclass
10 | from fiscalberry.common.Configberry import Configberry
11 | from twisted.internet.protocol import Protocol, ClientFactory
12 | from twisted.internet import reactor
13 |
14 | # Importo el módulo que se encarga de la comunicación con el servidor
15 | from fiscalberry.common.fiscalberry_logger import getLogger
16 |
17 | logger = getLogger()
18 |
19 | SERVICE_NAME = u'{packagename}.Service{servicename}'.format(
20 | packagename=u'com.paxapos.fiscalberry',
21 | servicename=u'Fiscalberryservice'
22 | )
23 |
24 |
25 | configberry = Configberry()
26 | sio = None
27 |
28 | class LogWidget(ScrollView):
29 |
30 | logs = []
31 |
32 |
33 | def __init__(self, **kwargs):
34 | super(LogWidget, self).__init__(**kwargs)
35 |
36 | self.background_color = (1, 0.75, 0.8, 1) # Set pink background color
37 |
38 | def add_log(self, log):
39 | self.logs.append(log)
40 | self.update_logs()
41 |
42 | def update_logs(self):
43 | self.clear_widgets()
44 | for log in self.logs:
45 | self.add_widget(Label(text=log, size_hint_y=None, height=40))
46 |
47 |
48 | class ConnectedImage(Image):
49 | source = "assets/disconnected.png"
50 |
51 |
52 |
53 | class MainLayout(BoxLayout):
54 |
55 | def __init__(self, **kwargs):
56 | super(MainLayout, self).__init__(**kwargs)
57 | self.log_widget = LogWidget(size_hint=(1, 0.5))
58 | self.add_widget(self.log_widget)
59 |
60 |
61 | class FiscalberryApp(App):
62 | assetpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
63 |
64 | background_image = StringProperty(f"{assetpath}/assets/bg.jpg")
65 | logo_image = StringProperty(f"{assetpath}/assets/fiscalberry.png")
66 | disconnected_image = StringProperty(f"{assetpath}/assets/disconnected.png")
67 | connected_image = StringProperty(f"{assetpath}/assets/connected.png")
68 |
69 | connected: bool = BooleanProperty(False)
70 |
71 | fiscalberry_sio = None
72 |
73 | sio = None
74 |
75 | def __init__(self, **kwargs):
76 | super(FiscalberryApp, self).__init__(**kwargs)
77 |
78 | # load APP config
79 | serverUrl = configberry.config.get("SERVIDOR", "sio_host", fallback="")
80 | self.sioServerUrl = serverUrl if serverUrl else self.sioServerUrl
81 |
82 | # Crear la fábrica del cliente y conectar al socket
83 | factory = AppInterConFactory(self)
84 | reactor.connectUNIX("/data/data/com.paxapos.fiscalberry/files/fiscalberry_socket", factory)
85 |
86 | def start_service(self, finishing=False):
87 | service = autoclass(SERVICE_NAME)
88 | mActivity = autoclass(u'org.kivy.android.PythonActivity').mActivity
89 | argument = ''
90 | if finishing:
91 | argument = '{"stop_service": 1}'
92 | service.start(mActivity, argument)
93 | if finishing:
94 | self.service = None
95 | else:
96 | self.service = service
97 |
98 | self.connected = True
99 |
100 | def stop_service(self):
101 | service = autoclass(SERVICE_NAME)
102 | mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
103 | service.stop(mActivity)
104 | self.start_service(finishing=True)
105 | self.connected = False
106 |
107 | def start_fiscaberry_service():
108 | service = autoclass('com.paxapos.fiscalberry.ServiceFiscalberryservice')
109 | mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
110 | argument = ''
111 | service.start(mActivity, argument)
112 |
113 | def build(self):
114 | self.root = MainLayout()
115 | self.log_widget = self.root.log_widget
116 | return self.root
117 |
118 | def add_log(self, text):
119 | self.log_widget.add_log(text)
120 |
121 |
122 | def store_new_host(self, value):
123 | host = value
124 | logger.info(f"FiscalberryApp: vino para guardar sio_host:: {host} antes estaba {self.sioServerUrl}")
125 | if host != self.sioServerUrl:
126 | logger.info(f"FiscalberryApp: se guarda nuevo sio_host:: {host}")
127 | configberry.writeKeyForSection("SERVIDOR", "sio_host", host)
128 |
129 |
130 | def on_resume(self):
131 | if self.service:
132 | self.start_service()
133 |
134 | def on_start(self):
135 | """
136 | This function is executed when the application starts.
137 | It is a Kivy function.
138 | """
139 |
140 | # Iniciar con el ícono rojo por defecto
141 | self.start_service()
142 | logger.info("FiscalberryApp: Iniciando la aplicación")
143 |
144 |
145 | def on_stop(self):
146 | """
147 | This function is executed when the application is closed.
148 | It is a Kivy function.
149 | """
150 | self.stop_service()
151 | logger.info("FiscalberryApp: Deteniendo la aplicación")
152 |
153 |
154 |
--------------------------------------------------------------------------------
/src/fiscalberryservice/android.py:
--------------------------------------------------------------------------------
1 | from time import sleep
2 | import os
3 | from jnius import autoclass
4 |
5 | # -*- coding: utf-8 -*-
6 |
7 | """
8 | Basic structure for an Android background service using Python.
9 | This typically requires a framework like Kivy (with python-for-android)
10 | or Chaquopy to bridge Python with the Android OS.
11 | """
12 |
13 |
14 | # Using jnius (common with Kivy/python-for-android) to interact with Android APIs
15 | try:
16 | # Example: Get access to the service context if running under PythonService
17 | # PythonService = autoclass('org.kivy.android.PythonService')
18 | # service = PythonService.mService
19 | pass
20 | except ImportError:
21 | print("jnius not found. Android-specific features may not work.")
22 | autoclass = None
23 | service = None
24 |
25 | def run_service_logic():
26 | """Contains the main logic for the background service."""
27 | print("Android Service: Starting background logic.")
28 |
29 | # Example: Retrieve an argument passed when starting the service
30 | # This environment variable is often set by the service starter (e.g., Kivy's PythonService)
31 | service_argument = os.environ.get('PYTHON_SERVICE_ARGUMENT', 'No argument provided')
32 | print(f"Android Service: Received argument: {service_argument}")
33 |
34 | # Main service loop
35 | counter = 0
36 | while True:
37 | print(f"Android Service: Running... Loop count: {counter}")
38 |
39 | # --- Add your background tasks here ---
40 | # Examples:
41 | # - Polling a server for updates
42 | # - Processing data in the background
43 | # - Interacting with hardware (requires specific permissions and APIs)
44 | # - Sending notifications (requires Android API calls via jnius/plyer)
45 |
46 | # Example: Show a Toast notification (requires context and jnius)
47 | # if autoclass and service:
48 | # try:
49 | # Toast = autoclass('android.widget.Toast')
50 | # CharSequence = autoclass('java.lang.CharSequence')
51 | # context = service.getApplicationContext()
52 | # if context:
53 | # def show_toast():
54 | # text = CharSequence(f"Service running {counter}")
55 | # toast = Toast.makeText(context, text, Toast.LENGTH_SHORT)
56 | # toast.show()
57 | # # Toasts must be shown on the UI thread
58 | # PythonActivity = autoclass('org.kivy.android.PythonActivity')
59 | # PythonActivity.mActivity.runOnUiThread(show_toast)
60 | # except Exception as e:
61 | # print(f"Android Service: Error showing toast - {e}")
62 |
63 | # Sleep for a period before the next iteration
64 | sleep(15) # Sleep for 15 seconds
65 | counter += 1
66 |
67 | # Add conditions to stop the service if necessary,
68 | # although services are often designed to run until explicitly stopped.
69 | # For example:
70 | # if should_stop_service():
71 | # print("Android Service: Stopping condition met.")
72 | # break
73 |
74 | print("Android Service: Background logic finished.")
75 |
76 | if __name__ == "__main__":
77 | # This block is executed when the script is run directly.
78 | # When launched as an Android service by the framework (e.g., Kivy),
79 | # the framework typically imports and runs this script, potentially
80 | # calling a specific function or just executing the module level code.
81 | # The `run_service_logic()` function contains the core tasks.
82 | run_service_logic()
83 |
84 | # --- Notes on Deployment as an Android Service ---
85 | #
86 | # 1. **Framework:** Use Kivy (with buildozer) or Chaquopy.
87 | # 2. **Configuration (Buildozer Example):**
88 | # In `buildozer.spec`, define the service:
89 | # ```
90 | # services = YourServiceName:./src/fiscalberryservice/android.py
91 | # ```
92 | # Replace `YourServiceName` and adjust the path.
93 | # 3. **Starting the Service:**
94 | # Start it from your main app (e.g., Kivy `main.py`) using Android Intents via jnius:
95 | # ```python
96 | # from jnius import autoclass
97 | # import os
98 | #
99 | # PythonActivity = autoclass('org.kivy.android.PythonActivity')
100 | # activity = PythonActivity.mActivity
101 | # Intent = autoclass('android.content.Intent')
102 | #
103 | # service_name = 'YourServiceName' # Must match buildozer.spec
104 | # service_class_name = f'{activity.getPackageName()}.Service{service_name}'
105 | #
106 | # intent = Intent()
107 | # intent.setClassName(activity, service_class_name)
108 | #
109 | # # Pass data to the service (optional)
110 | # argument = "Data for the service"
111 | # intent.putExtra('PYTHON_SERVICE_ARGUMENT', argument)
112 | # os.environ['PYTHON_SERVICE_ARGUMENT'] = argument # Set env var too
113 | #
114 | # activity.startService(intent)
115 | # ```
116 | # 4. **Permissions:** Add necessary permissions to `buildozer.spec`:
117 | # ```
118 | # android.permissions = INTERNET, FOREGROUND_SERVICE, ...
119 | # ```
120 | # `FOREGROUND_SERVICE` is often required for long-running tasks on newer Android versions.
121 | # 5. **Foreground Service:** For long-running tasks, consider running as a foreground service
122 | # to prevent Android from killing it. This requires showing a persistent notification.
123 | # You'll need more jnius code to create the notification channel and notification itself.
--------------------------------------------------------------------------------
/src/test.py:
--------------------------------------------------------------------------------
1 |
2 | from fiscalberry.common.ComandosHandler import ComandosHandler
3 | import sys
4 | from fiscalberry.common.printer_detector import listar_impresoras
5 |
6 | '''
7 | si ejecuto el script sin argumentos que me de un listado de opciones y ayuda de uso
8 | estre ecript se ejecuta con 2 argumentos, el primero es es el tipo de comando a enviar y
9 | las opciones son
10 | - remito1
11 | - remito2
12 | - facturaA
13 | - facturaB
14 | - cierreCaja
15 | - comanda
16 |
17 | y el segundo parametro es el nombre de la impresora a la que se le envia el comando, es opcional
18 | si no se especifica se envia a la impresora Dummy
19 | '''
20 |
21 |
22 |
23 | # Ejemplo de uso
24 | impresoras = listar_impresoras()
25 | print("Impresoras instaladas:", impresoras)
26 |
27 | class ComandosEnum:
28 | REMITO1 = "remito1"
29 | REMITO2 = "remito2"
30 | FACTURAA = "facturaA"
31 | FACTURAB = "facturaB"
32 | CIERRECAJA = "cierreCaja"
33 | COMANDA1 = "comanda1"
34 |
35 |
36 |
37 | if len(sys.argv) == 1:
38 | print("Listado de opciones y ayuda de uso:")
39 | for i, comando in enumerate(ComandosEnum.__dict__.keys()):
40 | if not comando.startswith("__"):
41 | print(f"{i+1}. {comando}")
42 | sys.exit()
43 |
44 | comando = sys.argv[1]
45 |
46 | #si no se especifica la comandera, se envia a la comandera Dummy, si se especifica
47 | #se envia a la comandera especificada
48 | if len(sys.argv) > 2:
49 | comandera = sys.argv[2]
50 | else:
51 | comandera = 'Dummy'
52 |
53 |
54 |
55 | comandosDisponibles = {}
56 | comandosDisponibles[ComandosEnum.REMITO1] = {'printRemito': {
57 | 'encabezado': {'imprimir_fecha_remito': ''},
58 | 'items': [
59 | {'alic_iva': '21.00', 'qty': 6, 'ds': "None", 'importe': 50, 'order': '0'},
60 | ],
61 | 'setTrailer': [
62 | 'Mozo: Ludmila', 'Mesa: 33'
63 | ]},
64 | 'printerName': 'File'
65 | }
66 |
67 | comandosDisponibles[ComandosEnum.REMITO2] = {'printRemito': {'encabezado': {'imprimir_fecha_remito': ''}, 'items': [
68 | {'alic_iva': '21.00', 'qty': '1', 'ds': 'Producto', 'importe': 1, 'order': None},
69 | {'alic_iva': '21.00', 'qty': '1', 'ds': None, 'importe': '2200', 'order': None}],
70 | 'setTrailer': ['Mozo: Cena Show', 'Mesa: prueba']}, 'printerName': 'dummy'}
71 |
72 | comandosDisponibles[ComandosEnum.FACTURAB] = {'printFacturaElectronica': {'encabezado': {'nombre_comercio': 'Paxapoga', 'razon_social': 'Riotorno SRL', 'cuit_empresa': '30715582593', 'domicilio_comercial': 'Beruti 4643', 'tipo_responsable': 'Resp. Inscripto', 'inicio_actividades': '', 'tipo_comprobante': '"B"', 'tipo_comprobante_codigo': '006', 'numero_comprobante': '0010-00011795', 'fecha_comprobante': '2024-08-29', 'documento_cliente': '0', 'nombre_cliente': '', 'domicilio_cliente': '', 'nombre_tipo_documento': 'Sin identificar', 'cae': '74353157058451', 'cae_vto': '2024-09-08', 'importe_total': '1.00', 'importe_neto': '4876.86', 'importe_iva': '1024.14', 'moneda': 'PES', 'ctz': 1, 'tipoDocRec': 99, 'tipoCodAut': 'E'}, 'items': [{'alic_iva': 0.21, 'importe': 1, 'ds': 'Producto', 'qty': 1}], 'pagos': [], 'setTrailer': ['Mozo: Cena Show', 'Mesa: prueba']}, 'printerName': comandera}
73 |
74 | comandosDisponibles[ComandosEnum.FACTURAA] = {"printFacturaElectronica": {"encabezado": {"nombre_comercio": "Riotorno SRL", "razon_social": "Riotorno SRL", "cuit_empresa": "30715582593", "domicilio_comercial": "una calle 2232", "tipo_responsable": "Resp. Inscripto", "inicio_actividades": None, "tipo_comprobante": '"A"', "tipo_comprobante_codigo": "001", "numero_comprobante": "0010-00000016", "fecha_comprobante": "2024-09-02", "documento_cliente": "33709585229", "nombre_cliente": "Google Arg. SRL", "domicilio_cliente": "", "nombre_tipo_documento": "CUIT", "cae": "74363194522383", "cae_vto": "2024-09-12", "importe_total": "2000.00", "importe_neto": "1652.88", "importe_iva": "347.12", "moneda": "PES", "ctz": 1, "tipoDocRec": 99, "tipoCodAut": "E"}, "items": [{"alic_iva": 164.88, "importe": 950, "ds": "DAIQUIRI S/A MARACUYA", "qty": 1}, {"alic_iva": 164.88, "importe": 950, "ds": "DAIQUIRI S/A FRUTILLA", "qty": 1}, {"alic_iva": 8.68, "importe": 50, "ds": "producto en otra impresora", "qty": 2}], "pagos": [], "setTrailer": ["Mozo: Riotorno SRL", "Mesa: 2"]}, "printerName": comandera}
75 |
76 | comandosDisponibles[ComandosEnum.COMANDA1] = {'printComanda': {'comanda': {'id': '629', 'created': '2024-09-02 08:48:20', 'observacion': 'observacion general', 'entradas': [{'nombre': 'cerveza con variante', 'cant': '1', 'sabores': ['(1) IMPERIAL RUBIA TIRADA ', '(3) Imperial Rubia Tirada x Lt']}], 'platos': [{'nombre': 'OLD FASHIONED', 'cant': '2'}, {'nombre': 'BOULEVARDIER', 'cant': '1'}, {'nombre': 'WHITE RUSSIAN', 'cant': '1'}, {'nombre': 'PISCO SOUR', 'cant': '1'}, {'nombre': 'MOJITO DE FRUTOS ROJOS', 'cant': '1'}, {'nombre': 'DAIQUIRI S/A FRUTILLA', 'cant': '1'}, {'nombre': 'DAIQUIRI S/A MARACUYA', 'cant': '1', 'observacion': 'un detalle del producto'}, {'nombre': 'producto en otra impresora', 'cant': '2'}]}, 'setTrailer': [' ', 'Mozo: Riotorno SRL', 'Mesa: 112', ' ']}, 'printerName': comandera}
77 |
78 |
79 |
80 | comandoHandler = ComandosHandler()
81 |
82 |
83 | # si comando tiene el nombre all, ejecutar todos
84 | if comando == "all":
85 | for comando in comandosDisponibles:
86 | comandoHandler.send_command(comandosDisponibles.get(comando))
87 | sys.exit()
88 |
89 |
90 | # si comando existe en el listado de comandos disponibles, envio el comando a
91 | # la impresora
92 | if comando in comandosDisponibles:
93 | comandoHandler.send_command(comandosDisponibles.get(comando))
94 | else:
95 | print(f"El comando {comando} no existe en la lista de comandos disponibles")
96 | sys.exit()
97 |
98 |
--------------------------------------------------------------------------------
/test_config_priority.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Script de prueba para verificar la lógica de prioridad config.ini vs SocketIO
4 | en la configuración de RabbitMQ.
5 | """
6 |
7 | import configparser
8 | import tempfile
9 | import os
10 | import sys
11 | sys.path.insert(0, 'src')
12 |
13 | def simulate_config_priority_logic():
14 | """Simula la lógica de prioridad sin dependencias externas"""
15 |
16 | # Crear un config temporal para las pruebas
17 | config = configparser.ConfigParser()
18 | config.add_section('RabbitMq')
19 |
20 | # CASO 1: Config.ini tiene valores, mensaje SocketIO también
21 | print("=== CASO 1: Config.ini con valores, SocketIO con valores ===")
22 | config.set('RabbitMq', 'host', 'localhost')
23 | config.set('RabbitMq', 'port', '5672')
24 | config.set('RabbitMq', 'user', 'admin')
25 | config.set('RabbitMq', 'password', 'secret')
26 | config.set('RabbitMq', 'vhost', '/prod')
27 | config.set('RabbitMq', 'queue', 'config_queue')
28 |
29 | # Datos del mensaje SocketIO (estos deberían ser ignorados)
30 | rabbit_cfg = {
31 | 'host': 'rabbitmq.server.com',
32 | 'port': '5673',
33 | 'user': 'socketio_user',
34 | 'password': 'socketio_pass',
35 | 'vhost': '/socketio',
36 | 'queue': 'socketio_queue'
37 | }
38 |
39 | def get_config_or_message_value(config_key, message_key, default_value=""):
40 | """Lógica exacta del código real"""
41 | config_value = config.get("RabbitMq", config_key, fallback="")
42 | if config_value and str(config_value).strip():
43 | print(f" {config_key}: usando config.ini -> {config_value}")
44 | return config_value
45 | else:
46 | message_value = rabbit_cfg.get(message_key, default_value)
47 | print(f" {config_key}: config.ini vacío, usando SocketIO -> {message_value}")
48 | return message_value
49 |
50 | host = get_config_or_message_value("host", "host")
51 | port_str = get_config_or_message_value("port", "port", "5672")
52 | user = get_config_or_message_value("user", "user", "guest")
53 | pwd = get_config_or_message_value("password", "password", "guest")
54 | vhost = get_config_or_message_value("vhost", "vhost", "/")
55 | queue = get_config_or_message_value("queue", "queue", "")
56 |
57 | print(f"Resultado final: host={host}, port={port_str}, user={user}, vhost={vhost}, queue={queue}")
58 | assert host == 'localhost', f"Expected localhost, got {host}"
59 | assert port_str == '5672', f"Expected 5672, got {port_str}"
60 | assert user == 'admin', f"Expected admin, got {user}"
61 | assert pwd == 'secret', f"Expected secret, got {pwd}"
62 | assert vhost == '/prod', f"Expected /prod, got {vhost}"
63 | assert queue == 'config_queue', f"Expected config_queue, got {queue}"
64 | print("✓ CASO 1 CORRECTO: Se usaron todos los valores del config.ini\n")
65 |
66 | # CASO 2: Config.ini parcialmente vacío
67 | print("=== CASO 2: Config.ini parcialmente vacío ===")
68 | config.set('RabbitMq', 'host', 'localhost') # Tiene valor
69 | config.set('RabbitMq', 'port', '') # Vacío
70 | config.set('RabbitMq', 'user', 'admin') # Tiene valor
71 | config.set('RabbitMq', 'password', '') # Vacío
72 | config.set('RabbitMq', 'vhost', '/prod') # Tiene valor
73 | config.set('RabbitMq', 'queue', '') # Vacío
74 |
75 | host = get_config_or_message_value("host", "host")
76 | port_str = get_config_or_message_value("port", "port", "5672")
77 | user = get_config_or_message_value("user", "user", "guest")
78 | pwd = get_config_or_message_value("password", "password", "guest")
79 | vhost = get_config_or_message_value("vhost", "vhost", "/")
80 | queue = get_config_or_message_value("queue", "queue", "")
81 |
82 | print(f"Resultado final: host={host}, port={port_str}, user={user}, vhost={vhost}, queue={queue}")
83 | assert host == 'localhost', f"Expected localhost, got {host}"
84 | assert port_str == '5673', f"Expected 5673 (from SocketIO), got {port_str}"
85 | assert user == 'admin', f"Expected admin, got {user}"
86 | assert pwd == 'socketio_pass', f"Expected socketio_pass (from SocketIO), got {pwd}"
87 | assert vhost == '/prod', f"Expected /prod, got {vhost}"
88 | assert queue == 'socketio_queue', f"Expected socketio_queue (from SocketIO), got {queue}"
89 | print("✓ CASO 2 CORRECTO: Se usó config.ini donde tenía valores, SocketIO donde estaba vacío\n")
90 |
91 | # CASO 3: Config.ini completamente vacío
92 | print("=== CASO 3: Config.ini completamente vacío ===")
93 | config.set('RabbitMq', 'host', '')
94 | config.set('RabbitMq', 'port', '')
95 | config.set('RabbitMq', 'user', '')
96 | config.set('RabbitMq', 'password', '')
97 | config.set('RabbitMq', 'vhost', '')
98 | config.set('RabbitMq', 'queue', '')
99 |
100 | host = get_config_or_message_value("host", "host")
101 | port_str = get_config_or_message_value("port", "port", "5672")
102 | user = get_config_or_message_value("user", "user", "guest")
103 | pwd = get_config_or_message_value("password", "password", "guest")
104 | vhost = get_config_or_message_value("vhost", "vhost", "/")
105 | queue = get_config_or_message_value("queue", "queue", "")
106 |
107 | print(f"Resultado final: host={host}, port={port_str}, user={user}, vhost={vhost}, queue={queue}")
108 | assert host == 'rabbitmq.server.com', f"Expected rabbitmq.server.com, got {host}"
109 | assert port_str == '5673', f"Expected 5673, got {port_str}"
110 | assert user == 'socketio_user', f"Expected socketio_user, got {user}"
111 | assert pwd == 'socketio_pass', f"Expected socketio_pass, got {pwd}"
112 | assert vhost == '/socketio', f"Expected /socketio, got {vhost}"
113 | assert queue == 'socketio_queue', f"Expected socketio_queue, got {queue}"
114 | print("✓ CASO 3 CORRECTO: Se usaron todos los valores del SocketIO\n")
115 |
116 | print("🎉 TODAS LAS PRUEBAS PASARON - La lógica de prioridad funciona correctamente")
117 |
118 | if __name__ == "__main__":
119 | simulate_config_priority_logic()
120 |
--------------------------------------------------------------------------------
/src/fiscalberry/diagnostics/rabbitmq_check.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Utilidad para diagnosticar problemas de conexión con RabbitMQ.
4 | Ayuda a identificar si el problema es DNS, conectividad de red, o configuración de RabbitMQ.
5 | """
6 |
7 | import socket
8 | import pika
9 | import sys
10 | import argparse
11 | from typing import Dict, Any
12 |
13 | def check_dns_resolution(host: str) -> bool:
14 | """Verifica si el hostname se puede resolver."""
15 | try:
16 | result = socket.getaddrinfo(host, None)
17 | print(f"✓ DNS: {host} se resuelve a {result[0][4][0]}")
18 | return True
19 | except socket.gaierror as e:
20 | print(f"✗ DNS: Error resolviendo {host}: {e}")
21 | print(" Sugerencias:")
22 | print(" - Verificar /etc/hosts")
23 | print(" - Verificar configuración DNS")
24 | print(" - Usar IP directa en lugar del hostname")
25 | return False
26 |
27 | def check_port_connectivity(host: str, port: int, timeout: int = 5) -> bool:
28 | """Verifica si el puerto está abierto."""
29 | try:
30 | with socket.create_connection((host, port), timeout=timeout):
31 | print(f"✓ Red: Puerto {port} en {host} está abierto")
32 | return True
33 | except socket.timeout:
34 | print(f"✗ Red: Timeout conectando a {host}:{port}")
35 | return False
36 | except ConnectionRefusedError:
37 | print(f"✗ Red: Conexión rechazada en {host}:{port}")
38 | print(" Sugerencias:")
39 | print(" - Verificar que RabbitMQ esté ejecutándose")
40 | print(" - Verificar firewall")
41 | return False
42 | except Exception as e:
43 | print(f"✗ Red: Error conectando a {host}:{port}: {e}")
44 | return False
45 |
46 | def check_rabbitmq_connection(host: str, port: int, user: str, password: str, vhost: str = "/") -> bool:
47 | """Verifica la conexión completa a RabbitMQ."""
48 | try:
49 | params = pika.ConnectionParameters(
50 | host=host,
51 | port=port,
52 | virtual_host=vhost,
53 | credentials=pika.PlainCredentials(user, password),
54 | socket_timeout=10,
55 | connection_attempts=1
56 | )
57 |
58 | connection = pika.BlockingConnection(params)
59 | channel = connection.channel()
60 |
61 | # Verificar que podemos declarar un exchange temporal
62 | test_exchange = "test_connection_check"
63 | channel.exchange_declare(exchange=test_exchange, exchange_type='direct', durable=False, auto_delete=True)
64 | channel.exchange_delete(exchange=test_exchange)
65 |
66 | connection.close()
67 | print(f"✓ RabbitMQ: Conexión exitosa a {host}:{port}")
68 | print(f" Usuario: {user}")
69 | print(f" VHost: {vhost}")
70 | return True
71 |
72 | except pika.exceptions.ProbableAuthenticationError as e:
73 | print(f"✗ RabbitMQ: Error de autenticación: {e}")
74 | print(" Sugerencias:")
75 | print(f" - Verificar usuario '{user}' existe")
76 | print(f" - Verificar contraseña")
77 | print(f" - Verificar permisos en vhost '{vhost}'")
78 | return False
79 | except pika.exceptions.ProbableAccessDeniedError as e:
80 | print(f"✗ RabbitMQ: Acceso denegado: {e}")
81 | print(" Sugerencias:")
82 | print(f" - Verificar permisos del usuario '{user}' en vhost '{vhost}'")
83 | return False
84 | except pika.exceptions.AMQPConnectionError as e:
85 | print(f"✗ RabbitMQ: Error de conexión AMQP: {e}")
86 | return False
87 | except Exception as e:
88 | print(f"✗ RabbitMQ: Error inesperado: {e}")
89 | return False
90 |
91 | def get_config_from_file(config_file: str = None) -> Dict[str, Any]:
92 | """Intenta leer la configuración del archivo config.ini de Fiscalberry."""
93 | try:
94 | from fiscalberry.common.Configberry import Configberry
95 | config = Configberry()
96 | return {
97 | 'host': config.get("RabbitMq", "host"),
98 | 'port': int(config.get("RabbitMq", "port")),
99 | 'user': config.get("RabbitMq", "user"),
100 | 'password': config.get("RabbitMq", "password"),
101 | 'vhost': config.get("RabbitMq", "vhost", "/")
102 | }
103 | except Exception as e:
104 | print(f"No se pudo leer configuración de Fiscalberry: {e}")
105 | return {}
106 |
107 | def main():
108 | parser = argparse.ArgumentParser(description='Diagnosticar conexión RabbitMQ')
109 | parser.add_argument('--host', default='rabbitmq', help='Hostname de RabbitMQ')
110 | parser.add_argument('--port', type=int, default=5672, help='Puerto de RabbitMQ')
111 | parser.add_argument('--user', default='guest', help='Usuario de RabbitMQ')
112 | parser.add_argument('--password', default='guest', help='Contraseña de RabbitMQ')
113 | parser.add_argument('--vhost', default='/', help='Virtual host de RabbitMQ')
114 | parser.add_argument('--from-config', action='store_true', help='Usar configuración de Fiscalberry')
115 |
116 | args = parser.parse_args()
117 |
118 | print("=== Diagnóstico de conexión RabbitMQ ===\n")
119 |
120 | # Si se especifica --from-config, intentar leer del archivo de configuración
121 | if args.from_config:
122 | config = get_config_from_file()
123 | if config:
124 | args.host = config['host']
125 | args.port = config['port']
126 | args.user = config['user']
127 | args.password = config['password']
128 | args.vhost = config['vhost']
129 | print(f"Usando configuración de Fiscalberry:")
130 | print(f" Host: {args.host}")
131 | print(f" Puerto: {args.port}")
132 | print(f" Usuario: {args.user}")
133 | print(f" VHost: {args.vhost}\n")
134 |
135 | print(f"Probando conexión a {args.host}:{args.port}...\n")
136 |
137 | # 1. Verificar resolución DNS
138 | dns_ok = check_dns_resolution(args.host)
139 |
140 | # 2. Verificar conectividad de puerto
141 | port_ok = False
142 | if dns_ok:
143 | port_ok = check_port_connectivity(args.host, args.port)
144 |
145 | # 3. Verificar conexión RabbitMQ completa
146 | rabbitmq_ok = False
147 | if port_ok:
148 | rabbitmq_ok = check_rabbitmq_connection(args.host, args.port, args.user, args.password, args.vhost)
149 |
150 | print("\n=== Resumen ===")
151 | print(f"DNS: {'✓' if dns_ok else '✗'}")
152 | print(f"Red: {'✓' if port_ok else '✗'}")
153 | print(f"RabbitMQ: {'✓' if rabbitmq_ok else '✗'}")
154 |
155 | if rabbitmq_ok:
156 | print("\n🎉 Conexión exitosa! RabbitMQ está funcionando correctamente.")
157 | return 0
158 | else:
159 | print("\n❌ Hay problemas con la conexión. Revisar los errores anteriores.")
160 | return 1
161 |
162 | if __name__ == "__main__":
163 | sys.exit(main())
164 |
--------------------------------------------------------------------------------
/src/fiscalberry/common/fiscalberry_sio.py:
--------------------------------------------------------------------------------
1 | import threading
2 | import socketio
3 | import queue
4 | import os
5 | import time
6 | from fiscalberry.common.ComandosHandler import ComandosHandler, TraductorException
7 | from fiscalberry.common.fiscalberry_logger import getLogger
8 | from fiscalberry.common.Configberry import Configberry
9 | from fiscalberry.common.rabbitmq.process_handler import RabbitMQProcessHandler
10 |
11 |
12 | environment = os.getenv('ENVIRONMENT', 'production')
13 | sioLogger = True if environment == 'development' else False
14 | logger = getLogger()
15 |
16 | class FiscalberrySio:
17 | _instance = None
18 | _lock = threading.Lock()
19 |
20 | rabbitmq_thread = None
21 |
22 | def __new__(cls, *a, **k):
23 | with cls._lock:
24 | if not cls._instance:
25 | cls._instance = super().__new__(cls)
26 | cls._instance._initialized = False
27 | return cls._instance
28 |
29 | def __init__(self, server_url: str, uuid: str, namespaces='/paxaprinter', on_message=None):
30 | if self._initialized:
31 | return
32 | self.server_url = server_url
33 | self.uuid = uuid
34 | self.namespaces = namespaces
35 | self.on_message = on_message
36 | self.sio = socketio.Client(
37 | reconnection=True,
38 | reconnection_attempts=0,
39 | reconnection_delay=2,
40 | reconnection_delay_max=15,
41 | logger=True,
42 | engineio_logger=False,
43 | )
44 | self.stop_event = threading.Event()
45 | self.thread = None
46 | self.config = Configberry()
47 | self.message_queue = queue.Queue()
48 | self.rabbit_handler = RabbitMQProcessHandler()
49 | self._register_events()
50 | self._initialized = True
51 |
52 | def _register_events(self):
53 | ns = self.namespaces
54 |
55 | @self.sio.event(namespace=ns)
56 | def connect():
57 | logger.info(f"SIO connect, SID={self.sio.sid}")
58 |
59 | @self.sio.event(namespace=ns)
60 | def connect_error(err):
61 | logger.error(f"SIO connect error: {err}")
62 |
63 | @self.sio.event(namespace=ns)
64 | def disconnect():
65 | logger.info("SIO disconnect")
66 |
67 | @self.sio.event(namespace=ns)
68 | def error(err):
69 | logger.error(f"SIO error: {err}")
70 |
71 | @self.sio.event(namespace=ns)
72 | def start_sio():
73 | logger.info("SIO start_sio")
74 |
75 | @self.sio.event(namespace=ns)
76 | def adopt(data):
77 | """ eliminar de configberry la info de la seccion paxaprinter"""
78 | logger.info(f"SIO adopt: {data}")
79 | try:
80 | # Detener servicio de RabbitMQ
81 | self.rabbit_handler.stop()
82 | if self.config.delete_section("Paxaprinter"):
83 | logger.info("RabbitMQ service stopped and Paxaprinter section removed")
84 | except Exception as e:
85 | logger.error(f"adopt: {e}")
86 |
87 |
88 | @self.sio.event(namespace=ns)
89 | def message(data):
90 | logger.info(f"SIO message: {data}")
91 | if self.on_message:
92 | try:
93 | self.on_message(data)
94 | except Exception as e:
95 | logger.error(f"on_message callback: {e}")
96 |
97 | @self.sio.event(namespace=ns)
98 | def command(cfg: dict):
99 | logger.info(f"Vino evento command {cfg}")
100 |
101 |
102 |
103 | @self.sio.event(namespace=ns)
104 | def start_rabbit(cfg: dict):
105 | logger.info(f"start_rabbit: RabbitMQ {cfg}")
106 |
107 | # config + restart, pasamos la cola para tail -f
108 | # Create and start a daemon thread to run configure_and_restart
109 | self.rabbitmq_thread = threading.Thread(
110 | target=self.rabbit_handler.configure_and_restart,
111 | args=(cfg, self.message_queue),
112 | daemon=True
113 | )
114 | self.rabbitmq_thread.start()
115 | self.rabbitmq_thread.join()
116 |
117 | def isRabbitMQRunning(self):
118 | """
119 | Verifica si el hilo de RabbitMQ está en ejecución.
120 | """
121 | if self.rabbitmq_thread and self.rabbitmq_thread.is_alive():
122 | return True
123 | else:
124 | return False
125 | # Si no hay hilo, significa que RabbitMQ no está corriendo
126 |
127 | def isSioRunning(self):
128 | """
129 | Verifica si el hilo de SIO está en ejecución.
130 | """
131 | if self.thread and self.thread.is_alive():
132 | return True
133 | else:
134 | return False
135 | # Si no hay hilo, significa que SIO no está corriendo
136 |
137 | def _run(self):
138 | try:
139 | logger.info(f"SIO run: {self.server_url}")
140 | self.sio.connect(self.server_url, namespaces=self.namespaces, headers={'x-uuid': self.uuid})
141 | self.sio.wait()
142 | except Exception as e:
143 | logger.error(f"SIO Error al conectar: {e}")
144 |
145 |
146 | def start(self) -> threading.Thread:
147 | if self.thread and self.thread.is_alive():
148 | return self.thread
149 | self.stop_event.clear()
150 | self.thread = threading.Thread(target=self._run, daemon=True)
151 | self.thread.start()
152 |
153 | return self.thread
154 |
155 | def stop(self, timeout=2):
156 | logger.info("SIO STOP")
157 | self.stop_event.set()
158 | try:
159 | self.sio.disconnect() # detenemios socketio
160 | self.rabbit_handler.stop() # detenemos RabbitMQ también
161 | except Exception as e:
162 | logger.error("Error al desconectar SIO o detener RabbitMQ: %s", e)
163 | finally:
164 | logger.info("SIO y RabbitMQ disconnected OK")
165 |
166 | if self.thread and self.thread.is_alive():
167 | logger.info(f"SIO es STILL LIVE!! no deberia, Waiting for SIO thread to stop, timeout={timeout} seconds.")
168 | self.thread.join(timeout)
169 | if self.thread.is_alive():
170 | logger.warning(f"SIO thread did not stop within the timeout period of {timeout} seconds.")
171 | self.thread = None
172 |
173 | if self.rabbitmq_thread and self.rabbitmq_thread.is_alive():
174 | logger.info(f"RabbitMQ thread is STILL LIVE!! no deberia, Waiting for RabbitMQ thread to stop, timeout={timeout} seconds.")
175 | self.rabbitmq_thread.join(timeout)
176 | if self.rabbitmq_thread.is_alive():
177 | logger.warning(f"RabbitMQ thread did not stop within the timeout period of {timeout} seconds.")
178 | self.rabbitmq_thread = None
179 | logger.info("SIO y RabbitMQ disconnected OK")
180 |
181 | return True
--------------------------------------------------------------------------------
/src/fiscalberry/common/fiscalberry_logger.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import logging
4 | import logging.config
5 | import tempfile
6 | import platform
7 | from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
8 | from pathlib import Path
9 | import traceback
10 |
11 | # Evitar referencia circular en la importación
12 | def get_configberry():
13 | from fiscalberry.common.Configberry import Configberry
14 | return Configberry()
15 |
16 | class FiscalberryLogger:
17 | """Sistema centralizado de logging para Fiscalberry."""
18 |
19 | # Instancia singleton
20 | _instance = None
21 | _initialized = False
22 |
23 | @classmethod
24 | def get_instance(cls):
25 | if cls._instance is None:
26 | cls._instance = cls()
27 | return cls._instance
28 |
29 | def __init__(self):
30 | if FiscalberryLogger._initialized:
31 | return
32 |
33 | # Inicialización básica
34 | self.logger = logging.getLogger("Fiscalberry")
35 | self.logger.setLevel(logging.DEBUG)
36 |
37 | # Limpiar handlers existentes para evitar duplicados
38 | if self.logger.handlers:
39 | self.logger.handlers.clear()
40 |
41 | # Determinar ambiente y configurar nivel de detalle
42 | try:
43 | configberry = get_configberry()
44 | self.environment = configberry.config.get("SERVIDOR", "environment", fallback="production").lower()
45 | self.log_level = configberry.config.get("SERVIDOR", "log_level", fallback="INFO").upper()
46 |
47 | # Anunciar el modo de ejecución
48 | if self.environment == "development":
49 | print("* * * * * Modo de desarrollo * * * * *")
50 | else:
51 | print("@ @ @ @ @ Modo de producción @ @ @ @ @")
52 | except Exception as e:
53 | self.environment = "production"
54 | self.log_level = "INFO"
55 | print(f"Error al cargar configuración de logger: {e}")
56 | print(traceback.format_exc())
57 |
58 | # Configurar rutas de log
59 | self.log_dir = self._determine_log_directory()
60 | self.log_file = os.path.join(self.log_dir, "fiscalberry.log")
61 |
62 | # Crear formateador de logs
63 | self.formatter = logging.Formatter(
64 | "%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
65 | datefmt="%Y-%m-%d %H:%M:%S"
66 | )
67 |
68 | # Configurar handlers
69 | self._setup_file_handler()
70 | self._setup_console_handler()
71 |
72 | # Configurar loggers de terceros
73 | self._configure_third_party_loggers()
74 |
75 | FiscalberryLogger._initialized = True
76 |
77 | # Auto-log de inicio
78 | self.logger.info(f"Sistema de logging inicializado en modo {self.environment}")
79 | self.logger.info(f"Archivos de log en: {self.log_file}")
80 |
81 | def _determine_log_directory(self):
82 | """Determina el directorio de logs según la plataforma."""
83 | app_name = "fiscalberry"
84 |
85 | # Android
86 | if hasattr(os, 'getenv') and os.getenv('ANDROID_STORAGE') is not None:
87 | log_dir = os.path.join(os.getenv('EXTERNAL_STORAGE', '/sdcard'), app_name, 'logs')
88 |
89 | # Windows
90 | elif platform.system() == 'Windows':
91 | log_dir = os.path.join(os.getenv('LOCALAPPDATA', os.path.expanduser('~')), app_name, 'logs')
92 |
93 | # Linux y otros Unix
94 | else:
95 | log_dir = os.path.join(os.path.expanduser('~'), '.local', 'share', app_name, 'logs')
96 |
97 | # Crear directorio si no existe
98 | try:
99 | os.makedirs(log_dir, exist_ok=True)
100 | except Exception as e:
101 | print(f"No se pudo crear directorio de logs: {e}")
102 | log_dir = tempfile.gettempdir()
103 |
104 | return log_dir
105 |
106 | def _setup_file_handler(self):
107 | """Configura el handler para archivo de logs."""
108 | try:
109 | # RotatingFileHandler: máximo 5MB por archivo, con 5 backups
110 | file_handler = RotatingFileHandler(
111 | self.log_file,
112 | maxBytes=5*1024*1024,
113 | backupCount=5,
114 | encoding='utf-8'
115 | )
116 |
117 | # Como alternativa, también puedes usar TimedRotatingFileHandler
118 | # file_handler = TimedRotatingFileHandler(
119 | # self.log_file,
120 | # when='midnight',
121 | # interval=1,
122 | # backupCount=30,
123 | # encoding='utf-8'
124 | # )
125 |
126 | file_handler.setFormatter(self.formatter)
127 | file_handler.setLevel(getattr(logging, self.log_level))
128 | self.logger.addHandler(file_handler)
129 | except Exception as e:
130 | print(f"Error al configurar file handler: {e}")
131 | print(traceback.format_exc())
132 |
133 | def _setup_console_handler(self):
134 | """Configura el handler para salida a consola."""
135 | try:
136 | console_handler = logging.StreamHandler(stream=sys.stdout)
137 | console_handler.setFormatter(self.formatter)
138 |
139 | # En desarrollo mostramos más información
140 | if self.environment == "development":
141 | console_level = logging.DEBUG
142 | else:
143 | console_level = logging.WARNING
144 |
145 | console_handler.setLevel(console_level)
146 | self.logger.addHandler(console_handler)
147 | except Exception as e:
148 | print(f"Error al configurar console handler: {e}")
149 |
150 | def _configure_third_party_loggers(self):
151 | """Configura niveles para loggers de librerías externas."""
152 | # Reducir el ruido de logs de librerías externas
153 | logging.getLogger("pika").setLevel(logging.WARNING)
154 | logging.getLogger("socketio").setLevel(logging.INFO)
155 | logging.getLogger("requests").setLevel(logging.WARNING)
156 | logging.getLogger("urllib3").setLevel(logging.WARNING)
157 |
158 | def get_logger(self, name=None):
159 | """Devuelve el logger, opcionalmente con un nombre específico."""
160 | if name:
161 | return logging.getLogger(f"Fiscalberry.{name}")
162 | return self.logger
163 |
164 | def get_log_file_path(self):
165 | """Devuelve la ruta del archivo de log."""
166 | return self.log_file
167 |
168 | # API pública simplificada
169 | def getLogger(name=None):
170 | """Obtiene el logger global o uno específico por nombre."""
171 | logger_instance = FiscalberryLogger.get_instance()
172 | return logger_instance.get_logger(name)
173 |
174 | def getLogFilePath():
175 | """Devuelve la ruta del archivo de log."""
176 | logger_instance = FiscalberryLogger.get_instance()
177 | return logger_instance.get_log_file_path()
178 |
179 | # Para mantener compatibilidad con el código existente
180 | Logger = getLogger()
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 | # ¿Para qué sirve?
3 |
4 | Para enviar un JSON (mediante websocket), que fiscalberry lo reciba, lo transforme en un conjunto de comandos compatible con la impresora instalada, conecte con la impresora y responda al websocket con la respuesta que nos envió la impresora.
5 |
6 | Descarga la última versión socketio para windows
7 |
8 | [fiscalberry-win]([https://github.com/paxapos/fiscalberry/releases/latest/download/fiscalberry-win.exe))
9 |
10 | Para Linux
11 | [fiscalberry-lin]([https://github.com/paxapos/fiscalberry/releases/latest/download/fiscalberry-lin))
12 |
13 | Para Raspberry pi
14 | [fiscalberry-lin]([https://github.com/paxapos/fiscalberry/releases/latest/download/fiscalberry-pi))
15 |
16 | # Como comenzar (solo developers del proyecto)
17 |
18 | fiscalberry se puede instalar para usar con UI o solo consola (por ejemplo para ser ejecutado en una raspberry sin UI)
19 |
20 | la forma mas simple es descargar el proyecto
21 | y ejecutar asi:
22 | pip install -e .
23 | y luego podras ejecutar fiscalberry_gui o fiscalberry_cli como un comando mas
24 |
25 | si lo queres instalar en prod seria sin el -e
26 | pip install .
27 |
28 | la otra opcion mas para DEVs seria crear un virtual environment:
29 |
30 | clonar repo
31 | crear enviroment (1 para kivy y otro para modo cli, consola)
32 | python3 -m venv .venv.kivy
33 | python3 -m venv .venv.cli
34 |
35 | luego activar el enviroment que se desea usar, por ejemplo modo solo consola:
36 | source venv.cli/bin/activate
37 |
38 | instalar requerimientos de modo consola
39 | pip install -r requirements.cli.txt
40 |
41 | ahora ya puede ejecutar:
42 | python src/cli.py
43 |
44 | lo mismo si se desea trabajar con interfaz kivy...
45 |
46 |
47 | # ¿Qué es?
48 |
49 | Fiscalberry es un servidor de websockets desarrollado en Python pensado para que corra en una raspberry-pi (de ahí viene el nombre de este proyecto). **Pero funciona perfectamente en otros sistemas operativos.**
50 | 
51 |
52 | # ¿Qué impresoras son compatibles?
53 |
54 | Fiscalberry tiene drivers desarrollados para conectarse con 2 tipos de impresoras: Fiscales y Receipt.
55 |
56 | Impresoras Fiscales compatibles: Hasar y Epson
57 |
58 | Nueva versión con soporte para las últimas impresoras Fiscales de segunda generación (2gen) HASAR y EPSON
59 | Modelos compatibles (HASAR: SMH/PT-250F, EPSON: TM-T900FA)
60 |
61 | Impresoras Receipt (de comandas) compatibles: las que soportan ESC/P
62 |
63 | ## Fiscalberry como servidor de impresión (print-server) de impresoras receipt (comanderas) y fiscales
64 |
65 | Fiscalberry es un 3x1, actúa como: protocolo, servidor y driver facilitando al programador la impresión de tickets, facturas o comprobantes fiscales.
66 |
67 | - _PROTOCOLO_: Siguiendo la estructura del JSON indicado, se podrá imprimir independientemente de la impresora conectada. Fiscalberry se encargará de conectarse y pelear con los códigos y comandos especiales de cada marca/modelo.
68 | - _SERVIDOR_: gracias al servidor de websockets es posible conectar tu aplicación para que ésta fácilmente pueda enviar JSON's y recibir las respuestas de manera asíncrona.
69 | - _DRIVER_: Es el encargado de transformar el JSON genérico en un conjunto de comandos especiales según marca y modelo de la impresora. Aquí es donde se adaptó el código del proyecto de Reingart () para impresoras Hasar y Epson.
70 |
71 | Funciona en cualquier PC con cualquier sistema operativo que soporte Python.
72 |
73 | La idea original fue pensada para que funcione en una raspberry pi, cuyo fin es integrar las fiscales al mundo de la Internet de las Cosas (IOT).
74 |
75 | ## ¿Para quienes está pensado?
76 |
77 | Para los desarrolladores que desean enviar a imprimir mediante JSON (es decir, desde algún lugar de la red, internet, intranet, etc, etc) de una forma "estándar" y que funcione en cualquier impresora, marca y modelo.
78 |
79 | ## PROBALO
80 |
81 | ### Descargar
82 |
83 | usando git
84 |
85 | ```sh
86 | git clone https://github.com/paxapos/fiscalberry.git
87 | ```
88 |
89 | o directamente el ZIP:
90 |
91 | ### Instalar Dependencias
92 |
93 | ATENCIÓN: Funciona con Python 2.7.* NO en Python 3!
94 |
95 | probado bajo python 2.7.6 en Linux, Raspian, Ubuntu, Open Suse y Windows
96 |
97 | Se necesitan varias dependencias:
98 |
99 | ```sh
100 | sudo pip install -r requirements.txt
101 |
102 | ```
103 |
104 | Si te encontras con el error "socket.gaierror: Name or service not known"
105 |
106 | A veces, en Linux (Open Suse), ser necesario poner el nombre del equipo (hostname) en el archivo /etc/hosts, si es que aún no lo tenías.
107 | Generalmente el archivo hosts viene solo con la dirección "127.0.0.1 localhost",
108 |
109 | para solucionarlo debés ejecutar el comando
110 |
111 | ```bash
112 | hostname
113 | ```
114 |
115 | y ver cuál es el nombre de la máquina para agregarlo al archivo /etc/hosts
116 | 127.0.0.1 nombre-PC localhost
117 |
118 | ### Iniciar el programa
119 |
120 | ```sh
121 | sudo python server.py
122 |
123 | # o iniciar como demonio linux
124 | sudo python rundaemon.py
125 | ```
126 |
127 | Ahora ya puedes conectarte en el puerto 12000
128 | entrando a un browser y la dirección
129 |
130 | ## Conceptos básicos ¿Cómo funciona?
131 |
132 | Supongamos que tenemos este JSON genérico:
133 |
134 | ```
135 | {
136 | "ACCION_A_EJECUTAR": {
137 | PARAMETROS_DE_LA_ACCION
138 | ...
139 | }
140 | }
141 | ```
142 |
143 | Lo enviamos usando websockets a un host y puerto determinado (el servidor fiscalberry), éste lo procesa, envía a imprimir, y responde al cliente con la respuesta de la impresora. Por ejemplo, devolviendo el número del último comprobante impreso.
144 |
145 | Otro ejemplo más concreto: queremos imprimir un ticket, esta acción en el protocolo fiscalberry se lo llama como acción "printTicket" y está compuesta de 2 parámetros obligatorios: "encabezado" e "items".
146 |
147 | El "encabezado" indica el tipo de comprobante a imprimir (y también podría agregarle datos del cliente, que son opcionales).
148 | Los ítems son una lista de productos a imprimir donde, en este ejemplo, tenemos una coca cola, con impuesto de 21%, importe $10, descripción del producto "COCA COLA" y la cantidad vendida es 2.
149 |
150 | ```json
151 | {
152 | "printTicket": {
153 | "encabezado": {
154 | "tipo_cbte": "T", // tipo tiquet *obligatorio
155 | },
156 | "items": [
157 | {
158 | "alic_iva": 21.0, // impuesto
159 | "importe": 10, // importe
160 | "ds": "COCA COLA", // descripcion producto
161 | "qty": 2.0 // cantidad
162 | }
163 | ]
164 | }
165 | }
166 | ```
167 |
168 | ### JSON RESPUESTA
169 |
170 | Existen 2 tipos de respuesta y siempre vienen con la forma de un JSON.
171 |
172 | Aquellos que son una respuesta a un comando enviado, comienzan con "ret"
173 | **_{"ret": ......}_**
174 |
175 | Aquellos que son un mensaje directo de algun dispositivo conectado, vienen con "msg"
176 |
177 | **_{"msg": ......}_**
178 |
179 | ```javascript
180 | // ejemplo retorno de un mensaje cuando no hay papel
181 | {"msg": ["Poco papel para comprobantes o tickets"]}
182 | ```
183 |
184 | #### NOTA
185 |
186 | Deberás enviar JSON válidos al servidor. Recomendamos usar la pagina para verificar como tu programa esta generando los JSON.
187 |
--------------------------------------------------------------------------------
/.github/workflows/desktop.build.yml:
--------------------------------------------------------------------------------
1 | name: Build Desktop & Pi Ejecutables - on New Version
2 | #
3 | # Este workflow construye ejecutables para múltiples plataformas:
4 | # - CLI: Linux x64, Windows x64, Raspberry Pi ARM64
5 | # - GUI: Solo Windows x64 (Kivy es más complejo en Linux)
6 | #
7 |
8 | on:
9 | push:
10 | tags: # Trigger on tags like v*.*.*
11 | - 'v*.*.*'
12 |
13 | concurrency:
14 | group: ${{ github.workflow }}-${{ github.ref }}
15 | cancel-in-progress: true # Cancel previous runs if new commits are pushed
16 |
17 | jobs:
18 | build-desktop:
19 | strategy:
20 | fail-fast: false # Continue with other jobs if one fails
21 | matrix:
22 | os: [ubuntu-22.04, windows-latest]
23 | include:
24 | - os: ubuntu-22.04
25 | os_name: linux
26 | asset_suffix_cli: cli-lin-x64
27 | artifact_path_cli: dist/fiscalberry-cli
28 | python_platform: linux
29 | build_gui: false
30 | - os: windows-latest
31 | os_name: windows
32 | asset_suffix_cli: cli-win-x64.exe
33 | asset_suffix_gui: gui-win-x64.exe
34 | artifact_path_cli: dist/fiscalberry-cli.exe
35 | artifact_path_gui: dist/fiscalberry-gui.exe
36 | python_platform: win32
37 | build_gui: true
38 |
39 |
40 | runs-on: ${{ matrix.os }}
41 |
42 | steps:
43 |
44 | - name: Checkout code
45 | uses: actions/checkout@v4
46 |
47 | - name: Set up Python
48 | uses: actions/setup-python@v5
49 | with:
50 | python-version: '3.12'
51 |
52 | - name: Install Linux dependencies (if applicable)
53 | if: runner.os == 'Linux'
54 | run: |
55 | sudo apt-get update
56 | sudo apt-get install -y \
57 | libcups2-dev \
58 | libgl1-mesa-glx libgles2-mesa libegl1-mesa libmtdev1 \
59 | xvfb
60 |
61 | # Add steps for macOS dependencies if needed (e.g., using brew)
62 | # - name: Install macOS dependencies (if applicable)
63 | # if: runner.os == 'macOS'
64 | # run: |
65 | # brew install sdl2 # Example
66 |
67 | - name: Cache PyInstaller
68 | uses: actions/cache@v4
69 | with:
70 | path: |
71 | ~/.cache/pip
72 | ~/.cache/pyinstaller
73 | ~/AppData/Local/pip/Cache # Windows cache
74 | key: ${{ runner.os }}-pyinstaller-${{ hashFiles('fiscalberry-gui.spec') }}
75 | restore-keys: |
76 | ${{ runner.os }}-pyinstaller-
77 |
78 | - name: Install PyInstaller
79 | run: python -m pip install --upgrade pip pyinstaller
80 |
81 | # --- Build CLI for all platforms ---
82 | - name: Install CLI dependencies
83 | run: pip install -r requirements.cli.txt
84 |
85 | - name: Build CLI executable (${{ matrix.os_name }})
86 | shell: bash
87 | run: |
88 | echo "Building CLI executable for ${{ matrix.os_name }}..."
89 | pyinstaller fiscalberry-cli.spec
90 |
91 | # --- Build GUI (only for Windows) ---
92 | - name: Install GUI dependencies (Windows only)
93 | if: matrix.build_gui == true
94 | run: pip install -r requirements.kivy.txt
95 |
96 | - name: Build GUI executable (Windows only)
97 | if: matrix.build_gui == true
98 | shell: bash
99 | run: |
100 | echo "Building GUI executable for Windows..."
101 | # Set environment variables to prevent Kivy from trying to initialize graphics
102 | export KIVY_NO_WINDOW=1
103 | export KIVY_GL_BACKEND=mock
104 | export KIVY_GRAPHICS=mock
105 | pyinstaller fiscalberry-gui.spec
106 |
107 | - name: List dist directory contents (after build)
108 | if: always()
109 | shell: bash
110 | run: |
111 | echo "Listing contents of ./dist directory:"
112 | ls -lR ./dist || echo "Dist directory not found or empty."
113 |
114 | - name: Rename CLI artifact
115 | shell: bash
116 | run: |
117 | mkdir -p artifacts
118 | # Ensure the CLI source path exists before moving
119 | if [ -e "${{ matrix.artifact_path_cli }}" ]; then
120 | echo "Moving ${{ matrix.artifact_path_cli }} to artifacts/fiscalberry-${{ matrix.asset_suffix_cli }}"
121 | mv ${{ matrix.artifact_path_cli }} artifacts/fiscalberry-${{ matrix.asset_suffix_cli }}
122 | else
123 | echo "ERROR: CLI source file ${{ matrix.artifact_path_cli }} not found!"
124 | exit 1
125 | fi
126 |
127 | - name: Rename GUI artifact (Windows only)
128 | if: matrix.build_gui == true
129 | shell: bash
130 | run: |
131 | # Ensure the GUI source path exists before moving
132 | if [ -e "${{ matrix.artifact_path_gui }}" ]; then
133 | echo "Moving ${{ matrix.artifact_path_gui }} to artifacts/fiscalberry-${{ matrix.asset_suffix_gui }}"
134 | mv ${{ matrix.artifact_path_gui }} artifacts/fiscalberry-${{ matrix.asset_suffix_gui }}
135 | else
136 | echo "ERROR: GUI source file ${{ matrix.artifact_path_gui }} not found!"
137 | exit 1
138 | fi
139 |
140 | - name: Upload executables artifact (${{ matrix.os_name }})
141 | uses: actions/upload-artifact@v4
142 | with:
143 | name: fiscalberry-executables-${{ matrix.os_name }}
144 | path: artifacts/
145 |
146 | build-raspberry-pi-cli:
147 | runs-on: ubuntu-22.04
148 | steps:
149 | - name: Checkout code
150 | uses: actions/checkout@v4
151 |
152 | - name: Build Pi CLI executable
153 | uses: uraimo/run-on-arch-action@v2
154 | with:
155 | arch: aarch64
156 | distro: ubuntu22.04
157 | run: |
158 | apt-get update
159 | apt-get install -y python3-pip python3-dev libcups2-dev
160 | # Install CLI dependencies only
161 | pip3 install -r requirements.cli.txt
162 | pip3 install pyinstaller
163 | pyinstaller fiscalberry-cli.spec
164 |
165 | - name: Rename Pi CLI artifact
166 | run: |
167 | mkdir -p artifacts
168 | sudo mv dist/fiscalberry-cli artifacts/fiscalberry-cli-pi-arm64
169 |
170 | - name: Upload Pi CLI executable artifact
171 | uses: actions/upload-artifact@v4
172 | with:
173 | name: fiscalberry-executables-pi
174 | path: artifacts/fiscalberry-cli-pi-arm64
175 |
176 | create-release:
177 | runs-on: ubuntu-22.04
178 | needs: [build-desktop, build-raspberry-pi-cli] # Wait for all builds
179 | permissions:
180 | contents: write # Needed to create release and upload assets
181 |
182 | steps:
183 | - name: Checkout code (optional, needed if release notes use repo files)
184 | uses: actions/checkout@v4
185 |
186 | - name: Download all artifacts
187 | uses: actions/download-artifact@v4
188 | with:
189 | path: ./artifacts # Download all artifacts into artifacts directory
190 | # No 'name' specified to download all artifacts from the workflow run
191 |
192 | - name: List downloaded artifacts (debugging)
193 | run: |
194 | find ./artifacts -type f
195 |
196 | - name: Create GitHub Release
197 | id: create_release
198 | uses: actions/create-release@v1
199 | env:
200 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
201 | with:
202 | tag_name: ${{ github.ref_name }} # Use the tag that triggered the workflow
203 | release_name: Release ${{ github.ref_name }}
204 | draft: false
205 | prerelease: false
206 | # body: | # Optional: Add release notes
207 | # Release notes for ${{ github.ref_name }}
208 | # - Feature A
209 | # - Bugfix B
210 |
211 | - name: Set upload URL
212 | run: echo "UPLOAD_URL=${{ steps.create_release.outputs.upload_url }}" >> $GITHUB_ENV
213 |
214 | - name: Upload all assets to release
215 | shell: bash
216 | run: |
217 | for artifact_dir in ./artifacts/fiscalberry-executables-*; do
218 | files=$(find "$artifact_dir" -type f | tr '\n' ' ')
219 | if [ -n "$files" ]; then
220 | echo "Uploading files from $artifact_dir"
221 | gh release upload "${{ github.ref_name }}" $files --clobber -R "${{ github.repository }}"
222 | fi
223 | done
224 | env:
225 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
226 |
--------------------------------------------------------------------------------
/src/fiscalberry/common/rabbitmq/consumer.py:
--------------------------------------------------------------------------------
1 | import os, pika, traceback
2 | import queue
3 |
4 | from fiscalberry.common.ComandosHandler import ComandosHandler, TraductorException
5 | from fiscalberry.common.fiscalberry_logger import getLogger
6 | from typing import Optional
7 |
8 |
9 | class RabbitMQConsumer:
10 |
11 | STREAM_NAME = "paxaprinter"
12 |
13 | # 5GB
14 | STREAM_RETENTION = 5000000000
15 |
16 | def __init__(
17 | self,
18 | host: str,
19 | port: int,
20 | user: str,
21 | password: str,
22 | queue_name: str,
23 | message_queue: Optional[queue.Queue] = None,
24 | vhost: str = "/"
25 | ) -> None:
26 | self.host = host
27 | self.port = port
28 | self.user = user
29 | self.vhost = vhost
30 | self.password = password
31 | self.connection = None
32 | self.channel = None
33 | self.queue = queue_name
34 | self.message_queue = message_queue
35 |
36 | # Configuro logger según ambiente
37 | environment = os.getenv('ENVIRONMENT', 'production')
38 | if environment == 'development':
39 | sioLogger = True
40 | else:
41 | sioLogger = False
42 |
43 | self.logger = getLogger()
44 |
45 |
46 | def connect(self):
47 | """Conecta al servidor RabbitMQ."""
48 | self.logger.info(f"Connecting to RabbitMQ server: {self.host}:{self.port}")
49 |
50 | # Use connection parameters with shorter timeouts for faster failure detection
51 | params = pika.ConnectionParameters(
52 | host=self.host,
53 | port=self.port,
54 | virtual_host=self.vhost,
55 | credentials=pika.PlainCredentials(self.user, self.password),
56 | heartbeat=600,
57 | blocked_connection_timeout=300,
58 | socket_timeout=10, # Timeout más corto para detectar errores de red rápidamente
59 | connection_attempts=1, # Solo un intento, el retry lo maneja process_handler
60 | retry_delay=1 # Delay corto entre intentos
61 | )
62 | self.connection = pika.BlockingConnection(params)
63 | self.channel = self.connection.channel()
64 | self.logger.info(f"Blockin channel creado")
65 | # Manejo del exchange
66 | try:
67 | self.logger.info(f"Probando si existe exchange")
68 | # Verificar si el exchange existe sin intentar modificarlo
69 | self.channel.exchange_declare(
70 | exchange=self.STREAM_NAME,
71 | passive=True # Solo verifica si existe, no intenta declararlo
72 | )
73 | self.logger.info(f"Exchange {self.STREAM_NAME} ya existe")
74 | except pika.exceptions.ChannelClosedByBroker:
75 | self.logger.info(f"bloquin contection magic pufff")
76 | # Si el exchange no existe, reconectamos y lo creamos
77 | self.connection = pika.BlockingConnection(params)
78 | self.channel = self.connection.channel()
79 |
80 | self.channel.exchange_declare(
81 | exchange=self.STREAM_NAME,
82 | exchange_type='direct', # Usar direct como tipo predeterminado
83 | durable=True
84 | )
85 | self.logger.info(f"Exchange {self.STREAM_NAME} creado como direct")
86 |
87 | self.logger.info(f"Conectado con RabbitMQ, ahora ... Conectando a cola: {self.queue}")
88 |
89 | # Manejo de la cola - verificar primero si existe
90 | try:
91 | # Verificar si la cola existe sin intentar modificarla
92 | self.channel.queue_declare(
93 | queue=self.queue,
94 | passive=True # Solo verifica si existe, no intenta declararla
95 | )
96 | self.logger.info(f"Cola {self.queue} ya existe, usando configuración existente")
97 | except pika.exceptions.ChannelClosedByBroker:
98 | # Si la cola no existe o hay problemas con el canal, reconectamos
99 | self.connection = pika.BlockingConnection(params)
100 | self.channel = self.connection.channel()
101 |
102 | # Declarar la cola ahora que sabemos que no existe o fue eliminada
103 | self.channel.queue_declare(
104 | queue=self.queue,
105 | durable=True # Crear como durable si es una nueva cola
106 | )
107 | self.logger.info(f"Cola {self.queue} creada como durable")
108 |
109 | # Binding - intentar con la cola como routing key
110 | try:
111 | self.channel.queue_bind(
112 | exchange=self.STREAM_NAME,
113 | queue=self.queue,
114 | routing_key=self.queue # Usar el nombre de la cola como routing key
115 | )
116 | self.logger.info(f"Cola {self.queue} enlazada a exchange {self.STREAM_NAME}")
117 | except pika.exceptions.ChannelClosedByBroker as e:
118 | self.logger.error(f"Error al enlazar cola: {e}")
119 | # Reconectar para seguir operando
120 | self.connection = pika.BlockingConnection(params)
121 | self.channel = self.connection.channel()
122 |
123 | self.logger.info(f"Connected to RabbitMQ and bound queue {self.queue} to exchange {self.STREAM_NAME}")
124 |
125 |
126 | def start(self):
127 | self.connect()
128 |
129 | def callback(ch, method, properties, body):
130 | self.logger.info(f"Received message in queue {self.queue}")
131 |
132 | try:
133 | # Parse the message body - it might be bytes and need decoding
134 | if isinstance(body, bytes):
135 | body_str = body.decode('utf-8')
136 | else:
137 | body_str = body
138 |
139 | # Log the raw message for debugging
140 | self.logger.info(f"Message content: {body_str[:100]}...") # Log first 100 chars
141 |
142 | # Try to parse as JSON if needed
143 | try:
144 | import json
145 | json_data = json.loads(body_str)
146 | self.logger.info("Successfully parsed message as JSON")
147 | except json.JSONDecodeError:
148 | self.logger.warning("Message is not valid JSON, passing as raw string")
149 | json_data = body_str
150 |
151 | # Process the message
152 | comandoHandler = ComandosHandler()
153 | result = comandoHandler.send_command(json_data)
154 |
155 | # Verificar si hay un error en la respuesta
156 | if "err" in result:
157 | self.logger.error(f"Error in command handler: {result['err']}")
158 | # Si es un error recuperable, podríamos reintentar o poner en una cola de espera
159 | # Por ahora, consideramos que es un error y no reconocemos el mensaje
160 | ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
161 | return
162 |
163 | self.logger.info(f"Command handler result: {result}")
164 |
165 | # Acknowledge the message ONLY after successful processing
166 | ch.basic_ack(delivery_tag=method.delivery_tag)
167 |
168 | except TraductorException as e:
169 | self.logger.error(f"TraductorException Error: {e}")
170 | ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
171 | self.logger.error(traceback.format_exc())
172 | except Exception as e:
173 | self.logger.error(f"Error processing message: {e}")
174 | self.logger.error(traceback.format_exc())
175 | ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
176 |
177 |
178 |
179 | # Set prefetch count to process one message at a time
180 | self.channel.basic_qos(prefetch_count=1)
181 | self.channel.basic_consume(queue=self.queue, on_message_callback=callback, auto_ack=False)
182 | self.logger.info(f"Waiting for messages in queue: {self.queue}")
183 |
184 | try:
185 | self.channel.start_consuming()
186 | except Exception as e:
187 | self.logger.error(f"Error in consumer: {e}")
188 | self.logger.error(traceback.format_exc())
189 | # Try to reconnect
190 | self.stop()
191 | raise
192 |
193 |
194 | def stop(self):
195 | """Detiene la conexión."""
196 | if self.connection:
197 | self.connection.close()
198 |
199 |
--------------------------------------------------------------------------------
/src/fiscalberry/ui/fiscalberry_app.py:
--------------------------------------------------------------------------------
1 | from kivy.app import App
2 | from kivy.uix.screenmanager import ScreenManager
3 | from fiscalberry.ui.main_screen import MainScreen
4 | from fiscalberry.ui.login_screen import LoginScreen
5 | from fiscalberry.ui.adopt_screen import AdoptScreen
6 | from kivy.clock import Clock, mainthread
7 | import threading
8 | import queue
9 | from fiscalberry.common.Configberry import Configberry
10 | from fiscalberry.common.service_controller import ServiceController
11 | from kivy.lang import Builder
12 | from kivy.properties import StringProperty, BooleanProperty
13 | from fiscalberry.ui.log_screen import LogScreen
14 | from threading import Thread
15 | import time
16 | import sys
17 | import os
18 | import signal
19 |
20 |
21 | from fiscalberry.version import VERSION
22 |
23 | class FiscalberryApp(App):
24 | name = StringProperty("Servidor de Impresión")
25 | uuid = StringProperty("")
26 | host = StringProperty("")
27 | tenant = StringProperty("")
28 | siteName = StringProperty("")
29 | siteAlias = StringProperty("")
30 | version = StringProperty( VERSION )
31 |
32 |
33 | assetpath = os.path.join(os.path.dirname(__file__), "assets")
34 |
35 | background_image = StringProperty(os.path.join(assetpath, "bg.jpg"))
36 | logo_image = StringProperty(os.path.join(assetpath, "fiscalberry.png"))
37 | disconnected_image = StringProperty(os.path.join(assetpath, "disconnected.png"))
38 | connected_image = StringProperty(os.path.join(assetpath, "connected.png"))
39 |
40 | sioConnected: bool = BooleanProperty(False)
41 | rabbitMqConnected: bool = BooleanProperty(False)
42 |
43 | status_message = StringProperty("Esperando conexión...")
44 |
45 | def __init__(self, **kwargs):
46 | super().__init__(**kwargs)
47 | self.message_queue = queue.Queue() # Inicializar la variable message_queue aquí
48 |
49 | self._service_controller = ServiceController()
50 | self._configberry = Configberry()
51 | self._stopping = False # Bandera para evitar múltiples llamadas de cierre
52 |
53 | self.updatePropertiesWithConfig()
54 |
55 | # Programar la verificación del estado de SocketIO cada 2 segundos
56 | Clock.schedule_interval(self._check_sio_status, 2)
57 | Clock.schedule_interval(self._check_rabbit_status, 2)
58 |
59 | def updatePropertiesWithConfig(self):
60 | """
61 | Actualiza las propiedades de la aplicación con los valores de configuración.
62 | """
63 | self.uuid = self._configberry.get("SERVIDOR", "uuid", fallback="")
64 | self.host = self._configberry.get("SERVIDOR", "sio_host", fallback="")
65 | self.tenant = self._configberry.get("Paxaprinter", "tenant", fallback="")
66 | self.siteName = self._configberry.get("Paxaprinter", "site_name", fallback="")
67 | self.siteAlias = self._configberry.get("Paxaprinter", "alias", fallback="")
68 |
69 | def _check_sio_status(self, dt):
70 | """Verifica el estado de la conexión SocketIO y actualiza la propiedad."""
71 | self.sioConnected = self._service_controller.isSocketIORunning()
72 | # Opcionalmente, puedes actualizar el status_message aquí también si lo deseas
73 | # if self.sioConnected:
74 | # self.status_message = "Conectado a SocketIO"
75 | # else:
76 | # self.status_message = "Esperando conexión SocketIO..."
77 |
78 | def _check_rabbit_status(self, dt):
79 | """Verifica el estado de la conexión RabbitMQ y actualiza la propiedad."""
80 | self.rabbitMqConnected = self._service_controller.isRabbitRunning()
81 | # Opcionalmente, puedes actualizar el status_message aquí también si lo deseas
82 | # if self.rabbitMqConnected:
83 | # self.status_message = "Conectado a RabbitMQ"
84 | # else:
85 | # self.status_message = "Esperando conexión RabbitMQ..."
86 |
87 | @mainthread # Decorar el método
88 | def _on_config_change(self, data):
89 | """
90 | Callback que se llama cuando hay un cambio en la configuración.
91 | hay que renderizar las pantallas de nuevo para que se vean los cambios.
92 | """
93 |
94 | self.updatePropertiesWithConfig()
95 |
96 |
97 |
98 | # Usar el ScreenManager existente (self.root)
99 | sm = self.root
100 | if sm: # Verificar que self.root ya existe
101 | if self.tenant:
102 | # Si hay un tenant, ir a la pantalla principal
103 | if sm.has_screen("main"):
104 | sm.current = "main"
105 | else:
106 | print("Advertencia: No se encontró la pantalla 'main'.")
107 | else:
108 | # Si no hay tenant, ir a la pantalla de adopción
109 | if sm.has_screen("adopt"):
110 | sm.current = "adopt"
111 | self.on_stop_service() # Detener servicio si se des-adopta
112 |
113 | else:
114 | print("Advertencia: No se encontró la pantalla 'adopt'.")
115 | else:
116 | print("Error: self.root (ScreenManager) aún no está disponible.")
117 |
118 |
119 | def on_toggle_service(self):
120 | """Llamado desde la GUI para alternar el estado del servicio."""
121 | if self._service_controller.is_service_running():
122 | # Si el servicio está corriendo, detenerlo
123 | self.on_stop_service()
124 | else:
125 | # Si el servicio no está corriendo, iniciarlo
126 | self.on_start_service()
127 |
128 |
129 | def on_start_service(self):
130 | """Llamado desde la GUI para iniciar el servicio."""
131 | Thread(target=self._service_controller.start, daemon=True).start()
132 |
133 |
134 | def on_stop_service(self):
135 | """Llamado desde la GUI para detener el servicio."""
136 | # Evitar múltiples llamadas
137 | if self._stopping:
138 | return
139 |
140 | print("Deteniendo servicios desde la GUI...")
141 |
142 | # Solo detener los servicios, no forzar salida de la aplicación
143 | self._service_controller._stop_services_only()
144 | print("Servicios detenidos desde la GUI")
145 |
146 |
147 | def on_stop(self):
148 | # Este método se llama al cerrar la aplicación
149 | if self._stopping:
150 | return
151 |
152 | print("Cerrando aplicación, deteniendo servicios...")
153 | self._stopping = True
154 |
155 | # Solo detener los servicios, no llamar a app.stop() para evitar recursión
156 | self._service_controller._stop_services_only()
157 | print("Servicios detenidos...")
158 |
159 | # Usar múltiples estrategias para forzar la salida
160 | Clock.schedule_once(self._force_exit, 0.1)
161 |
162 | def _force_exit(self, dt):
163 | """Método auxiliar para forzar la salida después de la limpieza."""
164 | import signal
165 | import threading
166 |
167 | print("Forzando salida de la aplicación...")
168 |
169 | # Estrategia 1: Detener el Clock de Kivy
170 | try:
171 | Clock.stop()
172 | except:
173 | pass
174 |
175 | # Estrategia 2: Terminar threads activos
176 | try:
177 | for thread in threading.enumerate():
178 | if thread != threading.current_thread() and thread.is_alive():
179 | if hasattr(thread, '_stop'):
180 | thread._stop()
181 | except:
182 | pass
183 |
184 | # Estrategia 3: Usar signal para terminar el proceso
185 | try:
186 | os.kill(os.getpid(), signal.SIGTERM)
187 | except:
188 | pass
189 |
190 | # Estrategia 4: Exit forzado como último recurso
191 | os._exit(0)
192 |
193 |
194 | def build(self):
195 | self.title = "Servidor de Impresión"
196 |
197 | self.icon = os.path.join(os.path.dirname(__file__), "assets", "fiscalberry.png")
198 |
199 | # escuchar cambios en configberry
200 | self._configberry.add_listener(self._on_config_change)
201 |
202 | self.on_start_service()
203 |
204 | # Cargar el archivo KV
205 | kv_path = os.path.join(os.path.dirname(__file__), "kv", "main.kv")
206 | Builder.load_file(kv_path)
207 |
208 | sm = ScreenManager()
209 | #login_screen = LoginScreen(name="login")
210 | #sm.add_widget(login_screen)
211 | sm.add_widget(AdoptScreen(name="adopt"))
212 | sm.add_widget(MainScreen(name="main"))
213 |
214 | # Agregar la pantalla de logs
215 | log_screen = LogScreen(name="logs")
216 | sm.add_widget(log_screen)
217 |
218 | # Verificar ya tiene tenant o debe ser adoptado
219 | if self.tenant:
220 | # Si hay un tenant, ir directamente a la pantalla principal
221 | sm.current = "main"
222 | else:
223 | # Si no hay token, mostrar la pantalla de adoptar
224 | sm.current = "adopt"
225 |
226 | return sm
227 |
228 | if __name__ == "__main__":
229 | FiscalberryApp().run()
--------------------------------------------------------------------------------
/src/fiscalberry/common/rabbitmq/process_handler.py.backup:
--------------------------------------------------------------------------------
1 | import threading
2 | import socket
3 | import time
4 | import logging
5 | def _run_consumer(self, host, port, user, password, queue_name, message_queue):
6 | """Loop de conexión/reconexión al servidor RabbitMQ con backoff exponencial."""
7 | retry_count = 0
8 | max_retries_before_backoff = 3
9 | base_delay = 5 # segundos
10 | max_delay = 300 # 5 minutos máximo
11 |
12 | while not self._stop_event.is_set():
13 | try:
14 | consumer = RabbitMQConsumer(
15 | host=host,
16 | port=port,
17 | user=user,
18 | password=password,
19 | queue_name=queue_name,
20 | message_queue=message_queue
21 | )
22 | consumer.start()
23 | # Si llegamos aquí, la conexión fue exitosa, resetear contador
24 | retry_count = 0
25 | logger.info("Conexión RabbitMQ establecida exitosamente")
26 | # consumer.start() sólo retorna al desconectarse o error.
27 | except socket.gaierror as ex:
28 | retry_count += 1
29 | logger.error(f"Error de resolución DNS para '{host}': {ex}")
30 | logger.error("Posibles soluciones:")
31 | logger.error("1. Verificar que el hostname 'rabbitmq' esté configurado correctamente")
32 | logger.error("2. Verificar conectividad de red")
33 | logger.error("3. Verificar que el servidor RabbitMQ esté ejecutándose")
34 | logger.error("4. Considerar usar una IP directa en lugar del hostname")
35 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
36 | except pika.exceptions.AMQPConnectionError as ex:
37 | retry_count += 1
38 | logger.error(f"Error de conexión AMQP a {host}:{port}: {ex}")
39 | logger.error("Verificar que el servidor RabbitMQ esté ejecutándose y accesible")
40 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
41 | except pika.exceptions.ProbableAuthenticationError as ex:
42 | retry_count += 1
43 | logger.error(f"Error de autenticación con RabbitMQ: {ex}")
44 | logger.error(f"Verificar credenciales - Usuario: {user}, VHost: {self.config.get('RabbitMq', 'vhost', '/')}")
45 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
46 | except pika.exceptions.ProbableAccessDeniedError as ex:
47 | retry_count += 1
48 | logger.error(f"Acceso denegado a RabbitMQ: {ex}")
49 | logger.error(f"Verificar permisos del usuario '{user}' en vhost '{self.config.get('RabbitMq', 'vhost', '/')}'")
50 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
51 | except ConnectionError as ex:
52 | retry_count += 1
53 | logger.error(f"Error de conexión de red a {host}:{port}: {ex}")
54 | logger.error("Verificar conectividad de red y que el puerto esté abierto")
55 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
56 | except Exception as ex:
57 | retry_count += 1
58 | logger.error(f"Error inesperado en RabbitMQConsumer: {ex}", exc_info=True)
59 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
60 |
61 | logger.info("Salida del bucle de RabbitMQProcessHandler.")
62 |
63 | def _handle_retry(self, retry_count, max_retries_before_backoff, base_delay, max_delay):
64 | """Maneja la lógica de reintento con backoff exponencial."""
65 | if self._stop_event.is_set():
66 | return
67 |
68 | if retry_count <= max_retries_before_backoff:
69 | delay = base_delay
70 | logger.warning(f"Reintento {retry_count}/{max_retries_before_backoff} - esperando {delay}s antes del siguiente intento...")
71 | else:
72 | # Backoff exponencial después de los primeros reintentos
73 | delay = min(base_delay * (2 ** (retry_count - max_retries_before_backoff)), max_delay)
74 | logger.warning(f"Reintento {retry_count} con backoff exponencial - esperando {delay}s antes del siguiente intento...")
75 |
76 | # Esperar con verificación periódica del stop_event
77 | for _ in range(int(delay)):
78 | if self._stop_event.is_set():
79 | break
80 | time.sleep(1)mon.Configberry import Configberry
81 | from fiscalberry.common.rabbitmq.consumer import RabbitMQConsumer
82 | import pika.exceptions
83 |
84 | logger = logging.getLogger(__name__)
85 |
86 | class RabbitMQProcessHandler:
87 | """Administra el hilo de RabbitMQConsumer: arranque, paro y reintentos."""
88 |
89 | _instance = None
90 | _lock = threading.Lock()
91 |
92 | def __new__(cls, *args, **kwargs):
93 | with cls._lock:
94 | if not cls._instance:
95 | cls._instance = super().__new__(cls)
96 | cls._instance._initialized = False
97 | return cls._instance
98 |
99 | def __init__(self):
100 | if self._initialized:
101 | return
102 | self._thread = None
103 | self._stop_event = threading.Event()
104 | self.config = Configberry()
105 | self._initialized = True
106 |
107 |
108 | def start(self, message_queue):
109 | """Arranca el consumidor en un hilo daemon (no bloqueante)."""
110 | if self._thread and self._thread.is_alive():
111 | logger.warning("RabbitMQ thread ya en ejecución.")
112 | return
113 |
114 | host = self.config.get("RabbitMq", "host")
115 | port = self.config.get("RabbitMq", "port")
116 | user = self.config.get("RabbitMq", "user")
117 | password = self.config.get("RabbitMq", "password")
118 |
119 | # TODO deberia usarse esto: queue_name = self.config.get("RabbitMq", "queue")
120 | queue_name = self.config.get("SERVIDOR", "uuid")
121 | self._stop_event.clear()
122 | self._thread = threading.Thread(
123 | target=self._run_consumer,
124 | args=(host, port, user, password, queue_name, message_queue),
125 | daemon=True
126 | )
127 | self._thread.start()
128 | logger.info("RabbitMQ thread iniciado.")
129 | #self._thread.join() # No bloqueante, solo para iniciar el hilo
130 | #logger.info("RabbitMQ thread en ejecución.")
131 |
132 |
133 |
134 | def _run_consumer(self, host, port, user, password, queue_name, message_queue):
135 | """Loop de conexión/reconexión al servidor RabbitMQ."""
136 | while not self._stop_event.is_set():
137 | try:
138 | consumer = RabbitMQConsumer(
139 | host=host,
140 | port=port,
141 | user=user,
142 | password=password,
143 | queue_name=queue_name,
144 | message_queue=message_queue
145 | )
146 | consumer.start()
147 | # consumer.start() sólo retorna al desconectarse o error.
148 | except Exception as ex:
149 | logger.error(f"Error en RabbitMQConsumer: {ex}", exc_info=True)
150 | if not self._stop_event.is_set():
151 | logger.warning("RabbitMQConsumer detenido; reintentando en 5 s...")
152 | time.sleep(5)
153 | logger.info("Salida del bucle de RabbitMQProcessHandler.")
154 |
155 | def stop(self, timeout: float = 1):
156 | """Detiene el hilo y espera su finalización."""
157 | if self._thread and self._thread.is_alive():
158 | logger.info("Deteniendo RabbitMQConsumer...")
159 | self._stop_event.set()
160 | self._thread.join(timeout)
161 | if self._thread.is_alive():
162 | logger.warning("RabbitMQConsumer no se detuvo a tiempo.")
163 | else:
164 | logger.info("RabbitMQConsumer detenido correctamente.")
165 | else:
166 | logger.warning("No hay hilo de RabbitMQConsumer en ejecución.")
167 | self._thread = None
168 | self._stop_event.clear()
169 | logger.info("RabbitMQProcessHandler detenido.")
170 |
171 | def configure_and_restart(self, data: dict, message_queue):
172 | """
173 | Recibe el JSON, actualiza Configberry si cambia y reinicia el hilo.
174 | data debe traer keys 'RabbitMq' y 'Paxaprinter' como en tu interfaz HiDto.
175 | """
176 | rabbit_cfg = data.get('RabbitMq', {})
177 | # valida campos...
178 | host = rabbit_cfg.get('host'); port = int(rabbit_cfg.get('port', 0))
179 | user = rabbit_cfg.get('user'); pwd = rabbit_cfg.get('password')
180 | vhost = rabbit_cfg.get('vhost'); queue = rabbit_cfg.get('queue')
181 | # compara con lo actual
182 | curr = {
183 | "host": self.config.get("RabbitMq", "host"),
184 | "port": self.config.get("RabbitMq", "port"),
185 | "user": self.config.get("RabbitMq", "user"),
186 | "password": self.config.get("RabbitMq", "password"),
187 | "vhost": self.config.get("RabbitMq", "vhost"),
188 | "queue": self.config.get("RabbitMq", "queue"),
189 | }
190 | new = {"host": host, "port": port, "user": user, "password": pwd, "vhost": vhost, "queue": queue}
191 | if curr != new:
192 | self.config.set("RabbitMq", new)
193 | logger.info("Configuración de RabbitMQ actualizada en disk.")
194 |
195 | # compara con lo actual de Paxaprinter
196 | pax_cfg = data.get('Paxaprinter', {})
197 | alias = pax_cfg.get('alias')
198 | tenant = pax_cfg.get('tenant')
199 | site_name = pax_cfg.get('site_name')
200 | curr_pax = {
201 | "alias": self.config.get("Paxaprinter", "alias"),
202 | "tenant": self.config.get("Paxaprinter", "tenant"),
203 | "site_name": self.config.get("Paxaprinter", "site_name")
204 | }
205 | new_pax = {"alias": alias, "tenant": tenant, "site_name": site_name}
206 | if curr_pax != new_pax:
207 | self.config.set("Paxaprinter", new_pax)
208 | logger.info("Configuración de Paxaprinter actualizada en disk.")
209 |
210 | # guarda en config y reinicia hilo
211 | if self._thread and self._thread.is_alive():
212 | self.stop(timeout=5)
213 | self.start(message_queue)
214 |
215 |
--------------------------------------------------------------------------------
/src/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/fiscalberry/common/rabbitmq/process_handler.py:
--------------------------------------------------------------------------------
1 | import threading
2 | import socket
3 | import time
4 | import logging
5 | from fiscalberry.common.Configberry import Configberry
6 | from fiscalberry.common.rabbitmq.consumer import RabbitMQConsumer
7 | import pika.exceptions
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | class RabbitMQProcessHandler:
12 | """Administra el hilo de RabbitMQConsumer: arranque, paro y reintentos."""
13 |
14 | _instance = None
15 | _lock = threading.Lock()
16 |
17 | def __new__(cls, *args, **kwargs):
18 | with cls._lock:
19 | if not cls._instance:
20 | cls._instance = super().__new__(cls)
21 | cls._instance._initialized = False
22 | return cls._instance
23 |
24 | def __init__(self):
25 | if self._initialized:
26 | return
27 | self._thread = None
28 | self._stop_event = threading.Event()
29 | self.config = Configberry()
30 | self._initialized = True
31 |
32 | def start(self, message_queue):
33 | """Arranca el consumidor en un hilo daemon (no bloqueante)."""
34 | if self._thread and self._thread.is_alive():
35 | logger.warning("RabbitMQ thread ya en ejecución.")
36 | return
37 |
38 | host = self.config.get("RabbitMq", "host")
39 | port = self.config.get("RabbitMq", "port")
40 | user = self.config.get("RabbitMq", "user")
41 | password = self.config.get("RabbitMq", "password")
42 |
43 | # TODO deberia usarse esto: queue_name = self.config.get("RabbitMq", "queue")
44 | queue_name = self.config.get("SERVIDOR", "uuid")
45 | self._stop_event.clear()
46 | self._thread = threading.Thread(
47 | target=self._run_consumer,
48 | args=(host, port, user, password, queue_name, message_queue),
49 | daemon=True
50 | )
51 | self._thread.start()
52 | logger.info("RabbitMQ thread iniciado.")
53 |
54 | def _check_network_connectivity(self, host, port, timeout=5):
55 | """Verifica la conectividad de red básica antes de intentar conectar con RabbitMQ."""
56 | try:
57 | # Primero verificar resolución DNS
58 | socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
59 |
60 | # Luego verificar si el puerto está abierto
61 | with socket.create_connection((host, port), timeout=timeout):
62 | return True
63 | except socket.gaierror as e:
64 | logger.error(f"Error de resolución DNS para {host}: {e}")
65 | return False
66 | except (socket.timeout, ConnectionRefusedError, OSError) as e:
67 | logger.error(f"No se puede conectar a {host}:{port} - {e}")
68 | return False
69 | except Exception as e:
70 | logger.error(f"Error inesperado verificando conectividad: {e}")
71 | return False
72 |
73 | def _run_consumer(self, host, port, user, password, queue_name, message_queue):
74 | """Loop de conexión/reconexión al servidor RabbitMQ con backoff exponencial."""
75 | retry_count = 0
76 | max_retries_before_backoff = 3
77 | base_delay = 5 # segundos
78 | max_delay = 300 # 5 minutos máximo
79 |
80 | while not self._stop_event.is_set():
81 | try:
82 | # Verificar conectividad básica antes de intentar conexión RabbitMQ
83 | if not self._check_network_connectivity(host, int(port)):
84 | retry_count += 1
85 | logger.warning(f"Conectividad de red falló para {host}:{port}")
86 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
87 | continue
88 |
89 | consumer = RabbitMQConsumer(
90 | host=host,
91 | port=port,
92 | user=user,
93 | password=password,
94 | queue_name=queue_name,
95 | message_queue=message_queue
96 | )
97 | consumer.start()
98 | # Si llegamos aquí, la conexión fue exitosa, resetear contador
99 | retry_count = 0
100 | logger.info("Conexión RabbitMQ establecida exitosamente")
101 | # consumer.start() sólo retorna al desconectarse o error.
102 | except socket.gaierror as ex:
103 | retry_count += 1
104 | logger.error(f"Error de resolución DNS para '{host}': {ex}")
105 | logger.error("Posibles soluciones:")
106 | logger.error("1. Verificar que el hostname 'rabbitmq' esté configurado correctamente")
107 | logger.error("2. Verificar conectividad de red")
108 | logger.error("3. Verificar que el servidor RabbitMQ esté ejecutándose")
109 | logger.error("4. Considerar usar una IP directa en lugar del hostname")
110 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
111 | except pika.exceptions.AMQPConnectionError as ex:
112 | retry_count += 1
113 | logger.error(f"Error de conexión AMQP a {host}:{port}: {ex}")
114 | logger.error("Verificar que el servidor RabbitMQ esté ejecutándose y accesible")
115 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
116 | except pika.exceptions.ProbableAuthenticationError as ex:
117 | retry_count += 1
118 | logger.error(f"Error de autenticación con RabbitMQ: {ex}")
119 | logger.error(f"Verificar credenciales - Usuario: {user}, VHost: {self.config.get('RabbitMq', 'vhost', '/')}")
120 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
121 | except pika.exceptions.ProbableAccessDeniedError as ex:
122 | retry_count += 1
123 | logger.error(f"Acceso denegado a RabbitMQ: {ex}")
124 | logger.error(f"Verificar permisos del usuario '{user}' en vhost '{self.config.get('RabbitMq', 'vhost', '/')}'")
125 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
126 | except ConnectionError as ex:
127 | retry_count += 1
128 | logger.error(f"Error de conexión de red a {host}:{port}: {ex}")
129 | logger.error("Verificar conectividad de red y que el puerto esté abierto")
130 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
131 | except Exception as ex:
132 | retry_count += 1
133 | logger.error(f"Error inesperado en RabbitMQConsumer: {ex}", exc_info=True)
134 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay)
135 |
136 | logger.info("Salida del bucle de RabbitMQProcessHandler.")
137 |
138 | def _handle_retry(self, retry_count, max_retries_before_backoff, base_delay, max_delay):
139 | """Maneja la lógica de reintento con backoff exponencial."""
140 | if self._stop_event.is_set():
141 | return
142 |
143 | if retry_count <= max_retries_before_backoff:
144 | delay = base_delay
145 | logger.warning(f"Reintento {retry_count}/{max_retries_before_backoff} - esperando {delay}s antes del siguiente intento...")
146 | else:
147 | # Backoff exponencial después de los primeros reintentos
148 | delay = min(base_delay * (2 ** (retry_count - max_retries_before_backoff)), max_delay)
149 | logger.warning(f"Reintento {retry_count} con backoff exponencial - esperando {delay}s antes del siguiente intento...")
150 |
151 | # Esperar con verificación periódica del stop_event
152 | for _ in range(int(delay)):
153 | if self._stop_event.is_set():
154 | break
155 | time.sleep(1)
156 |
157 | def stop(self, timeout: float = 1):
158 | """Detiene el hilo y espera su finalización."""
159 | if self._thread and self._thread.is_alive():
160 | logger.info("Deteniendo RabbitMQConsumer...")
161 | self._stop_event.set()
162 | self._thread.join(timeout)
163 | if self._thread.is_alive():
164 | logger.warning("RabbitMQConsumer no se detuvo a tiempo.")
165 | else:
166 | logger.info("RabbitMQConsumer detenido correctamente.")
167 | else:
168 | logger.warning("No hay hilo de RabbitMQConsumer en ejecución.")
169 | self._thread = None
170 | self._stop_event.clear()
171 | logger.info("RabbitMQProcessHandler detenido.")
172 |
173 | def configure_and_restart(self, data: dict, message_queue):
174 | """
175 | Recibe el JSON, actualiza Configberry si cambia y reinicia el hilo.
176 | data debe traer keys 'RabbitMq' y 'Paxaprinter' como en tu interfaz HiDto.
177 |
178 | LÓGICA: Para cada campo, si tiene contenido en config.ini, usar ese valor.
179 | Solo usar valores de SocketIO para completar campos vacíos en config.
180 | """
181 | rabbit_cfg = data.get('RabbitMq', {})
182 |
183 | def get_config_or_message_value(config_key, message_key, default_value=""):
184 | """
185 | Obtiene valor del config.ini si existe y no está vacío, sino del mensaje SocketIO.
186 | Prioridad: config.ini -> mensaje SocketIO -> valor por defecto
187 | """
188 | config_value = self.config.get("RabbitMq", config_key, fallback="")
189 | if config_value and str(config_value).strip():
190 | logger.info(f"{config_key}: usando config.ini -> {config_value}")
191 | return config_value
192 | else:
193 | message_value = rabbit_cfg.get(message_key, default_value)
194 | logger.info(f"{config_key}: config.ini vacío, usando SocketIO -> {message_value}")
195 | return message_value
196 |
197 | logger.info("=== Configuración RabbitMQ - Prioridad config.ini ===")
198 |
199 | # Aplicar la lógica a todos los parámetros
200 | host = get_config_or_message_value("host", "host")
201 | port_str = get_config_or_message_value("port", "port", "5672")
202 | user = get_config_or_message_value("user", "user", "guest")
203 | pwd = get_config_or_message_value("password", "password", "guest")
204 | vhost = get_config_or_message_value("vhost", "vhost", "/")
205 | queue = get_config_or_message_value("queue", "queue", "")
206 |
207 | # Convertir puerto a entero
208 | try:
209 | port = int(port_str)
210 | except (ValueError, TypeError):
211 | logger.warning(f"Puerto inválido '{port_str}', usando 5672 por defecto")
212 | port = 5672
213 |
214 | # compara con lo actual
215 | curr = {
216 | "host": self.config.get("RabbitMq", "host"),
217 | "port": self.config.get("RabbitMq", "port"),
218 | "user": self.config.get("RabbitMq", "user"),
219 | "password": self.config.get("RabbitMq", "password"),
220 | "vhost": self.config.get("RabbitMq", "vhost"),
221 | "queue": self.config.get("RabbitMq", "queue"),
222 | }
223 | new = {"host": host, "port": port, "user": user, "password": pwd, "vhost": vhost, "queue": queue}
224 |
225 | # Verificar si hay cambios en la configuración
226 | if curr != new:
227 | self.config.set("RabbitMq", new)
228 | logger.info("Configuración de RabbitMQ actualizada en disk con valores prioritarios del config.ini")
229 | else:
230 | logger.info("Configuración de RabbitMQ sin cambios")
231 |
232 | # compara con lo actual de Paxaprinter
233 | pax_cfg = data.get('Paxaprinter', {})
234 | alias = pax_cfg.get('alias')
235 | tenant = pax_cfg.get('tenant')
236 | site_name = pax_cfg.get('site_name')
237 | curr_pax = {
238 | "alias": self.config.get("Paxaprinter", "alias"),
239 | "tenant": self.config.get("Paxaprinter", "tenant"),
240 | "site_name": self.config.get("Paxaprinter", "site_name")
241 | }
242 | new_pax = {"alias": alias, "tenant": tenant, "site_name": site_name}
243 | if curr_pax != new_pax:
244 | self.config.set("Paxaprinter", new_pax)
245 | logger.info("Configuración de Paxaprinter actualizada en disk.")
246 |
247 | # guarda en config y reinicia hilo
248 | if self._thread and self._thread.is_alive():
249 | self.stop(timeout=5)
250 | self.start(message_queue)
--------------------------------------------------------------------------------
/src/fiscalberry/ui/kv/main.kv:
--------------------------------------------------------------------------------
1 | ScreenManager:
2 | LoginScreen:
3 | name: "login"
4 | MainScreen:
5 | name: "main"
6 | LogScreen: # Add LogScreen here
7 | name: "logs" # Give it a name for navigation
8 |
9 | :
10 | BoxLayout:
11 | orientation: 'vertical'
12 | padding: 20
13 | spacing: 15
14 | canvas.before:
15 | Color:
16 | rgba: 0.9, 0.9, 1, 1 # Fondo celeste casi blanco
17 | Rectangle:
18 | pos: self.pos
19 | size: self.size
20 |
21 | Image:
22 | source: app.logo_image
23 | size_hint_y: None
24 | height: 96
25 | allow_stretch: True
26 | keep_ratio: True
27 | color: 1, 1, 1, 1
28 | margin_bottom: 10
29 | AsyncImage:
30 | source: root.qrCodeLink
31 | size_hint_y: None
32 | height: 96
33 | allow_stretch: True
34 | keep_ratio: True
35 | color: 1, 1, 1, 1
36 | margin_bottom: 10
37 | Button:
38 | text: "Hace click aquí para vincular el dispositivo"
39 | size_hint_y: None
40 | height: 40
41 | background_color: 0.8, 0.5, 0.2, 1 # Naranja suave
42 | color: 1, 1, 1, 1
43 | on_press: import webbrowser; webbrowser.open(root.adoptarLink)
44 |
45 | :
46 | BoxLayout:
47 | orientation: 'vertical'
48 | padding: 20
49 | spacing: 15
50 | canvas.before:
51 | Color:
52 | rgba: 0.9, 0.9, 1, 1 # Fondo celeste casi blanco
53 | Rectangle:
54 | pos: self.pos
55 | size: self.size
56 |
57 | Image:
58 | source: app.logo_image
59 | size_hint_y: None
60 | height: 96
61 | allow_stretch: True
62 | keep_ratio: True
63 | color: 1, 1, 1, 1
64 | margin_bottom: 10
65 |
66 |
67 | Label:
68 | text: "Iniciar Sesión"
69 | font_size: 24
70 | color: 0.1, 0.2, 0.5, 1 # Azul oscuro
71 |
72 | Label:
73 | text: "Utilice su usuario y contraseña para acceder al sistema."
74 | size_hint_y: None
75 | height: 30
76 | halign: "center"
77 | valign: "middle"
78 | text_size: self.size
79 | color: 0.5, 0.5, 0.5, 1 # Gris tenue
80 |
81 | Label:
82 | text: root.errorMsg
83 | color: 0.8, 0.2, 0.2, 1 # Rojo estándar para errores
84 | font_size: 16
85 | size_hint_y: None
86 | height: 30
87 | halign: "center"
88 | valign: "middle"
89 | text_size: self.size
90 |
91 | TextInput:
92 | id: username
93 | hint_text: "Usuario"
94 | multiline: False
95 | size_hint_y: None
96 | height: 50
97 | background_color: 1, 1, 1, 1
98 | foreground_color: 0, 0, 0, 1
99 | padding: [10, 10]
100 | on_text_validate: password.focus = True # Al presionar Enter, enfocar el siguiente input
101 | TextInput:
102 | id: password
103 | hint_text: "Contraseña"
104 | password: True
105 | multiline: False
106 | size_hint_y: None
107 | height: 50
108 | background_color: 1, 1, 1, 1
109 | foreground_color: 0, 0, 0, 1
110 | padding: [10, 10]
111 | on_text_validate: root.login(username.text, password.text) # Al presionar Enter, ejecutar login
112 | Button:
113 | text: "Login"
114 | size_hint_y: None
115 | height: 50
116 | background_color: 0.1, 0.2, 0.5, 1 # Azul oscuro
117 | color: 1, 1, 1, 1
118 | on_press: root.login(username.text, password.text)
119 |
120 | # Define a reusable Separator widget
121 | :
122 | canvas:
123 | Color:
124 | rgba: 0.8, 0.8, 0.8, 1 # Light gray color for separator
125 | Rectangle:
126 | pos: self.x, self.center_y - dp(0.5) # Center the line vertically
127 | size: self.width, dp(1) # Make it 1dp thick
128 |
129 | :
130 | BoxLayout:
131 | orientation: 'vertical'
132 | padding: dp(20)
133 | spacing: dp(15)
134 | canvas.before:
135 | Color:
136 | rgba: 0.95, 0.95, 1, 1 # Slightly lighter blue background
137 | Rectangle:
138 | pos: self.pos
139 | size: self.size
140 |
141 | # Header: Logo, Title, Overall Status Icon
142 | BoxLayout:
143 | orientation: 'horizontal'
144 | size_hint_y: None
145 | height: dp(48) # Fixed height for the header
146 | spacing: dp(10)
147 | Image:
148 | source: app.logo_image
149 | size_hint: None, None # Use explicit size
150 | size: dp(48), dp(48)
151 | allow_stretch: True
152 | keep_ratio: True
153 | Label:
154 | text: app.name # Consider using app.name if available
155 | font_size: '20sp'
156 | halign: 'center'
157 | valign: 'middle'
158 | color: 0.1, 0.2, 0.5, 1 # Dark blue text
159 | size_hint_x: 1 # Allow label to expand horizontally
160 | Image:
161 | # Show overall status based on cloud connection primarily
162 | source: root.connected_image if app.sioConnected else root.disconnected_image
163 | size_hint: None, None # Use explicit size
164 | size: dp(32), dp(32)
165 |
166 | Separator:
167 | height: dp(1) # Use the defined Separator
168 |
169 | # Status Information Area
170 | BoxLayout:
171 | orientation: 'vertical'
172 | size_hint_y: None # Size based on content
173 | height: self.minimum_height # Calculate height automatically
174 | spacing: dp(10)
175 |
176 | Label:
177 | text: "Estado de Conexión"
178 | font_size: '18sp'
179 | color: 0.1, 0.2, 0.5, 1
180 | size_hint_y: None
181 | height: self.texture_size[1] # Fit height to text
182 |
183 | # Cloud Status (SocketIO)
184 | BoxLayout:
185 | orientation: 'horizontal'
186 | size_hint_y: None
187 | height: dp(24) # Fixed height for status line
188 | spacing: dp(5)
189 | Image:
190 | source: root.connected_image if app.sioConnected else root.disconnected_image
191 | size_hint: None, None
192 | size: dp(24), dp(24)
193 | Label:
194 | # Display text and color based on connection status
195 | text: "Nube (Sync): " + ("Conectado" if app.sioConnected else "Desconectado")
196 | color: (0.2, 0.8, 0.2, 1) if app.sioConnected else (0.8, 0.2, 0.2, 1) # Green/Red
197 | halign: 'left'
198 | valign: 'middle'
199 | text_size: self.width, None # Enable text wrapping if needed
200 | size_hint_x: 1
201 |
202 | # Queue Status (RabbitMQ)
203 | BoxLayout:
204 | orientation: 'horizontal'
205 | size_hint_y: None
206 | height: dp(24) # Fixed height for status line
207 | spacing: dp(5)
208 | Image:
209 | # Assumes root has appropriate images for connection status
210 | source: root.connected_image if app.rabbitMqConnected else root.disconnected_image
211 | size_hint: None, None
212 | size: dp(24), dp(24)
213 | Label:
214 | # Display text and color based on connection status
215 | text: "Cola Local (Cmds): " + ("Conectado" if app.rabbitMqConnected else "Desconectado")
216 | color: (0.2, 0.8, 0.2, 1) if app.rabbitMqConnected else (0.8, 0.2, 0.2, 1) # Green/Red
217 | halign: 'left'
218 | valign: 'middle'
219 | text_size: self.width, None # Enable text wrapping
220 | size_hint_x: 1
221 |
222 | Separator:
223 | height: dp(1)
224 |
225 | # Site Information Area
226 | BoxLayout:
227 | orientation: 'vertical'
228 | size_hint_y: None # Size based on content
229 | height: self.minimum_height # Calculate height automatically
230 | spacing: dp(5)
231 |
232 | Label:
233 | text: "Información del Sitio"
234 | font_size: '18sp'
235 | color: 0.1, 0.2, 0.5, 1
236 | size_hint_y: None
237 | height: self.texture_size[1]
238 |
239 | Label:
240 | text: f"Comercio: {app.siteName}"
241 | font_size: '14sp'
242 | color: 0.3, 0.3, 0.3, 1 # Dark gray text
243 | halign: 'left'
244 | valign: 'middle'
245 | text_size: self.width, None # Enable wrapping
246 | size_hint_y: None
247 | height: self.texture_size[1] # Fit height to text
248 |
249 | Label:
250 | text: f"Dispositivo: {app.siteAlias}"
251 | font_size: '14sp'
252 | color: 0.3, 0.3, 0.3, 1
253 | halign: 'left'
254 | valign: 'middle'
255 | text_size: self.width, None
256 | size_hint_y: None
257 | height: self.texture_size[1]
258 |
259 | # Spacer widget to push subsequent elements to the bottom
260 | Widget:
261 |
262 | # Action Button
263 | Button:
264 | text: "Ver Logs del Servicio"
265 | size_hint_y: None # Fixed height
266 | height: dp(50)
267 | background_color: 0.1, 0.2, 0.5, 1 # Consistent button blue
268 | color: 1, 1, 1, 1 # White text
269 | on_press: app.root.current = "logs" # Navigate to LogScreen
270 |
271 | # Footer Information (UUID, Version, Host)
272 | BoxLayout:
273 | orientation: 'horizontal'
274 | size_hint_y: None
275 | height: dp(20) # Compact footer height
276 | spacing: dp(10)
277 | Label:
278 | text: f"UUID: {app.uuid[:8]}"
279 | font_size: '10sp' # Smaller font for footer details
280 | color: 0.5, 0.5, 0.5, 1 # Gray text
281 | halign: "left"
282 | valign: "middle"
283 | size_hint_x: 0.4 # Allocate space
284 | Label:
285 | text: f"v{app.version}"
286 | font_size: '10sp'
287 | color: 0.5, 0.5, 0.5, 1
288 | halign: "center"
289 | valign: "middle"
290 | size_hint_x: 0.2
291 | Label:
292 | text: f"Host: {app.host}"
293 | font_size: '10sp'
294 | color: 0.5, 0.5, 0.5, 1
295 | halign: "right"
296 | valign: "middle"
297 | size_hint_x: 0.4
298 |
299 | :
300 | BoxLayout:
301 | orientation: 'vertical'
302 | padding: 10
303 | spacing: 10
304 |
305 | Label:
306 | text: "Logs del Servicio"
307 | font_size: 24
308 | size_hint_y: None
309 | height: 50
310 |
311 | Button:
312 | text: root.logFilePath
313 | font_size: 12
314 | size_hint_y: None
315 | height: self.texture_size[1] + dp(10) # Adjust height based on text
316 | # Style to look like a clickable link
317 | background_color: 0, 0, 0, 0 # Transparent background
318 | color: 0.1, 0.2, 0.8, 1 # Blue color like a hyperlink
319 | # Action to open the file using a method defined in the Python class
320 | # Ensure 'open_log_file' method exists in your LogScreen Python class
321 | on_press: root.open_log_file()
322 |
323 | ScrollView:
324 | id: scroll_view # Agregar un ID para acceder desde Python
325 | size_hint: (1, 1)
326 | TextInput:
327 | id: log_output
328 | text: root.logs
329 | readonly: True
330 | font_size: 14
331 | background_color: 0, 0, 0, 1 # Fondo negro
332 | foreground_color: 1, 1, 1, 1 # Texto blanco
333 | size_hint_y: None
334 | height: self.minimum_height
335 | multiline: True
336 |
337 | Button:
338 | text: "Volver"
339 | size_hint_y: None
340 | height: 50
341 | on_press: app.root.current = "main"
--------------------------------------------------------------------------------
/src/fiscalberry/common/service_controller.py:
--------------------------------------------------------------------------------
1 | from fiscalberry.common.fiscalberry_logger import getLogger
2 | import sys
3 | import os
4 | import signal
5 | import socketio
6 | from fiscalberry.common.fiscalberry_sio import FiscalberrySio
7 | from fiscalberry.common.discover import send_discover_in_thread
8 | from fiscalberry.common.Configberry import Configberry
9 | import time
10 | import threading
11 | from fiscalberry.common.fiscalberry_logger import getLogger
12 | logger = getLogger()
13 |
14 | class ServiceController:
15 | """
16 | Controla el inicio y detención del servicio socketio de Fiscalberry.
17 | El servicio controla 2 cosas: por un lado el socketio queda como daemond, el discover se ejecuta y finaliza.
18 | SocketIO sirve para mensajear y recibir mensajes de la aplicación cliente. Sirve para comunicarse y enviarle comandos de managment bidireccionales.
19 | Por ejemplo un event importante que tiene el servicio de SocketIO es inicializar el cliente de RabbitMQ (RabbitMQProcessHandler)
20 | El discover: es un request donde envia informacion como la IP, el UUID y datos de las impresoras instaladas en esta PC. Envia el request y el thread deberia finalizar.
21 |
22 | El servicio de socketio queda corriendo en segundo plano y se encarga de recibir mensajes y enviar respuestas.
23 | """
24 |
25 | sio: FiscalberrySio = None
26 | _instance = None
27 |
28 | def __new__(cls):
29 | if cls._instance is None:
30 | cls._instance = super(ServiceController, cls).__new__(cls)
31 | return cls._instance
32 |
33 | def __init__(self):
34 | if hasattr(self, 'initialized'):
35 | return
36 |
37 | self.initialized = True
38 | self.socketio_thread = None
39 | self.discover_thread = None
40 | self.configberry = Configberry()
41 | self._stop_event = threading.Event() # Evento para detener limpiamente
42 | self.initial_retries = 0 # Contador para chequeo inicial de UUID
43 |
44 | sio_host = self.configberry.get("SERVIDOR", "sio_host")
45 | if not sio_host:
46 | logger.error("sio_host no configurado. Abortando.")
47 | os._exit(1)
48 |
49 | # --- Bucle de chequeo inicial de UUID ---
50 | uuidval = self.configberry.get("SERVIDOR", "uuid", fallback="")
51 | if not uuidval:
52 | logger.error(f"UUID NO encontrado en config file: {uuidval}")
53 | os._exit(1)
54 |
55 | self.sio = FiscalberrySio(sio_host, uuidval)
56 |
57 | # Configurar manejo de señales
58 | self._setup_signal_handlers()
59 |
60 | def _setup_signal_handlers(self):
61 | """Configura los manejadores de señales para detener limpiamente."""
62 | # Guardar las referencias a los manejadores originales
63 | self._original_sigint_handler = signal.getsignal(signal.SIGINT)
64 | self._original_sigterm_handler = signal.getsignal(signal.SIGTERM)
65 |
66 | # Registrar nuestros manejadores
67 | signal.signal(signal.SIGINT, self._signal_handler)
68 | signal.signal(signal.SIGTERM, self._signal_handler)
69 |
70 | def _signal_handler(self, sig, frame):
71 | """Manejador de señales para cierre graceful."""
72 | logger.info(f"Recibida señal {signal.Signals(sig).name}. Deteniendo servicios...")
73 |
74 | # Solicitar detención limpia (sin sys.exit)
75 | self._stop_services_only()
76 |
77 | # Terminar inmediatamente después de la limpieza
78 | logger.info("Terminando aplicación...")
79 | os._exit(0) # Terminar de forma inmediata y definitiva
80 |
81 | def is_service_running(self):
82 | """Verifica si el servicio ya está en ejecución."""
83 | # Verifica si el hilo está activo
84 | if self.socketio_thread and self.socketio_thread.is_alive():
85 | return True
86 | return False
87 |
88 | def isSocketIORunning(self):
89 | """
90 | Verifica si el hilo de SIO está en ejecución.
91 | """
92 | return self.sio.isSioRunning()
93 |
94 | def isRabbitRunning(self):
95 | """
96 | Verifica si el hilo de RabbitMQ está en ejecución.
97 | """
98 | return self.sio.isRabbitMQRunning()
99 |
100 | def toggle_service(self):
101 | """Alterna el estado del servicio."""
102 | if self.is_service_running():
103 | logger.info("Deteniendo el servicio...")
104 | return self.stop()
105 | else:
106 | logger.info("Iniciando el servicio...")
107 | return self.start()
108 |
109 | def _run_sio_instance(self):
110 | """Ejecuta una instancia/conexión de FiscalberrySio y ESPERA a que termine."""
111 | sio_internal_thread = None # Para almacenar el hilo devuelto por sio.start()
112 | try:
113 | # sio.start() crea y arranca el hilo interno (_run) y lo devuelve
114 | sio_internal_thread = self.sio.start()
115 |
116 | # ¡IMPORTANTE! Esperar a que el hilo interno de SIO termine.
117 | if sio_internal_thread and isinstance(sio_internal_thread, threading.Thread):
118 | # Usar join con timeout para poder verificar periódicamente si hay una solicitud de stop
119 | while sio_internal_thread.is_alive() and not self._stop_event.is_set():
120 | sio_internal_thread.join(timeout=1.0)
121 |
122 | if self._stop_event.is_set() and sio_internal_thread.is_alive():
123 | logger.info("Stop event detected during SIO internal thread join")
124 | else:
125 | logger.info("FiscalberrySio internal thread finished.")
126 | else:
127 | logger.warning("self.start() did not return a running thread to wait for.")
128 |
129 | except socketio.exceptions.ConnectionError as e:
130 | logger.error(f"SIO Connection Error in instance thread: {e}", exc_info=False)
131 | except Exception as e:
132 | logger.error(f"Unhandled Exception in SIO instance thread: {e}", exc_info=True)
133 | finally:
134 | logger.info("Exiting _run_sio_instance.")
135 |
136 | def start(self):
137 | """Inicia y mantiene vivo el proceso de conexión SIO."""
138 | logger.info("Iniciando Fiscalberry SIO Service Loop")
139 | self._stop_event.clear()
140 | self.initial_retries = 0
141 |
142 | if self._stop_event.is_set():
143 | logger.info("Stop requested during initial UUID check.")
144 | return # Salir si se pidió detener
145 |
146 | # Enviar el discover al servidor la primera vez
147 | self.discover_thread = send_discover_in_thread()
148 | self.discover_thread.start()
149 |
150 | # --- Bucle principal de reconexión ---
151 | while not self._stop_event.is_set():
152 | # Creamos un hilo que ejecutará _run_sio_instance
153 | self.socketio_thread = threading.Thread(
154 | target=self._run_sio_instance,
155 | daemon=True # Daemon para que no bloquee la salida si el principal muere
156 | )
157 | logger.info("* * * * * SocketIO thread start.")
158 | self.socketio_thread.start()
159 |
160 | # En lugar de un join simple que puede bloquear indefinidamente,
161 | # usamos un join con timeout para verificar periódicamente el estado
162 | while self.socketio_thread.is_alive() and not self._stop_event.is_set():
163 | self.socketio_thread.join(timeout=1.0)
164 |
165 | logger.info("* * * * * SocketIO thread finished or stop requested.")
166 |
167 | # Si el hilo terminó, verificamos si fue por una señal de stop
168 | if self._stop_event.is_set():
169 | logger.info("Stop event received. Exiting SIO loop.")
170 | break # Salir del bucle while
171 |
172 | # Si no fue por stop, asumimos desconexión/error y reintentamos
173 | logger.warning("SIO thread terminated. Reconnecting in 5 seconds...")
174 | time.sleep(5)
175 |
176 | logger.info("Fiscalberry SIO Service Loop finished.")
177 |
178 | def _is_gui_mode(self):
179 | """Detecta si la aplicación está ejecutándose en modo GUI."""
180 | try:
181 | # Intentar importar kivy para verificar si está disponible
182 | import kivy
183 | from kivy.app import App
184 | # Si hay una app de Kivy ejecutándose, estamos en modo GUI
185 | return App.get_running_app() is not None
186 | except ImportError:
187 | # Si no se puede importar Kivy, definitivamente es CLI
188 | return False
189 | except:
190 | # Si hay cualquier otro error, asumir CLI
191 | return False
192 |
193 | def stop(self):
194 | """Detiene el bucle de servicios y la instancia SIO."""
195 | # Detectar automáticamente si estamos en modo GUI o CLI
196 | if self._is_gui_mode():
197 | return self.stop_for_gui()
198 | else:
199 | return self.stop_for_cli()
200 |
201 | def stop_for_cli(self):
202 | """Detiene el bucle de servicios específicamente para modo CLI."""
203 | logger.info("# # # Requesting SIO services stop (CLI mode)...")
204 | self._stop_event.set()
205 |
206 | self.sio.stop()
207 |
208 | if self.socketio_thread and self.socketio_thread.is_alive():
209 | # Dar tiempo para que termine limpiamente
210 | for _ in range(5): # Esperar hasta 5 segundos en incrementos de 1 segundo
211 | if not self.socketio_thread.is_alive():
212 | break
213 | time.sleep(1)
214 |
215 | if self.socketio_thread.is_alive():
216 | logger.warning("SIO thread did not stop within the timeout period.")
217 | else:
218 | logger.info("SIO thread already stopped or not started.")
219 |
220 | # Detener el hilo de discover si está activo
221 | if self.discover_thread and self.discover_thread.is_alive():
222 | logger.info("Stopping discover thread...")
223 | self.discover_thread.join(timeout=2)
224 | if self.discover_thread.is_alive():
225 | logger.warning("Discover thread did not stop within the timeout period.")
226 | else:
227 | logger.info("Discover thread already stopped or not started.")
228 |
229 | logger.info("SIO services stopped (CLI mode).")
230 |
231 | # Para CLI usamos sys.exit(0)
232 | sys.exit(0)
233 |
234 | return True # Esta línea ya no se ejecutará, pero la dejamos por compatibilidad
235 |
236 | def stop_for_gui(self):
237 | """Detiene el bucle de servicios específicamente para modo GUI."""
238 | logger.info("# # # Requesting SIO services stop (GUI mode)...")
239 | self._stop_event.set()
240 |
241 | self.sio.stop()
242 |
243 | if self.socketio_thread and self.socketio_thread.is_alive():
244 | # Dar tiempo para que termine limpiamente
245 | for _ in range(5): # Esperar hasta 5 segundos en incrementos de 1 segundo
246 | if not self.socketio_thread.is_alive():
247 | break
248 | time.sleep(1)
249 |
250 | if self.socketio_thread.is_alive():
251 | logger.warning("SIO thread did not stop within the timeout period.")
252 | else:
253 | logger.info("SIO thread already stopped or not started.")
254 |
255 | # Detener el hilo de discover si está activo
256 | if self.discover_thread and self.discover_thread.is_alive():
257 | logger.info("Stopping discover thread...")
258 | self.discover_thread.join(timeout=2)
259 | if self.discover_thread.is_alive():
260 | logger.warning("Discover thread did not stop within the timeout period.")
261 | else:
262 | logger.info("Discover thread already stopped or not started.")
263 |
264 | logger.info("SIO services stopped (GUI mode).")
265 |
266 | # Para GUI NO intentar cerrar Kivy desde aquí - evitar recursión
267 | # La aplicación GUI debe manejar su propio cierre desde on_stop()
268 | return True
269 |
270 | def _stop_services_only(self):
271 | """Detiene solo los servicios sin llamar a sys.exit() - para uso interno."""
272 | logger.info("# # # Requesting SIO services stop...")
273 | self._stop_event.set()
274 |
275 | self.sio.stop()
276 |
277 | if self.socketio_thread and self.socketio_thread.is_alive():
278 | # Dar tiempo para que termine limpiamente
279 | for _ in range(5): # Esperar hasta 5 segundos en incrementos de 1 segundo
280 | if not self.socketio_thread.is_alive():
281 | break
282 | time.sleep(1)
283 |
284 | if self.socketio_thread.is_alive():
285 | logger.warning("SIO thread did not stop within the timeout period.")
286 | else:
287 | logger.info("SIO thread already stopped or not started.")
288 |
289 | # Detener el hilo de discover si está activo
290 | if self.discover_thread and self.discover_thread.is_alive():
291 | logger.info("Stopping discover thread...")
292 | self.discover_thread.join(timeout=2)
293 | if self.discover_thread.is_alive():
294 | logger.warning("Discover thread did not stop within the timeout period.")
295 | else:
296 | logger.info("Discover thread already stopped or not started.")
297 |
298 | logger.info("SIO services stopped.")
299 |
300 | return True
301 |
302 |
303 |
304 |
--------------------------------------------------------------------------------