Compare commits

...

16 Commits
Usb ... main

14 changed files with 330 additions and 16 deletions

0
Api/__init__.py Normal file
View File

View File

@ -1,10 +1,13 @@
from fastapi import FastAPI, Response
from escpos.printer import Dummy, Network
from escpos.printer import Dummy
from escpos.printer import Network
import sys
import json
from pydantic import BaseModel
from datetime import datetime
import tempfile
from .qr_generator import QRCodeGenerator
app = FastAPI(
title="Print Server FastAPI",
@ -28,6 +31,7 @@ def print_bill(data, address, waiter):
# Imprime el encabezado
printer.set(align='center', bold=False, height=1, width=1)
printer.text(d["shop_name"]+'\n')
printer.text(d["shop_nit"]+'\n')
printer.text(d["shop_address"]+'\n')
printer.set(align='left', bold=False, height=1, width=1)
printer.textln('===============================================')
@ -63,6 +67,8 @@ def print_bill(data, address, waiter):
printer.textln('================================================')
text = "Descuento Realizado: "+str(d["total_discount"])+"\n"
printer.text(text)
text = "Propina: "+str(d["total_tip"])+"\n"
printer.text(text)
text = "Total (sin impuestos): "+str(d["untaxed_amount"])+"\n"
printer.text(text)
text = "Impuestos (INC): "+str(d["tax_amount"])+"\n"
@ -79,6 +85,17 @@ def print_bill(data, address, waiter):
printer.textln(text)
printer.set(align='center', bold=False, height=1, width=1)
printer.textln('==============================================\n')
if d["fe_cufe"]:
QR = QRCodeGenerator(d["fe_cufe"]).generate_qr()
with tempfile.NamedTemporaryFile(
delete=True, suffix='.png', dir='/tmp') as temp_file:
temp_filename = temp_file.name
QR.save(temp_filename)
printer.image(f"{temp_filename}")
printer.set(align='left', bold=False, height=1, width=1)
text = str("CUFE: " + d['cufe'])
printer.textln(text)
printer.set(align='center', bold=False, height=1, width=1)
printer.text("Sigue nuestras redes sociales\n")
printer.text("@bicipizza\n")
printer.text("Recuerde que la propina es voluntaria.\n")
@ -91,20 +108,20 @@ def print_bill(data, address, waiter):
printer.text(str(waiter)+'\n')
# Corta el papel (solo para impresoras que soportan esta función)
printer.cut()
printer.close()
# printer.cut()
# printer.close()
# Obtiene el contenido del ticket de prueba
ticket_contenido = printer.output
# Imprime el contenido en la consola
sys.stdout.write(ticket_contenido.decode('utf-8'))
sys.stdout.write(ticket_contenido.decode('utf-8', errors='ignore'))
def print_customer_order(data, address, waiter):
d = data
# Crea una instancia de la impresora ficticia
# printer = Network(str(address))
# printer.open()
printer = Dummy()
printer = Network(str(address))
printer.open()
# printer = Dummy()
# Imprime el encabezado
printer.set(align='center', bold=False, height=1, width=1)
format_date_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
@ -126,17 +143,28 @@ def print_customer_order(data, address, waiter):
for line in d["lines"]:
if line['type'] != 'title':
if combination_pizza and pizza < 2:
printer.set(align='center', bold=False, height=2, width=2, custom_size=True)
printer.set(
align='center',
bold=False,
height=2,
width=2,
custom_size=True)
pizza += 1
elif pizza >= 2:
combination_pizza = False
printer.set(
align='left', bold=False, height=2, width=2, custom_size=True
align='left',
bold=False,
height=2,
width=2, custom_size=True
)
else:
printer.set(
align='left', bold=False, height=2, width=2, custom_size=True
)
align='left',
bold=False,
height=2,
width=2,
custom_size=True)
text = line['product'] + " " + str(line['quantity']) + "\n"
printer.text(text)
@ -155,14 +183,20 @@ def print_customer_order(data, address, waiter):
printer.text("\nPIZZA COMBINADA\n")
combination_pizza = True
pizza = 0
# if d["deleted_lines"]:
# for line in d["deleted_lines"]:
# text = line['product'] + " " + str(
# line['quantity']) + " " + str(
# line['unit'])
# printer.text(text)
# Corta el papel (solo para impresoras que soportan esta función)
printer.cut()
printer.close()
# Obtiene el contenido del ticket de prueba
ticket_contenido = printer.output
# ticket_contenido = printer.output
# Imprime el contenido en la consola
sys.stdout.write(ticket_contenido.decode('utf-8'))
# sys.stdout.write(ticket_contenido.decode('utf-8', errors='replace'))
@app.post("/print_bill")
@ -174,7 +208,7 @@ def print_ticket_bill(info: Info):
data = json.loads(data.replace("'", "\""))
print_bill(data, address, waiter)
message = "!Impresión Realizada!"
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)
@ -188,7 +222,7 @@ def print_ticket_file_kitchen(info: Info):
data = json.loads(data.replace("'", "\""))
print_customer_order(data, address, waiter)
message = "!Impresión Realizada!"
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)
@ -202,6 +236,6 @@ def print_ticket_file_bar(info: Info):
data = json.loads(data.replace("'", "\""))
print_customer_order(data, address, waiter)
message = "!Impresión Realizada!"
message = "!Impresion Realizada!"
return Response(content=message, status_code=200)

23
Api/qr_generator.py Normal file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
import qrcode
class QRCodeGenerator:
"""Qr Generato"""
def __init__(self, url):
self.url = url
def generate_qr(self):
"""Genera un código QR a partir de la URL"""
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=6,
border=4,
)
qr.add_data(self.url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
return img

0
Api/test/__init__.py Normal file
View File

0
Api/test/fixtures/__init__.py vendored Normal file
View File

35
Api/test/fixtures/bill.json vendored Normal file
View File

@ -0,0 +1,35 @@
{"shop_name": "SE",
"shop_nit": "902929200",
"shop_address": "Calle Arriba 1234",
"fe_cufe": "https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey=936ef10e23f9fbae2d2c70869050b907b7af55a7d2c80a9f2e906450a8f98b20a88f7e4219fd0d02962f6e639b29ba20",
"cufe": "936ef10e23f9fbae2d2c70869050b907b7af55a7d2c80a9f2e906450a8f98b20a88f7e4219fd0d02962f6e639b29ba20",
"invoice": {
"invoice_number": "FPES2068",
"resolution": {
"resolution_number": "18764072418755",
"resolution_prefix": "FPES",
"valid_date_time_from": "2024-06-06",
"valid_date_time_to": "2026-06-06",
"from_number": 1, "to_number": 30000}
}, "party": "00-Consumidor Final",
"tax_identifier_type": "NIT",
"tax_identifier_code": "222222222",
"address": "Dg 74A #C-2-56", "city": "Medellín",
"zone": "CHIMENEA (CH)",
"table": "CH1",
"lines": [
{"type": "line", "product": "Club negra", "quantity": 1.0, "uom": "u", "unit_price": "9800.00", "taxes": "8.00%"},
{"type": "line", "product": "Club roja", "quantity": 1.0, "uom": "u", "unit_price": "9800.00", "taxes": "8.00%"},
{"type": "line", "product": "Aguila ligth", "quantity": 1.0, "uom": "u", "unit_price": "8600.00", "taxes": "8.00%"},
{"type": "line", "product": "Propinas", "quantity": 1.0, "uom": "u", "unit_price": "27333.00", "taxes": null}],
"total_discount": "0.00",
"untaxed_amount": "300666.30",
"tax_amount": "21866.67",
"total_tip": "10000",
"total": "322532.97",
"state": "CUENTA FINAL",
"payments": [
{"statement": "Transferencia TPV", "amount": "222532.00"},
{"statement": "Efectivo TPV", "amount": "100000.97"}
]
}

24
Api/test/fixtures/customer_order.json vendored Normal file
View File

@ -0,0 +1,24 @@
{
"party": "00-Consumidor Final",
"tax_identifier_type": "NIT",
"tax_identifier_code": "222222222",
"address": "Dg 74A #C-2-56",
"city": "Medellín",
"zone": "CHIMENEA (CH)",
"table": "CH1",
"lines": [
{"type": "line", "product": "Club negra", "description": "", "quantity": 1.0, "uom": "Unidad"},
{"type": "line", "product": "Club roja", "description": "", "quantity": 1.0, "uom": "Unidad"},
{"type": "line", "product": "Aguila ligth", "description": "", "quantity": 1.0, "uom": "Unidad"},
{"type": "line", "product": "Té hatsu", "description": "", "quantity": 1.0, "uom": "Unidad"},
{"type": "line", "product": "Aromatica frutos rojos", "description": "", "quantity": 1.0, "uom": "Unidad"},
{"type": "title", "product": null, "description": null, "quantity": null, "uom": null},
{"type": "line", "product": "Santiago Botero", "description": "", "quantity": 0.5, "uom": "Media Unidad"},
{"type": "line", "product": "Villa Clara", "description": "", "quantity": 0.5, "uom": "Media Unidad"},
{"type": "title", "product": null, "description": null, "quantity": null, "uom": null},
{"type": "line", "product": "Granma", "description": "", "quantity": 1.0, "uom": "Media Unidad"},
{"type": "title", "product": null, "description": null, "quantity": null, "uom": null},
{"type": "line", "product": "Camilo Cienfuegos", "description": "", "quantity": 0.5, "uom": "Media Unidad"},
{"type": "line", "product": "Propinas", "description": null, "quantity": 1.0, "uom": "Unidad"}],
"deleted_lines": []
}

View File

@ -0,0 +1,13 @@
{
"party": "00-Consumidor Final",
"tax_identifier_type": "NIT",
"tax_identifier_code": "222222222",
"address": "Dg 74A #C-2-56",
"city": "Medellín",
"zone": "SALON",
"table": "SL5",
"lines": [],
"deleted_lines": [
{"product": "Playa Girón", "quantity": -1.0, "unit": "Media Unidad"},
{"product": "Ciclobi", "quantity": -1.0, "unit": "Media Unidad"}
]}

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python3
from ..qr_generator import QRCodeGenerator
def test_generate_qr():
url = "https://www.gnu.org/"
qr_generator = QRCodeGenerator(url)
filename = qr_generator.generate_qr()
print(filename)

76
Api/test/test_main.py Normal file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
from fastapi.testclient import TestClient
from ..main import app
import json
client = TestClient(app)
def load_json(file_path):
with open(file_path, 'r') as filejson:
return json.load(filejson)
def test_print_bill():
test_info = {
"content": str(
json.dumps(
load_json('test/fixtures/bill.json')
)),
"ip_printer": "192.168.1.105",
"user_name": "Juan"
}
response = client.post("/print_bill", json=test_info)
assert response.status_code == 200
assert response.content.decode() == "!Impresion Realizada!"
def test_print_customer_order():
test_info = {
"content": str(
json.dumps(
load_json('test/fixtures/customer_order.json')
)),
"ip_printer": "192.168.1.100",
"user_name": "Juan"
}
response = client.post("/order_kitchen", json=test_info)
assert response.status_code == 200
assert response.content.decode() == "!Impresion Realizada!"
def test_print_customer_order_deleted_lines():
test_info = {
"content": str(
json.dumps(
load_json(
'test/fixtures/customer_order_deleted_lines.json')
)),
"ip_printer": "192.168.1.110",
"user_name": "Juan"
}
response = client.post("/order_kitchen", json=test_info)
assert response.status_code == 200
assert response.content.decode() == "!Impresion Realizada!"
def test_print_bar_order():
test_info = {
"content": str(
json.dumps(
load_json('test/fixtures/customer_order.json')
)),
"ip_printer": "192.168.1.110",
"user_name": "Juan"
}
response = client.post("/order_bar", json=test_info)
assert response.status_code == 200
assert response.content.decode() == "!Impresion Realizada!"

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
# Usa una imagen base de Python
FROM python:3.11-slim
# Establece el directorio de trabajo
WORKDIR /app
# Copia el archivo de requerimientos y lo instala
COPY ./requirements.txt /tmp/
RUN pip install --no-cache-dir -r /tmp/requirements.txt
# Copia el código fuente
COPY . .
# Expone el puerto que usará FastAPI
EXPOSE 8000
# Comando de arranque
CMD ["uvicorn", "Api.main:app", "--host", "0.0.0.0", "--port", "8000"]

62
Rakefile Normal file
View File

@ -0,0 +1,62 @@
require 'bundler/setup'
$:.unshift File.expand_path('../lib', __FILE__)
DOCKER_COMPOSE='docker-compose.yml'
desc 'entorno vivo'
namespace :live do
task :up do
compose('up', '--build', '-d', compose: DOCKER_COMPOSE)
end
desc 'monitorear salida'
task :tail do
compose('logs', '-f', 'escpos', compose: DOCKER_COMPOSE)
end
desc 'monitorear salida'
task :tail_end do
compose('logs', '-f', '-n 50', 'escpos', compose: DOCKER_COMPOSE)
end
desc 'detener entorno'
task :down do
compose('down', compose: DOCKER_COMPOSE)
end
desc 'detener entorno'
task :stop do
compose('stop', compose: DOCKER_COMPOSE)
end
desc 'eliminar entorno'
task :del do
compose('down', '-v', '--rmi', 'all', compose: DOCKER_COMPOSE)
end
desc 'reiniciar entorno'
task :restart do
compose('restart', compose: DOCKER_COMPOSE)
end
desc 'detener entorno'
task :stop do
compose('stop', compose: DOCKER_COMPOSE)
end
desc 'terminal'
task :sh do
compose('exec', 'escpos', 'bash')
end
end
desc 'iterar'
task :tdd do
compose('exec', 'escpos', "bash -c 'cd Api && flake8 *'")
compose('exec', 'escpos', "bash -c 'cd Api && pytest -vvv'")
end
def compose(*arg, compose: DOCKER_COMPOSE)
sh "docker compose -f #{compose} #{arg.join(' ')}"
end

12
docker-compose.yml Normal file
View File

@ -0,0 +1,12 @@
version: '3.8'
services:
# Servicio de FastAPI
escpos:
build: .
container_name: escpos
command: uvicorn Api.main:app --host 0.0.0.0 --port 8000 --reload
volumes:
- .:/app
ports:
- "8050:8000"

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
fastapi
uvicorn[standard]
httpx
pytest
escpos
qrcode
flake8