epok-toolkit 0.1.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of epok-toolkit might be problematic. Click here for more details.

@@ -1,3 +1,15 @@
1
- DEFAULT_EMAIL_SENDER = "no-reply@epok.ai"
2
- EMAIL_SUBJECT_PREFIX = "[Epok] "
3
- EPOK_WHATSAPP_DEFAULT = True
1
+ # Configuración de correo electrónico
2
+ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
3
+ EMAIL_HOST = 'smtp.gmail.com'
4
+ EMAIL_PORT = 465
5
+ EMAIL_USE_SSL = True
6
+ EMAIL_USE_TLS = False
7
+ EMAIL_DEFAULT_FROM_EMAIL = "no-reply@epok.ai"
8
+ EMAIL_HOST_USER = "no-reply@epok.ai"
9
+ EMAIL_HOST_PASSWORD = "your-email-password"
10
+
11
+
12
+ # Configuración de whatsApp
13
+ API_KEY = "your-whatsapp-api-key"
14
+ INSTANCE = "instance-id"
15
+ SERVER_URL = "https://your-server-url.com/"
@@ -0,0 +1 @@
1
+ from .email_async import send_email
@@ -0,0 +1 @@
1
+ from .whatsapp_instanced import send_whatsapp_message
@@ -0,0 +1,367 @@
1
+ import requests
2
+ from typing import Optional
3
+ from functools import wraps
4
+ from dataclasses import dataclass
5
+
6
+
7
+ def timeout_response(func):
8
+ @wraps(func)
9
+ def wrapper(*args, **kwargs):
10
+ try:
11
+ return func(*args, **kwargs)
12
+ except requests.Timeout:
13
+ print("La solicitud ha excedido el tiempo de espera.")
14
+ return HttpResponse(status_code=408, text="Timeout", json_data=None)
15
+ except requests.RequestException as e:
16
+ print(f"Error en la solicitud: {e}")
17
+ return HttpResponse(
18
+ status_code=500, text="Error", json_data={"error": str(e)}
19
+ )
20
+
21
+ return wrapper
22
+
23
+
24
+ def require_connection(method):
25
+ """
26
+ Decorador para métodos de WhatsappClient que necesitan una conexión activa.
27
+ Llama a `self.ensure_connected()` y solo ejecuta el método original si la
28
+ conexión se confirma; de lo contrario devuelve False.
29
+ """
30
+ from functools import wraps
31
+
32
+ @wraps(method)
33
+ def _wrapper(self, *args, **kwargs):
34
+ if not self.ensure_connected():
35
+ print("❌ No fue posible establecer conexión.")
36
+ return False
37
+ return method(self, *args, **kwargs)
38
+
39
+ return _wrapper
40
+
41
+
42
+ @dataclass
43
+ class HttpResponse:
44
+ status_code: int
45
+ text: str
46
+ json_data: Optional[dict] = None
47
+
48
+
49
+ class WhatsAppInstance:
50
+ def __init__(self, api_key: str, instance: str, server_url: str):
51
+ self.api_key = api_key
52
+ self.name_instance = instance
53
+ self.status = "disconnected"
54
+ self.server_url = server_url.rstrip("/")
55
+ self.headers = {"apikey": self.api_key, "Content-Type": "application/json"}
56
+
57
+ def create_instance(self) -> HttpResponse:
58
+ """Crea una nueva instancia de WhatsApp usando la API de Envole."""
59
+ url = f"{self.server_url}/instance/create"
60
+ payload = {
61
+ "instanceName": self.name_instance,
62
+ "integration": "WHATSAPP-BAILEYS",
63
+ "syncFullHistory": False,
64
+ }
65
+ response = requests.post(url, json=payload, headers=self.headers)
66
+ return HttpResponse(response.status_code, response.text, response.json())
67
+
68
+ def delete_instance(self) -> HttpResponse:
69
+ """Elimina una instancia de WhatsApp usando la API de Envole."""
70
+ url = f"{self.server_url}/instance/delete/{self.name_instance}"
71
+ response = requests.delete(url, headers=self.headers)
72
+ return HttpResponse(response.status_code, response.text)
73
+
74
+ def show_qr(self, qr_text: str) -> None:
75
+ """Genera un código QR a partir de `qr_text` y lo muestra con el visor por defecto."""
76
+ import qrcode
77
+
78
+ qr = qrcode.QRCode(border=2)
79
+ qr.add_data(qr_text)
80
+ qr.make(fit=True)
81
+ img = qr.make_image()
82
+ img.show()
83
+
84
+ def connect_instance_qr(self) -> None:
85
+ """Conecta una instancia de WhatsApp y muestra una imagen"""
86
+ url = f"{self.server_url}/instance/connect/{self.name_instance}"
87
+ response = requests.get(url, headers=self.headers)
88
+ codigo = response.json().get("code")
89
+ self.show_qr(codigo)
90
+
91
+ def mode_connecting(self):
92
+ """
93
+ Se intentará por 30 min el mantenter intentos de conexión a la instancia
94
+ generando un qr cada 10 segundos, si es exitoso se podra enviar un mensaje,
95
+ si después de eso no se conecta, se devolvera un error
96
+ """
97
+ pass
98
+
99
+
100
+ class WhatsAppSender:
101
+ def __init__(self, instance: WhatsAppInstance):
102
+ self.instance = instance.name_instance
103
+ self.server_url = instance.server_url
104
+ self.headers = instance.headers
105
+ self._instance_obj = instance
106
+ self.connected = True # estado de conexión conocido
107
+
108
+ def test_connection_status(self) -> bool:
109
+ cel_epok = "5214778966517"
110
+ print(f"Probando conexión enviando mensaje a {cel_epok}...")
111
+ ok = bool(self.send_text(cel_epok, "ping"))
112
+ self.connected = ok
113
+ return ok
114
+
115
+ @timeout_response
116
+ def get(self, endpoint: str, params: Optional[dict] = None) -> requests.Response:
117
+ url = f"{self.server_url}{endpoint}"
118
+ return requests.get(url, headers=self.headers, params=params)
119
+
120
+ def put(self, endpoint: str) -> requests.Response:
121
+ url = f"{self.server_url}{endpoint}"
122
+ return requests.put(url, headers=self.headers)
123
+
124
+ def post(self, endpoint: str, payload: dict):
125
+ url = f"{self.server_url}{endpoint}"
126
+ request = requests.post(url, json=payload, headers=self.headers, timeout=10)
127
+ # if timeout:
128
+ try:
129
+ return request
130
+ except requests.Timeout:
131
+ print("Request timed out")
132
+ return HttpResponse(status_code=408, text="Timeout", json_data=None)
133
+
134
+ def send_text(self, number: str, text: str, link_preview: bool = True, delay_ms: int = 0) -> str:
135
+ payload = {
136
+ "number": number,
137
+ "text": text,
138
+ "delay": delay_ms,
139
+ "linkPreview": link_preview,
140
+ }
141
+ print(f"Enviando mensaje a {number}: {text}")
142
+ resp = self.post(f"/message/sendText/{self.instance}", payload)
143
+
144
+ # Si la solicitud se convirtió en HttpResponse por timeout
145
+ status = resp.status_code if hasattr(resp, "status_code") else 0
146
+
147
+ if 200 <= status < 300:
148
+ self.connected = True
149
+ return resp.text
150
+
151
+ # Fallo: marcar desconexión y reportar
152
+ print(f"Error al enviar mensaje a {number}: {status} - {resp.text}")
153
+ self.connected = False
154
+ return False
155
+
156
+ def send_media(self, number: str, media_b64: str, filename: str, caption: str, mediatype: str = "document", mimetype: str = "application/pdf") -> str:
157
+ payload = {
158
+ "number": number,
159
+ "mediatype": mediatype,
160
+ "mimetype": mimetype,
161
+ "caption": caption,
162
+ "media": media_b64,
163
+ "fileName": filename,
164
+ "delay": 0,
165
+ "linkPreview": False,
166
+ "mentionsEveryOne": False,
167
+ }
168
+ resp = self.post(f"/message/sendMedia/{self.instance}", payload)
169
+ return resp.text
170
+
171
+ def send_sticker(self, number: str, sticker_b64: str, delay: int = 0, link_preview: bool = True, mentions_everyone: bool = True) -> str:
172
+ """Envía un sticker a un contacto específico."""
173
+ payload = {
174
+ "number": number,
175
+ "sticker": sticker_b64,
176
+ "delay": delay,
177
+ "linkPreview": link_preview,
178
+ "mentionsEveryOne": mentions_everyone,
179
+ }
180
+ resp = self.post(f"/message/sendSticker/{self.instance}", payload)
181
+ return resp.text
182
+
183
+ def send_location(self, number: str, name: str, address: str, latitude: float, longitude: float, delay: int = 0) -> str:
184
+ """Envía una ubicación a un contacto."""
185
+ payload = {
186
+ "number": number,
187
+ "name": name,
188
+ "address": address,
189
+ "latitude": latitude,
190
+ "longitude": longitude,
191
+ "delay": delay,
192
+ }
193
+ resp = self.post(f"/message/sendLocation/{self.instance}", payload)
194
+ return resp.text
195
+
196
+ def send_audio(self, number: str, audio_b64: str, delay: int = 0) -> str:
197
+ """Envía un audio en formato base64 a un contacto."""
198
+ payload = {
199
+ "audio": audio_b64,
200
+ "number": number,
201
+ "delay": delay,
202
+ }
203
+ resp = self.post(f"/message/sendWhatsAppAudio/{self.instance}", payload)
204
+ return resp.text
205
+
206
+ def connect(self, number: str) -> str:
207
+ querystring = {"number": number}
208
+ resp = self.get(f"/instance/connect/{self.instance}", params=querystring)
209
+ return resp.text
210
+
211
+ def set_webhook(self, webhook_url: str, enabled: bool = True, webhook_by_events: bool = True, webhook_base64: bool = True, events: Optional[list] = None) -> str:
212
+ """Configura el webhook para la instancia."""
213
+ if events is None:
214
+ events = ["SEND_MESSAGE"]
215
+ payload = {
216
+ "url": webhook_url,
217
+ "enabled": enabled,
218
+ "webhookByEvents": webhook_by_events,
219
+ "webhookBase64": webhook_base64,
220
+ "events": events,
221
+ }
222
+ resp = self.post(f"/webhook/set/{self.instance}", payload)
223
+ return resp.text
224
+
225
+ def fetch_groups(self, get_participants: bool = True) -> list:
226
+ """Obtiene todos los grupos y sus participantes."""
227
+ params = {"getParticipants": str(get_participants).lower()}
228
+ resp = self.get(f"/group/fetchAllGroups/{self.instance}", params=params)
229
+ if resp.status_code == 200:
230
+ return resp.json()
231
+ else:
232
+ raise Exception(
233
+ f"Error al obtener grupos: {resp.status_code} - {resp.text}"
234
+ )
235
+
236
+ @staticmethod
237
+ def fetch_instances(api_key: str, server_url: str) -> list:
238
+ """Obtiene todas las instancias disponibles en el servidor."""
239
+ url = f"{server_url}/instance/fetchInstances"
240
+ headers = {"apikey": api_key}
241
+ response = requests.get(url, headers=headers, verify=False)
242
+ # Puede ser una lista o dict, depende del backend
243
+ try:
244
+ return response.json()
245
+ except Exception:
246
+ return []
247
+
248
+ @staticmethod
249
+ def get_instance_info(api_key: str, instance_name: str, server_url: str):
250
+ """Busca la info de una instancia específica por nombre, robusto a diferentes formatos de respuesta."""
251
+ instances = WhatsAppSender.fetch_instances(api_key, server_url)
252
+
253
+ # Normalizar a lista para iterar
254
+ if isinstance(instances, dict):
255
+ instances = [instances]
256
+ # print(f"Buscando instancia: {instance_name} en {len(instances)} instancias disponibles.")
257
+ for item in instances:
258
+ data = (
259
+ item.get("instance")
260
+ if isinstance(item, dict) and "instance" in item
261
+ else item
262
+ )
263
+ # print(data)
264
+ if not isinstance(data, dict):
265
+ continue # Formato inesperado para us
266
+
267
+ if data.get("name") == instance_name:
268
+ return data
269
+ return {}
270
+
271
+
272
+
273
+ class WhatsappClient:
274
+ """
275
+ Cliente para interactuar con la API de WhatsApp.
276
+ """
277
+ def __init__(self, api_key: str, server_url: str, instance_name: str = "EPOK"):
278
+ self.instance = WhatsAppInstance(api_key, instance_name, server_url)
279
+ self.sender: Optional[WhatsAppSender] = None
280
+ self._auto_initialize_sender()
281
+
282
+ def _auto_initialize_sender(self):
283
+ """Solo asigna sender si la instancia está enlazada a WhatsApp."""
284
+ info = WhatsAppSender.get_instance_info(
285
+ self.instance.api_key, self.instance.name_instance, self.instance.server_url
286
+ )
287
+ if info.get("ownerJid"): # <- si tiene owner, significa que ya está enlazada
288
+ self.sender = WhatsAppSender(self.instance)
289
+
290
+ def ensure_connected(self, retries: int = 3, delay: int = 30) -> bool:
291
+ """
292
+ Garantiza que la instancia esté conectada.
293
+ Si aún no existe `self.sender`, intentará crearlo.
294
+ Si la prueba de conexión falla, muestra un QR y reintenta.
295
+ """
296
+ import time
297
+
298
+ # Si ya tenemos sender y está marcado como conectado, salimos rápido
299
+ if self.sender and getattr(self.sender, "connected", False):
300
+ return True
301
+
302
+ def _init_sender():
303
+ if self.sender is None:
304
+ # Intentar inicializar si la instancia ya está enlazada
305
+ info = WhatsAppSender.get_instance_info(
306
+ self.instance.api_key,
307
+ self.instance.name_instance,
308
+ self.instance.server_url,
309
+ )
310
+ if info.get("ownerJid"):
311
+ self.sender = WhatsAppSender(self.instance)
312
+
313
+ # Primer intento de inicializar el sender
314
+ _init_sender()
315
+
316
+ for attempt in range(1, retries + 1):
317
+ if self.sender and self.sender.test_connection_status():
318
+ return True
319
+
320
+ print(
321
+ f"[{attempt}/{retries}] Conexión no disponible, mostrando nuevo QR (espera {delay}s)…"
322
+ )
323
+ self.instance.connect_instance_qr() # muestra nuevo QR
324
+ time.sleep(delay)
325
+
326
+ # Reintentar inicializar sender después de mostrar QR
327
+ _init_sender()
328
+
329
+ print("❌ No fue posible establecer conexión después de varios intentos.")
330
+ return False
331
+
332
+ @require_connection
333
+ def send_text(self, number: str, text: str, link_preview: bool = True, delay_ms: int = 1000):
334
+ return self.sender.send_text(number, text, link_preview, delay_ms=delay_ms)
335
+
336
+ @require_connection
337
+ def send_media(self, number: str, media_b64: str, filename: str, caption: str, mediatype: str = "document", mimetype: str = "application/pdf"):
338
+ return self.sender.send_media(number, media_b64, filename, caption, mediatype, mimetype)
339
+
340
+ @require_connection
341
+ def send_sticker(self, number: str, sticker_b64: str, delay: int = 0, link_preview: bool = True, mentions_everyone: bool = True):
342
+ return self.sender.send_sticker(number, sticker_b64, delay, link_preview, mentions_everyone)
343
+
344
+ @require_connection
345
+ def send_location(self, number: str, name: str, address: str, latitude: float, longitude: float, delay: int = 0):
346
+ return self.sender.send_location(number, name, address, latitude, longitude, delay)
347
+
348
+ @require_connection
349
+ def send_audio(self, number: str, audio_b64: str, delay: int = 0):
350
+ return self.sender.send_audio(number, audio_b64, delay)
351
+
352
+ @require_connection
353
+ def connect_number(self, number: str):
354
+ return self.sender.connect(number)
355
+
356
+ @require_connection
357
+ def fetch_groups(self, get_participants: bool = True):
358
+ return self.sender.fetch_groups(get_participants)
359
+
360
+ def create_instance(self):
361
+ return self.instance.create_instance()
362
+
363
+ def delete_instance(self):
364
+ return self.instance.delete_instance()
365
+
366
+ def connect_instance_qr(self):
367
+ return self.instance.connect_instance_qr()
@@ -0,0 +1,22 @@
1
+ from .whatsapp import WhatsappClient
2
+ from django.conf import settings
3
+ from celery import shared_task
4
+
5
+
6
+ API_KEY = settings.API_KEY
7
+ INSTANCE = settings.INSTANCE
8
+ SERVER_URL = settings.SERVER_URL
9
+
10
+ client = WhatsappClient(api_key=API_KEY, server_url=SERVER_URL, instance_name=INSTANCE)
11
+
12
+ @shared_task(
13
+ bind=True,
14
+ autoretry_for=(ConnectionError, TimeoutError),
15
+ retry_backoff=True, # 2, 4, 8, 16… s
16
+ retry_jitter=True, # +- aleatorio
17
+ max_retries=3,
18
+ ignore_result=True, # <-- ¡importante!
19
+ )
20
+ def send_whatsapp_message(number: str, message: str):
21
+ return client.send_text(number, message)
22
+
@@ -0,0 +1 @@
1
+ from .ticket_pdf import TicketPDF
@@ -0,0 +1,246 @@
1
+ from reportlab.lib.utils import ImageReader
2
+ from reportlab.pdfbase import pdfmetrics
3
+ from datetime import date, datetime
4
+ from reportlab.pdfbase.ttfonts import TTFont
5
+ from reportlab.pdfgen import canvas
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from typing import Optional
9
+ from PIL import Image
10
+ import locale
11
+ from uuid import uuid4, UUID
12
+ import os
13
+ import qrcode
14
+ from io import BytesIO
15
+
16
+
17
+ # pip install reportlab qrcode
18
+
19
+
20
+ fuente_path = os.path.join(os.path.dirname(__file__), "fuentes", "Kollektif-Bold.ttf")
21
+ pdfmetrics.registerFont(TTFont("kollektif", fuente_path))
22
+
23
+
24
+ def hex_to_rgb(hex_color):
25
+ hex_color = hex_color.lstrip('#')
26
+ lv = len(hex_color)
27
+ return tuple(int(hex_color[i:i + lv // 3], 16) / 255.0 for i in range(0, lv, lv // 3))
28
+
29
+
30
+ class Config:
31
+ base_path = os.path.dirname(__file__)
32
+ plantillas = base_path + "/plantillas/"
33
+ plantilla_path = os.path.join(plantillas, "Ticket_congrats.png")
34
+ output_path = os.path.join(base_path, "ticket_final.pdf")
35
+ fuente = "Helvetica"
36
+ fuente_bold = "kollektif"
37
+ #bg_color = "#0082FF"
38
+ bg_color = "#FFFFFF"
39
+ font_color = "#000000"
40
+
41
+
42
+ class Contenedor:
43
+ def __init__(self, x, y, w, h, bold: bool = False):
44
+ self.x = x
45
+ self.y = y
46
+ self.w = w
47
+ self.h = h
48
+ self.color = Config.bg_color
49
+ self.font_size = 28
50
+ self.font_color = Config.font_color
51
+ self.fuente = Config.fuente_bold if bold else Config.fuente
52
+
53
+ def dibujar(self, c):
54
+ c.setFillColorRGB(*hex_to_rgb(self.color))
55
+ c.rect(self.x, self.y, self.w, self.h, stroke=0, fill=1)
56
+
57
+
58
+ def dibujar_texto(self, c, texto):
59
+ c.setFillColorRGB(*hex_to_rgb(self.font_color))
60
+ c.setFont(self.fuente, self.font_size)
61
+ ascent = pdfmetrics.getAscent(self.fuente) * self.font_size / 1000
62
+ descent = abs(pdfmetrics.getDescent(self.fuente) * self.font_size / 1000)
63
+ text_h = ascent + descent
64
+ baseline_x = self.x + self.w / 2
65
+ baseline_y = self.y + (self.h - text_h) / 2 + descent
66
+ c.drawCentredString(baseline_x, baseline_y, texto)
67
+
68
+
69
+ class ContenedorTexto(Contenedor):
70
+ def __init__(self, x, y, w, h, bold: bool = False, texto="Demo",
71
+ font_size=28,
72
+ font_color=None,
73
+ color=None,
74
+ fuente=None):
75
+ super().__init__(x, y, w, h, bold=bold)
76
+ self.texto = texto
77
+ if font_size:
78
+ self.font_size = font_size
79
+ if font_color:
80
+ self.font_color = font_color
81
+ if color:
82
+ self.color = color
83
+ if fuente:
84
+ self.fuente = fuente
85
+
86
+ def dibujar(self, c):
87
+ super().dibujar(c)
88
+ self.dibujar_texto(c, self.texto)
89
+
90
+
91
+ class ContenedorQR(Contenedor):
92
+ def __init__(self, x, y, w, h, texto="QR vacío", color=None):
93
+ super().__init__(x, y, w, h)
94
+ self.texto = texto
95
+ if color:
96
+ self.color = color
97
+
98
+ def dibujar(self, c):
99
+ super().dibujar(c)
100
+ qr = qrcode.QRCode(
101
+ version=3,
102
+ error_correction=qrcode.constants.ERROR_CORRECT_H, # type: ignore
103
+ box_size=10,
104
+ border=1,
105
+ )
106
+ qr.add_data(self.texto)
107
+ qr.make(fit=True)
108
+ qr_img = qr.make_image(fill_color="black", back_color="white").convert("RGB") # type: ignore
109
+ qr_img = qr_img.resize((self.w, self.h), Image.LANCZOS) # type: ignore
110
+ buffer = BytesIO()
111
+ qr_img.save(buffer, format="PNG")
112
+ buffer.seek(0)
113
+ c.drawImage(ImageReader(buffer), self.x, self.y, width=self.w, height=self.h)
114
+
115
+
116
+ @dataclass
117
+ class TicketPDF:
118
+ nombre_evento: str
119
+ fecha: Optional[date]
120
+ titulo_ticket: str
121
+ precio: float
122
+ edad_min: int
123
+ tipo_evento: str
124
+ direccion: str
125
+ ticket_actual: int
126
+ total_tickets: int
127
+ nombre_persona: str
128
+ uuid: Optional[UUID] = None
129
+ hora_evento: Optional[str] = None
130
+ qr : Optional[str] = None
131
+
132
+ def __post_init__(self):
133
+ if isinstance(self.fecha, str):
134
+ self.fecha = datetime.strptime(self.fecha, "%Y-%m-%d %H:%M")
135
+ elif isinstance(self.fecha, date) and not isinstance(self.fecha, datetime):
136
+ hora = self.hora_evento or "00:00"
137
+ self.fecha = datetime.combine(self.fecha, datetime.strptime(hora, "%H:%M").time())
138
+ if not self.uuid:
139
+ raise ValueError("UUID no puede ser None")
140
+
141
+ self.hora_evento = self.fecha.strftime("%H:%M") # type: ignore
142
+ self.width = 0
143
+ self.height = 0
144
+ self.font_size = 28
145
+ self.font_color = Config.font_color
146
+ self.color = Config.bg_color
147
+ self.fuente = Config.fuente
148
+ self.img = Image.open(Config.plantilla_path)
149
+ self.width, self.height = self.img.size
150
+ self.buffer = BytesIO()
151
+ self.c = canvas.Canvas(self.buffer, pagesize=(self.width, self.height))
152
+
153
+
154
+ def generate_ticket(self):
155
+ def y(px): return self.height - px
156
+ self.c.drawImage(ImageReader(Config.plantilla_path), 0, 0, width=self.width, height=self.height, mask='auto')
157
+
158
+ # NOMBRE EVENTO
159
+ nombre_evento = ContenedorTexto(x=260, y=y(210), w=1400, h=90, texto=self.nombre_evento, font_size=78, bold=True)
160
+ nombre_evento.dibujar(self.c)
161
+
162
+ # FECHA
163
+ SPANISH_DAYS = {
164
+ 'Monday': 'Lunes', 'Tuesday': 'Martes', 'Wednesday': 'Miercoles',
165
+ 'Thursday': 'Jueves', 'Friday': 'Viernes', 'Saturday': 'Sabado', 'Sunday': 'Domingo'
166
+ }
167
+ SPANISH_MONTHS = {
168
+ 'January': 'Enero', 'February': 'Febrero', 'March': 'Marzo', 'April': 'Abril',
169
+ 'May': 'Mayo', 'June': 'Junio', 'July': 'Julio', 'August': 'Agosto',
170
+ 'September': 'Septiembre', 'October': 'Octubre', 'November': 'Noviembre', 'December': 'Diciembre'
171
+ }
172
+ fecha_obj = self.fecha # type: ignore
173
+ day_name = SPANISH_DAYS[fecha_obj.strftime('%A')] # type: ignore
174
+ month_name = SPANISH_MONTHS[fecha_obj.strftime('%B')] # type: ignore
175
+ fecha = f"{day_name}, {fecha_obj.day} {month_name}".upper() #type: ignore
176
+ fecha = ContenedorTexto(x=570, y=y(320), w=900, h=90, texto=fecha, font_size=38, bold=True)
177
+ fecha.dibujar(self.c)
178
+
179
+ # TICKET TITULO
180
+ ticket_titulo = ContenedorTexto(x=260, y=y(525), w=950, h=90, texto=self.titulo_ticket, font_size=68, bold=True)
181
+ ticket_titulo.dibujar(self.c)
182
+
183
+ # PRECIO
184
+ precio = f"{self.precio:,.2f} MXN"
185
+ precio = ContenedorTexto(x=450, y=y(650), w=600, h=90, texto=precio, font_size=58, bold=True)
186
+ precio.dibujar(self.c)
187
+
188
+ # EDAD MINIMA
189
+ edad_min = str(self.edad_min)
190
+ edad_min = ContenedorTexto(x=345, y=y(760), w=80, h=30, texto=edad_min, font_size=18, bold=True)
191
+ edad_min.dibujar(self.c)
192
+
193
+ # HORA DE ACCESO
194
+ hora_evento = ContenedorTexto(x=670, y=y(760), w=80, h=30, texto=self.hora_evento, font_size=18, bold=True) # type: ignore
195
+ hora_evento.dibujar(self.c)
196
+
197
+ # TIPO DE EVENTO
198
+ tipo_evento = ContenedorTexto(x=995, y=y(760), w=150, h=30, texto=self.tipo_evento, font_size=18, bold=True)
199
+ tipo_evento.dibujar(self.c)
200
+
201
+ # DIRECCION
202
+ direccion = ContenedorTexto(x=200, y=y(950), w=1400, h=50, texto=self.direccion, font_size=38, bold=True)
203
+ direccion.dibujar(self.c)
204
+
205
+ # TICKET ACTUAL
206
+ texto = f"Ticket {self.ticket_actual} de {self.total_tickets}"
207
+ ticket_actual = ContenedorTexto(x=680, y=y(1035), w=340, h=50, texto=texto, font_size=28, bold=True)
208
+ ticket_actual.dibujar(self.c)
209
+
210
+ # NOMBRE PERSONA
211
+ nombre_persona = ContenedorTexto(x=1080, y=y(1035), w=760, h=60, texto=self.nombre_persona, font_size=48, bold=True)
212
+ nombre_persona.dibujar(self.c)
213
+
214
+ # UUID
215
+ uuid_string = self.uuid
216
+ # UUID
217
+ uuid_string = str(self.uuid)
218
+ uuid = ContenedorTexto(x=1390, y=y(770), w=400, h=30, texto=uuid_string, font_size=18, bold=True)
219
+ uuid.dibujar(self.c)
220
+ tam = 400
221
+ qr = ContenedorQR(x=1390, y=y(740), w=tam, h=tam, texto=uuid_string, color=Config.bg_color)
222
+ qr.dibujar(self.c)
223
+
224
+ self.c.showPage()
225
+ self.c.save()
226
+ self.buffer.seek(0)
227
+ return self.buffer.getvalue()
228
+
229
+
230
+
231
+
232
+ if __name__ == "__main__":
233
+ ticket = TicketPDF(
234
+ nombre_evento="14 aniversario Cerveza Libertad",
235
+ fecha=datetime(2025, 5, 31, 14, 30),
236
+ titulo_ticket="General",
237
+ precio=410,
238
+ edad_min=18,
239
+ tipo_evento="Aniversario",
240
+ direccion="Restaurante dentro de Hacienda del Conde",
241
+ ticket_actual=2,
242
+ total_tickets=5,
243
+ nombre_persona="Carolina Franco Medina",
244
+ uuid=uuid4(),
245
+ )
246
+ ticket.generate_ticket()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: epok-toolkit
3
- Version: 0.1.0
3
+ Version: 1.0.0
4
4
  Summary: A toolkit for building Django applications with Celery support
5
5
  Author-email: Fernando Leon Franco <fernanlee2131@gmail.com>
6
6
  License: MIT
@@ -0,0 +1,17 @@
1
+ epok_toolkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ epok_toolkit/apps.py,sha256=O3q3CcucJOHjlYIS0VgbKsbtim2hpng_FxpKEG_MlWs,486
3
+ epok_toolkit/default_settings.py,sha256=hxsRmsAcjk4ar-pRz_X6AI17mJ3192Ljbx-xBuP5rdY,452
4
+ epok_toolkit/email/__init__.py,sha256=pyJwysyVoq6DuYAG72fulsKFoOuAfjw3aBH7FhmYGHc,35
5
+ epok_toolkit/email/email_async.py,sha256=oC0WowWNUpTpXdxo6hJag5gWUaStxI6VBEArEKQxXko,1521
6
+ epok_toolkit/email/engine.py,sha256=IIifqRI9z76pHdrO5oSSZ25aP5txOTAgrj1JuVVPlMY,2387
7
+ epok_toolkit/email/templates.py,sha256=uO3gYn2iiGxcjxioaG066gGPts1m-3C1Gp7XatGgVOg,7578
8
+ epok_toolkit/messaging/__init__.py,sha256=kO7-PTlYLqxeuouqjp_3hqV0aVMosOAMMLh6h7HSBjA,53
9
+ epok_toolkit/messaging/whatsapp.py,sha256=TrMSiKzvnhWOotSDEGil1BGgOJ7jLK7h3MXKdW3zCJw,14114
10
+ epok_toolkit/messaging/whatsapp_instanced.py,sha256=FsyJNPgAonmo_9EYBGmfh4Ykovn7lXhzDyclyCepKno,627
11
+ epok_toolkit/pdf/__init__.py,sha256=Scb1iOYnVIUEiUVHLNaPmcigyD-jOSBs3ws5RmolMKE,33
12
+ epok_toolkit/pdf/ticket_pdf.py,sha256=6-QhnL2LCtWZeRIGnuQDeTybjMs39DnzohYx1mPQoPQ,8821
13
+ epok_toolkit-1.0.0.dist-info/licenses/LICENSE,sha256=iLDbGXdLSIOT5OsxzHCvtmxHtonE21GiFlS3LNkug4A,128
14
+ epok_toolkit-1.0.0.dist-info/METADATA,sha256=j2fxHYN14mUnmjkIdjKL4FB5Hr7DAMqPiEifrLjRWww,739
15
+ epok_toolkit-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ epok_toolkit-1.0.0.dist-info/top_level.txt,sha256=Wo72AqIFcfWwBGM5F5iGFw9PrO3WBnTSprFZIJk_pNg,13
17
+ epok_toolkit-1.0.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- epok_toolkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- epok_toolkit/apps.py,sha256=O3q3CcucJOHjlYIS0VgbKsbtim2hpng_FxpKEG_MlWs,486
3
- epok_toolkit/default_settings.py,sha256=SxRAoLm67uHhUHwz1-xeMaN6MRB8FiN0ZVlQZFDF27U,103
4
- epok_toolkit/email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- epok_toolkit/email/async_task.py,sha256=oC0WowWNUpTpXdxo6hJag5gWUaStxI6VBEArEKQxXko,1521
6
- epok_toolkit/email/engine.py,sha256=IIifqRI9z76pHdrO5oSSZ25aP5txOTAgrj1JuVVPlMY,2387
7
- epok_toolkit/email/templates.py,sha256=uO3gYn2iiGxcjxioaG066gGPts1m-3C1Gp7XatGgVOg,7578
8
- epok_toolkit-0.1.0.dist-info/licenses/LICENSE,sha256=iLDbGXdLSIOT5OsxzHCvtmxHtonE21GiFlS3LNkug4A,128
9
- epok_toolkit-0.1.0.dist-info/METADATA,sha256=sDgLv2prb5w5aJTKKR6OmQnHhl-qV40z5rDrJinUeHU,739
10
- epok_toolkit-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- epok_toolkit-0.1.0.dist-info/top_level.txt,sha256=Wo72AqIFcfWwBGM5F5iGFw9PrO3WBnTSprFZIJk_pNg,13
12
- epok_toolkit-0.1.0.dist-info/RECORD,,
File without changes