From b435bc51e947c92e911b4ff62fc7a513f31f01c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Z=C3=A1rate=20Montero?= Date: Mon, 27 Apr 2026 08:13:24 -0600 Subject: [PATCH 01/65] Primeros pasos de label --- src/sga/label_generator.py | 606 ++++++++++++++++++ .../sga/substance/detail_substance.html | 76 ++- src/sga/urls.py | 5 + src/sga/views/substance/views.py | 68 ++ 4 files changed, 754 insertions(+), 1 deletion(-) create mode 100644 src/sga/label_generator.py diff --git a/src/sga/label_generator.py b/src/sga/label_generator.py new file mode 100644 index 000000000..9833f0d68 --- /dev/null +++ b/src/sga/label_generator.py @@ -0,0 +1,606 @@ +# -*- coding: utf-8 -*- +""" +Generador de etiquetas GHS/SGA para reactivos químicos. +Módulo standalone sin dependencias de tkinter. + +Basado en el proyecto Etiquetador - Escuela de Química, UNA. +Cumple RTCR 481:2015 (SGA/GHS). +""" + +import io +import os +import re +import logging +import platform +from datetime import datetime + +from PIL import Image, ImageDraw, ImageFont +import qrcode + +logger = logging.getLogger(__name__) + +# Pictogramas GHS → nombres de archivo EPS +PICTOGRAMAS_OSHA = { + "Explosivo": "exploding_bomb.eps", + "Inflamable": "flame.eps", + "Comburente": "flame_over_circle.eps", + "Gas Comprimido": "gas_cylinder.eps", + "Corrosivo": "corrosion.eps", + "Tóxico": "skull_crossbones.eps", + "Peligro para la Salud": "health_hazard.eps", + "Peligro Ambiental": "environment.eps", + "Irritante": "exclamation.eps", +} + +# H-code → pictograma(s) según estándar GHS +HCODE_PICTOGRAMAS = { + 'H200': ['Explosivo'], 'H201': ['Explosivo'], 'H202': ['Explosivo'], + 'H203': ['Explosivo'], 'H204': ['Explosivo'], 'H205': ['Explosivo'], + 'H220': ['Inflamable'], 'H221': ['Inflamable'], 'H222': ['Inflamable'], + 'H223': ['Inflamable'], 'H224': ['Inflamable'], 'H225': ['Inflamable'], + 'H226': ['Inflamable'], 'H227': ['Inflamable'], 'H228': ['Inflamable'], + 'H229': ['Gas Comprimido'], + 'H240': ['Explosivo'], 'H241': ['Explosivo'], 'H242': ['Inflamable'], + 'H250': ['Inflamable'], 'H251': ['Inflamable'], 'H252': ['Inflamable'], + 'H260': ['Inflamable'], 'H261': ['Inflamable'], + 'H270': ['Comburente'], 'H271': ['Comburente'], 'H272': ['Comburente'], + 'H280': ['Gas Comprimido'], 'H281': ['Gas Comprimido'], + 'H290': ['Corrosivo'], + 'H300': ['Tóxico'], 'H301': ['Tóxico'], 'H302': ['Irritante'], + 'H304': ['Peligro para la Salud'], 'H305': ['Peligro para la Salud'], + 'H310': ['Tóxico'], 'H311': ['Tóxico'], 'H312': ['Irritante'], + 'H314': ['Corrosivo'], 'H315': ['Irritante'], 'H317': ['Irritante'], + 'H318': ['Corrosivo'], 'H319': ['Irritante'], + 'H330': ['Tóxico'], 'H331': ['Tóxico'], 'H332': ['Irritante'], + 'H334': ['Peligro para la Salud'], 'H335': ['Irritante'], 'H336': ['Irritante'], + 'H340': ['Peligro para la Salud'], 'H341': ['Peligro para la Salud'], + 'H350': ['Peligro para la Salud'], 'H351': ['Peligro para la Salud'], + 'H360': ['Peligro para la Salud'], 'H361': ['Peligro para la Salud'], + 'H362': ['Peligro para la Salud'], + 'H370': ['Peligro para la Salud'], 'H371': ['Peligro para la Salud'], + 'H372': ['Peligro para la Salud'], 'H373': ['Peligro para la Salud'], + 'H400': ['Peligro Ambiental'], 'H410': ['Peligro Ambiental'], + 'H411': ['Peligro Ambiental'], 'H412': ['Peligro Ambiental'], + 'H413': ['Peligro Ambiental'], 'H420': ['Peligro Ambiental'], +} + + +# ------------------------------------------------------------------ +# Fuentes +# ------------------------------------------------------------------ +_FUENTE_CACHE = {} +_FUENTE_RUTA_CACHE = {} + + +def _candidatos_fuente(bold): + candidatos = [] + sistema = platform.system() + if sistema == "Windows": + candidatos += [ + r"C:\Windows\Fonts\DejaVuSans-Bold.ttf" if bold else r"C:\Windows\Fonts\DejaVuSans.ttf", + r"C:\Windows\Fonts\arialbd.ttf" if bold else r"C:\Windows\Fonts\arial.ttf", + ] + elif sistema == "Darwin": + candidatos += [ + "/Library/Fonts/DejaVuSans-Bold.ttf" if bold else "/Library/Fonts/DejaVuSans.ttf", + ] + else: + candidatos += [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold + else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf" if bold + else "/usr/share/fonts/TTF/DejaVuSans.ttf", + "/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf" if bold + else "/usr/share/fonts/dejavu/DejaVuSans.ttf", + ] + return candidatos + + +def obtener_fuente(tamano, bold=False): + tamano = max(1, int(tamano)) + clave = (bold, tamano) + if clave in _FUENTE_CACHE: + return _FUENTE_CACHE[clave] + + if bold not in _FUENTE_RUTA_CACHE: + encontrada = None + for ruta in _candidatos_fuente(bold): + if os.path.exists(ruta): + encontrada = ruta + break + _FUENTE_RUTA_CACHE[bold] = encontrada + + ruta = _FUENTE_RUTA_CACHE[bold] + if ruta is None: + fuente = ImageFont.load_default() + else: + try: + fuente = ImageFont.truetype(ruta, tamano) + except Exception: + fuente = ImageFont.load_default() + + _FUENTE_CACHE[clave] = fuente + return fuente + + +# ------------------------------------------------------------------ +# Resolución de pictogramas desde H-codes +# ------------------------------------------------------------------ +def resolver_pictogramas(h_codes): + """Retorna lista de nombres de pictogramas a partir de H-codes.""" + pictogramas = set() + for code in h_codes: + partes = [p.strip() for p in code.replace(" + ", "+").split("+")] + for parte in partes: + if parte in HCODE_PICTOGRAMAS: + pictogramas.update(HCODE_PICTOGRAMAS[parte]) + return sorted(pictogramas) + + +def resolver_palabra_advertencia(warning_words_qs): + """Determina la palabra de advertencia más severa de un queryset de WarningWord.""" + palabra = "" + for ww in warning_words_qs: + nombre = (ww.name or "").strip().lower() + if nombre == "peligro": + return "PELIGRO" + elif nombre in ("atención", "atencion"): + palabra = "ATENCIÓN" + return palabra + + +# ------------------------------------------------------------------ +# Clase principal +# ------------------------------------------------------------------ +class LabelGenerator: + """Genera etiquetas GHS/SGA como imágenes PIL.""" + + def __init__(self, logo_izq_path=None, logo_der_path=None, + pictogramas_dir=None): + """ + Args: + logo_izq_path: Ruta al logo izquierdo (opcional). + logo_der_path: Ruta al logo derecho (opcional). + pictogramas_dir: Carpeta con archivos EPS de pictogramas OSHA. + """ + self.logo_izq = self._cargar_logo(logo_izq_path) + self.logo_der = self._cargar_logo(logo_der_path) + self.pictogramas_dir = pictogramas_dir + self.cache_pictogramas = {} + + @staticmethod + def _cargar_logo(path): + if path and os.path.exists(path): + try: + return Image.open(path).convert('RGBA') + except Exception as e: + logger.warning(f"No se pudo cargar logo {path}: {e}") + return None + + # --- Helpers para fórmulas con subíndices --- + @staticmethod + def _parsear_formula(formula): + tokens = [] + i = 0 + s = formula + while i < len(s): + if s[i] in ('_', '^'): + kind = 's' if s[i] == '_' else 'p' + i += 1 + if i < len(s) and s[i] == '{': + j = s.find('}', i + 1) + if j == -1: + contenido = s[i + 1:] + i = len(s) + else: + contenido = s[i + 1:j] + i = j + 1 + else: + contenido = s[i] if i < len(s) else '' + i += 1 + if contenido: + tokens.append((kind, contenido)) + else: + j = i + 1 + while j < len(s) and s[j] not in ('_', '^'): + j += 1 + tokens.append(('n', s[i:j])) + i = j + return tokens + + def _draw_text_subscript_suffix(self, draw, x, y, main_text, suffix, + font_main, font_sub, fill='black'): + draw.text((x, y), main_text, fill=fill, font=font_main) + bbox_main = draw.textbbox((0, 0), main_text, font=font_main) + w_main = bbox_main[2] - bbox_main[0] + h_main = bbox_main[3] - bbox_main[1] + bbox_sub = draw.textbbox((0, 0), suffix, font=font_sub) + h_sub = bbox_sub[3] - bbox_sub[1] + y_sub = y + h_main - h_sub + draw.text((x + w_main + 2, y_sub), suffix, fill=fill, font=font_sub) + return x + w_main + 2 + (bbox_sub[2] - bbox_sub[0]) + + def _draw_formula_with_markup(self, draw, x, y, formula, + font_main, font_sub, font_sup, fill='black'): + tokens = self._parsear_formula(formula) + cursor_x = x + h_main = draw.textbbox((0, 0), 'A', font=font_main)[3] + for kind, texto in tokens: + if kind == 'n': + draw.text((cursor_x, y), texto, fill=fill, font=font_main) + cursor_x += draw.textbbox((0, 0), texto, font=font_main)[2] + elif kind == 's': + h_s = draw.textbbox((0, 0), texto, font=font_sub)[3] + y_s = y + h_main - h_s + draw.text((cursor_x, y_s), texto, fill=fill, font=font_sub) + cursor_x += draw.textbbox((0, 0), texto, font=font_sub)[2] + elif kind == 'p': + y_p = y - int(h_main * 0.30) + draw.text((cursor_x, y_p), texto, fill=fill, font=font_sup) + cursor_x += draw.textbbox((0, 0), texto, font=font_sup)[2] + return cursor_x + + # --- Pictogramas --- + def obtener_pictograma(self, simbolo, tamano): + clave = f"{simbolo}_{tamano}" + if clave in self.cache_pictogramas: + return self.cache_pictogramas[clave] + + img = None + if self.pictogramas_dir and simbolo in PICTOGRAMAS_OSHA: + archivo = os.path.join(self.pictogramas_dir, PICTOGRAMAS_OSHA[simbolo]) + if os.path.exists(archivo): + try: + eps_img = Image.open(archivo) + base_size = 500 + w, h = eps_img.size + proporcion = base_size / max(w, h) + eps_img = eps_img.resize( + (int(w * proporcion), int(h * proporcion)), + Image.Resampling.LANCZOS + ) + if eps_img.mode != 'RGBA': + eps_img = eps_img.convert('RGBA') + img = eps_img.resize((tamano, tamano), Image.Resampling.LANCZOS) + except Exception as e: + logger.warning(f"Error procesando EPS {simbolo}: {e}") + + if img is None: + img = self._generar_pictograma_respaldo(simbolo, tamano) + self.cache_pictogramas[clave] = img + return img + + def _generar_pictograma_respaldo(self, simbolo, tamano): + img = Image.new('RGBA', (tamano, tamano), (255, 255, 255, 0)) + draw = ImageDraw.Draw(img) + margen = int(tamano * 0.08) + cx, cy = tamano // 2, tamano // 2 + puntos = [(cx, margen), (tamano - margen, cy), + (cx, tamano - margen), (margen, cy)] + draw.polygon(puntos, fill='white') + for i in range(3): + offset = i * 0.8 + draw.polygon([ + (puntos[0][0], puntos[0][1] + offset), + (puntos[1][0] - offset, puntos[1][1]), + (puntos[2][0], puntos[2][1] - offset), + (puntos[3][0] + offset, puntos[3][1]) + ], outline='#C8102E', width=max(2, int(tamano * 0.025))) + font = obtener_fuente(int(tamano * 0.3), bold=True) + if "Explosivo" in simbolo: + draw.ellipse([cx-tamano*0.2, cy-tamano*0.2, cx+tamano*0.2, cy+tamano*0.2], outline='black', width=3) + elif "Inflamable" in simbolo: + draw.ellipse([cx-tamano*0.15, cy-tamano*0.25, cx+tamano*0.15, cy+tamano*0.1], fill='#FF6600') + draw.polygon([(cx, cy-tamano*0.35), (cx-tamano*0.1, cy-tamano*0.1), (cx+tamano*0.1, cy-tamano*0.1)], fill='#FF6600') + elif "Comburente" in simbolo: + draw.ellipse([cx-tamano*0.2, cy-tamano*0.2, cx+tamano*0.2, cy+tamano*0.2], outline='black', width=3) + bbox = draw.textbbox((0,0), "O", font=font) + tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] + draw.text((cx-tw/2, cy-th/2), "O", fill='black', font=font) + elif "Gas Comprimido" in simbolo: + draw.rectangle([cx-tamano*0.2, cy-tamano*0.15, cx+tamano*0.2, cy+tamano*0.2], outline='black', width=2) + elif "Corrosivo" in simbolo: + draw.rectangle([cx-tamano*0.25, cy-tamano*0.15, cx-tamano*0.1, cy+tamano*0.2], fill='gray', outline='black') + draw.rectangle([cx+tamano*0.1, cy-tamano*0.1, cx+tamano*0.25, cy+tamano*0.15], fill='gray', outline='black') + elif "Tóxico" in simbolo: + draw.ellipse([cx-tamano*0.2, cy-tamano*0.25, cx-tamano*0.05, cy-tamano*0.1], fill='black') + draw.ellipse([cx+tamano*0.05, cy-tamano*0.25, cx+tamano*0.2, cy-tamano*0.1], fill='black') + elif "Irritante" in simbolo: + draw.rectangle([cx-5, cy-tamano*0.25, cx+5, cy-tamano*0.05], fill='black') + draw.ellipse([cx-5, cy+tamano*0.05, cx+5, cy+tamano*0.15], fill='black') + else: + bbox = draw.textbbox((0,0), simbolo[0] if simbolo else "?", font=font) + tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] + draw.text((cx-tw/2, cy-th/2), simbolo[0] if simbolo else "?", fill='black', font=font) + return img + + # ------------------------------------------------------------------ + # MÉTODO PRINCIPAL – CREAR ETIQUETA + # ------------------------------------------------------------------ + def crear_etiqueta(self, datos, ancho_mm=70, alto_mm=40, dpi=300): + """ + Genera una etiqueta GHS como imagen PIL. + + Args: + datos: dict con campos del reactivo (ver documentación). + ancho_mm: ancho de la etiqueta en milímetros. + alto_mm: alto de la etiqueta en milímetros. + dpi: resolución en puntos por pulgada. + + Returns: + PIL.Image.Image + """ + ancho_px = int(ancho_mm * dpi / 25.4) + alto_px = int(alto_mm * dpi / 25.4) + img = Image.new('RGB', (ancho_px, alto_px), color='white') + draw = ImageDraw.Draw(img) + margen = int(ancho_px * 0.025) + margen_int = int(ancho_px * 0.015) + + # 1. LOGOS Y TEXTO INSTITUCIONAL + alto_logo = int(alto_px * 0.16) + y_logo = margen + ancho_logo_izq = 0 + x_der = ancho_px - margen + + if self.logo_izq: + proporcion = alto_logo / self.logo_izq.height + ancho_logo_izq = int(self.logo_izq.width * proporcion) + logo_redim = self.logo_izq.resize((ancho_logo_izq, alto_logo), Image.Resampling.LANCZOS) + if logo_redim.mode == 'RGBA': + fondo = Image.new('RGB', logo_redim.size, (255, 255, 255)) + fondo.paste(logo_redim, mask=logo_redim.split()[3]) + logo_redim = fondo + img.paste(logo_redim, (margen, y_logo)) + + if self.logo_der: + proporcion = alto_logo / self.logo_der.height + ancho_logo_der = int(self.logo_der.width * proporcion) + logo_redim = self.logo_der.resize((ancho_logo_der, alto_logo), Image.Resampling.LANCZOS) + if logo_redim.mode == 'RGBA': + fondo = Image.new('RGB', logo_redim.size, (255, 255, 255)) + fondo.paste(logo_redim, mask=logo_redim.split()[3]) + logo_redim = fondo + x_der = ancho_px - margen - ancho_logo_der + img.paste(logo_redim, (x_der, y_logo)) + + lineas_institucion = [] + for linea in datos.get('lineas_institucion', []): + if linea.strip(): + lineas_institucion.append(linea.strip()) + if not lineas_institucion: + nombre_org = datos.get('organizacion', '') + if nombre_org: + lineas_institucion.append(nombre_org) + + if lineas_institucion: + font_inst = obtener_fuente(int(alto_px * 0.035), bold=True) + ancho_espacio = (x_der - (margen + ancho_logo_izq)) if self.logo_izq and self.logo_der else (ancho_px - 2 * margen) + for linea in lineas_institucion: + bbox = draw.textbbox((0, 0), linea, font=font_inst) + if bbox[2] - bbox[0] > ancho_espacio: + factor = ancho_espacio / (bbox[2] - bbox[0]) * 0.95 + font_inst = obtener_fuente(max(8, int(int(alto_px * 0.035) * factor)), bold=True) + break + alturas = [draw.textbbox((0, 0), l, font=font_inst)[3] - draw.textbbox((0, 0), l, font=font_inst)[1] for l in lineas_institucion] + alto_texto = sum(alturas) + (len(alturas) - 1) * (margen_int // 2) + y_inst = y_logo + (alto_logo - alto_texto) // 2 + for i, linea in enumerate(lineas_institucion): + bbox = draw.textbbox((0, 0), linea, font=font_inst) + x_centro = (ancho_px - (bbox[2] - bbox[0])) // 2 + draw.text((x_centro, y_inst), linea, fill='#000000', font=font_inst) + y_inst += alturas[i] + (margen_int // 2) + + y = y_logo + alto_logo + margen_int + draw.line([(margen, y), (ancho_px - margen, y)], fill='#CCCCCC', width=1) + y += margen_int + + # 2. FUENTES + font_nombre = obtener_fuente(int(alto_px * 0.08), bold=True) + font_sub_nombre = obtener_fuente(int(alto_px * 0.05), bold=True) + font_normal = obtener_fuente(int(alto_px * 0.045), bold=False) + font_sub_normal = obtener_fuente(int(alto_px * 0.029), bold=False) + font_sup_normal = obtener_fuente(int(alto_px * 0.029), bold=False) + font_pequena = obtener_fuente(int(alto_px * 0.035), bold=False) + font_muy_pequena_bold = obtener_fuente(int(alto_px * 0.03), bold=True) + font_pequena_bold = obtener_fuente(int(alto_px * 0.035), bold=True) + x = margen + + # 3. NOMBRE + nombre = datos.get('nombre', 'SIN NOMBRE').upper() + estado_fisico = datos.get('estado_fisico', '').strip() + max_ancho = int(ancho_px * 0.85) + palabras = nombre.split() + lineas = [] + linea_actual = "" + for palabra in palabras: + prueba = linea_actual + " " + palabra if linea_actual else palabra + bbox = draw.textbbox((0, 0), prueba, font=font_nombre) + if bbox[2] - bbox[0] <= max_ancho: + linea_actual = prueba + else: + if linea_actual: + lineas.append(linea_actual) + linea_actual = palabra + if linea_actual: + lineas.append(linea_actual) + + for i, linea in enumerate(lineas[:2]): + es_ultima = (i == min(len(lineas), 2) - 1) + if es_ultima and estado_fisico: + sufijo = "(líq)" if estado_fisico == "l" else f"({estado_fisico})" + self._draw_text_subscript_suffix(draw, x, y, linea, sufijo, font_nombre, font_sub_nombre) + else: + draw.text((x, y), linea, fill='black', font=font_nombre) + y += int(font_nombre.getbbox(linea)[3] * 1.2) + y += margen_int // 2 + + # 4. FÓRMULA, CAS, CONCENTRACIÓN + cursor_x = x + cursor_y = y + if datos.get('formula'): + label_f = "Fórmula: " + draw.text((cursor_x, cursor_y), label_f, fill='#333333', font=font_normal) + cursor_x += draw.textbbox((0, 0), label_f, font=font_normal)[2] + cursor_x = self._draw_formula_with_markup(draw, cursor_x, cursor_y, datos['formula'], + font_normal, font_sub_normal, font_sup_normal, fill='#333333') + if datos.get('cas'): + cursor_x += draw.textbbox((0, 0), " ", font=font_normal)[2] + if datos.get('cas'): + label_c = "CAS: " + draw.text((cursor_x, cursor_y), label_c, fill='#333333', font=font_normal) + cursor_x += draw.textbbox((0, 0), label_c, font=font_normal)[2] + draw.text((cursor_x, cursor_y), datos['cas'], fill='#333333', font=font_normal) + y = cursor_y + int(font_normal.getbbox("A")[3] + margen_int * 0.8) + + # 5. PICTOGRAMAS Y QR + simbolos = datos.get('simbolos', []) + tam_picto = int(alto_px * 0.15) + tamano_qr = int(alto_px * 0.20) + + qr_url = datos.get('qr_url', '') + if qr_url: + qr_obj = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=12, border=1) + qr_obj.add_data(qr_url) + qr_obj.make(fit=True) + qr_img = qr_obj.make_image(fill_color="black", back_color="white") + qr_img = qr_img.resize((tamano_qr, tamano_qr), Image.Resampling.LANCZOS) + if qr_img.mode != 'RGB': + qr_img = qr_img.convert('RGB') + img.paste(qr_img, (ancho_px - margen - tamano_qr, y)) + + if simbolos: + x_picto = x + for simb in simbolos[:5]: + picto = self.obtener_pictograma(simb, tam_picto) + img.paste(picto, (x_picto, y), picto) + x_picto += tam_picto + int(margen_int * 0.5) + + alto_bloque = max(tam_picto if simbolos else 0, tamano_qr if qr_url else 0) + y += alto_bloque + margen_int if alto_bloque else 0 + + # 6. PALABRA DE ADVERTENCIA + palabra = datos.get('palabra_advertencia', '') + if palabra: + color = '#C8102E' if palabra == 'PELIGRO' else '#FFA500' + draw.text((x, y), palabra, fill=color, font=font_nombre) + y += int(font_nombre.getbbox(palabra)[3] + margen_int * 0.8) + + # 7. H-CODES + if datos.get('frases_peligro'): + codigos_h = re.findall(r'H\d{3}(?:\d{2})?', datos['frases_peligro']) + if codigos_h: + draw.text((x, y), "Peligros: " + ", ".join(codigos_h[:5]), fill='black', font=font_pequena) + y += int(font_pequena.getbbox("A")[3] + margen_int * 0.5) + + # 8. P-CODES + if datos.get('consejos_prudencia'): + codigos_p = re.findall(r'P\d{3}(?:\d{2})?', datos['consejos_prudencia']) + if codigos_p: + draw.text((x, y), "Prudencia: " + ", ".join(codigos_p[:5]), fill='black', font=font_pequena) + y += int(font_pequena.getbbox("A")[3] + margen_int * 0.5) + + # 9. PIE + y_pie = alto_px - int(alto_px * 0.09) + textos_pie = [] + if datos.get('lote'): + textos_pie.append(f"Lote: {datos['lote']}") + if datos.get('fecha_caducidad'): + textos_pie.append(f"Caducidad: {datos['fecha_caducidad']}") + if datos.get('cantidad'): + textos_pie.append(f"Cantidad: {datos['cantidad']}") + if textos_pie: + texto_unido = " • ".join(textos_pie) + draw.text((margen, y_pie), texto_unido, fill='#444444', font=font_muy_pequena_bold) + + # 10. MARCO + palabra_borde = (datos.get('palabra_advertencia', '') or '').upper() + if 'PELIGRO' in palabra_borde: + color_borde = '#CD1719' + grosor = max(4, int(min(ancho_px, alto_px) * 0.012)) + elif 'ATENCI' in palabra_borde: + color_borde = '#034991' + grosor = max(4, int(min(ancho_px, alto_px) * 0.012)) + else: + color_borde = '#a7a7a9' + grosor = max(3, int(min(ancho_px, alto_px) * 0.008)) + for offset in range(grosor): + draw.rectangle([offset, offset, ancho_px - 1 - offset, alto_px - 1 - offset], + outline=color_borde) + + return img + + def crear_etiqueta_bytes(self, datos, ancho_mm=70, alto_mm=40, dpi=300, formato='PNG'): + """Genera la etiqueta y retorna bytes del archivo de imagen.""" + img = self.crear_etiqueta(datos, ancho_mm, alto_mm, dpi) + buf = io.BytesIO() + img.save(buf, format=formato) + buf.seek(0) + return buf.getvalue() + + +def generar_datos_desde_sustancia(substance): + """ + Convierte una instancia de sga.models.Substance a un dict + compatible con LabelGenerator.crear_etiqueta(). + """ + from sga.models import SGAComplement + + datos = { + 'nombre': substance.comercial_name or substance.uipa_name or '', + 'formula': '', + 'cas': '', + 'simbolos': [], + 'palabra_advertencia': '', + 'frases_peligro': '', + 'consejos_prudencia': '', + } + + # SubstanceCharacteristics + try: + chars = substance.substancecharacteristics + datos['formula'] = chars.molecular_formula or '' + datos['cas'] = chars.cas_id_number or '' + except Exception: + pass + + # H-codes desde danger_indications + h_codes = [] + warning_words = [] + for di in substance.danger_indications.all(): + h_codes.append(di.code) + if di.warning_words: + warning_words.append(di.warning_words) + + datos['frases_peligro'] = "\n".join( + f"{di.code} - {di.description}" for di in substance.danger_indications.all() + ) + + # P-codes desde SGAComplement o danger_indications.prudence_advice + p_codes = set() + for di in substance.danger_indications.all(): + for pa in di.prudence_advice.all(): + p_codes.add((pa.code, pa.name)) + + # También de SGAComplement si existe + try: + complement = SGAComplement.objects.filter(substance=substance).first() + if complement: + for pa in complement.prudence_advice.all(): + p_codes.add((pa.code, pa.name)) + if complement.warningword: + warning_words.append(complement.warningword) + except Exception: + pass + + datos['consejos_prudencia'] = "\n".join(f"{code} - {name}" for code, name in sorted(p_codes)) + + # Pictogramas + datos['simbolos'] = resolver_pictogramas([di.code for di in substance.danger_indications.all()]) + + # Palabra de advertencia + if warning_words: + datos['palabra_advertencia'] = resolver_palabra_advertencia(warning_words) + + return datos diff --git a/src/sga/templates/sga/substance/detail_substance.html b/src/sga/templates/sga/substance/detail_substance.html index 00db5b577..e21db09c3 100644 --- a/src/sga/templates/sga/substance/detail_substance.html +++ b/src/sga/templates/sga/substance/detail_substance.html @@ -192,4 +192,78 @@
- \ No newline at end of file + + +
+
+
+
{% trans 'Generate GHS Label' %}
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ {% trans 'Click "Generate Label" to preview' %} +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/sga/urls.py b/src/sga/urls.py index 7c15a30f8..af29eeb14 100644 --- a/src/sga/urls.py +++ b/src/sga/urls.py @@ -72,6 +72,11 @@ substance.detail_substance, name="detail_substance", ), + path( + "generate_label//", + substance.generate_label, + name="generate_label", + ), # sga/get_get_templateList path("labels/", editor.create_personal_template, name="add_personal"), path("edit_personal/", editor.edit_personal_template, name="edit_personal"), diff --git a/src/sga/views/substance/views.py b/src/sga/views/substance/views.py index 5e41e99a6..5299d9f90 100644 --- a/src/sga/views/substance/views.py +++ b/src/sga/views/substance/views.py @@ -884,3 +884,71 @@ def add_sga_provider(request, org_pk): {"result": True, "provider_pk": provider.pk, "provider": provider.name} ) return JsonResponse(response) + + +@login_required +@permission_required( + ("sga.view_substance", "auth_and_perms.institution_can_access"), + raise_exception=True, +) +def generate_label(request, org_pk, pk): + """Genera una etiqueta GHS/SGA para una sustancia como imagen PNG o PDF.""" + import os + import io + from sga.label_generator import LabelGenerator, generar_datos_desde_sustancia + + organization = get_object_or_404( + OrganizationStructure.objects.using(settings.READONLY_DATABASE), pk=org_pk + ) + user_is_allowed_on_organization(request.user, organization) + substance = get_object_or_404(Substance, pk=pk) + + # Generar datos desde la sustancia + datos = generar_datos_desde_sustancia(substance) + + # Datos opcionales del request (GET params) + datos['lote'] = request.GET.get('lote', '') + datos['fecha_caducidad'] = request.GET.get('fecha_caducidad', '') + datos['cantidad'] = request.GET.get('cantidad', '') + datos['organizacion'] = organization.name + + # Líneas institucionales opcionales + lineas = request.GET.getlist('linea_institucion') + if lineas: + datos['lineas_institucion'] = lineas + + # QR URL opcional + datos['qr_url'] = request.GET.get('qr_url', '') + + # Tamaño de etiqueta + ancho_mm = int(request.GET.get('ancho_mm', 70)) + alto_mm = int(request.GET.get('alto_mm', 40)) + + generador = LabelGenerator() + + formato = request.GET.get('formato', 'png').lower() + if formato == 'pdf': + from reportlab.pdfgen import canvas as pdf_canvas + from reportlab.lib.pagesizes import letter + from reportlab.lib.units import mm + import tempfile + + img = generador.crear_etiqueta(datos, ancho_mm, alto_mm, dpi=300) + + buf = io.BytesIO() + c = pdf_canvas.Canvas(buf, pagesize=letter) + with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp: + img.save(tmp.name, 'PNG') + c.drawImage(tmp.name, 20, letter[1] - alto_mm * mm - 20, + width=ancho_mm * mm, height=alto_mm * mm) + os.unlink(tmp.name) + c.save() + buf.seek(0) + response = HttpResponse(buf.getvalue(), content_type='application/pdf') + response['Content-Disposition'] = f'attachment; filename="etiqueta_{substance.pk}.pdf"' + return response + else: + img_bytes = generador.crear_etiqueta_bytes(datos, ancho_mm, alto_mm, dpi=200) + response = HttpResponse(img_bytes, content_type='image/png') + response['Content-Disposition'] = f'inline; filename="etiqueta_{substance.pk}.png"' + return response From abf3008cd36e3d8c1df86168e4f63b44c09b78f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Z=C3=A1rate=20Montero?= Date: Sat, 27 Jun 2026 02:20:42 -0600 Subject: [PATCH 02/65] Integra motor de etiquetas SGA (label_engine) coherente con el recipiente - Vendoriza label_engine en src/sga/label_engine/ (PNG/SVG/PDF, negociacion de tamano) - Capa de mapeo sga/label_blueprint.py para Substance, ShelfObject y DisplayLabel - Etiqueta cualquier ShelfObject: hereda color del estante y tamano segun capacidad - Renderer de acento con el color del contenedor; conserva el borde SGA por palabra de advertencia - Vista/URL laboratory:shelfobject_label + boton 'Imprimir etiqueta' en el detalle de ShelfObject - Recablea generate_label y unifica el preview del editor (engine_label_preview, label_in_png) - Elimina src/sga/label_generator.py; agrega reportlab; tests y traducciones --- plans/AGENT_BRIEFING.md | 337 + requirements.txt | 1 + .../locale/en/LC_MESSAGES/django.po | 601 ++ .../locale/es/LC_MESSAGES/django.po | 1574 ++- .../locale/es/LC_MESSAGES/djangojs.po | 143 +- .../laboratory/shelfobject_detail.html | 13 + src/laboratory/urls.py | 5 + src/laboratory/views/shelfobject.py | 34 + src/locale/en/LC_MESSAGES/django.po | 6104 +++++++++++- src/locale/es/LC_MESSAGES/django.po | 8578 +++++++++-------- src/locale/es/LC_MESSAGES/djangojs.po | 1294 +-- src/sga/label_blueprint.py | 338 + src/sga/label_engine/__init__.py | 89 + .../iso_7010_m002.GIKfkOch_Z1Nlr4c.eps.svg | 1 + .../iso_7010_m003.DYrcitbT_Z1Nlr4c.eps.svg | 1 + .../iso_7010_m004.BZLCx1Al_Z1Nlr4c.eps.svg | 1 + .../EPP/iso_7010_m007.DHP0HkAt_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m008.Dfei-e-a_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m009.C4a05Z0P_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m010.CMjDResu_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m013.BDtEfWhj_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m014.c5DcfJ-P_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m015.BJlzi0Nd_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m016.DqW2aYya_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m017.CR8IskbW_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m018.DiKYd9EM_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m019.Bh-p8kFN_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m023.CyBpQefX_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m027.BAVX0WYY_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m028.DOcqhIK0_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m032.CcVi0cPl_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m047.CNK9Z8H2_Z1Nlr4c.eps | 1 + .../EPP/iso_7010_m048.ywiY7UXB_ZbP5cz.eps | 1 + .../iso_7010_m007.DHP0HkAt_Z1Nlr4c.svg | 1 + .../iso_7010_m008.Dfei-e-a_Z1Nlr4c.svg | 1 + .../iso_7010_m009.C4a05Z0P_Z1Nlr4c.svg | 1 + .../iso_7010_m010.CMjDResu_Z1Nlr4c.svg | 1 + .../iso_7010_m013.BDtEfWhj_Z1Nlr4c.svg | 1 + .../iso_7010_m014.c5DcfJ-P_Z1Nlr4c.svg | 1 + .../iso_7010_m015.BJlzi0Nd_Z1Nlr4c.svg | 1 + .../iso_7010_m016.DqW2aYya_Z1Nlr4c.svg | 1 + .../iso_7010_m017.CR8IskbW_Z1Nlr4c.svg | 1 + .../iso_7010_m018.DiKYd9EM_Z1Nlr4c.svg | 1 + .../iso_7010_m019.Bh-p8kFN_Z1Nlr4c.svg | 1 + .../iso_7010_m023.CyBpQefX_Z1Nlr4c.svg | 1 + .../iso_7010_m027.BAVX0WYY_Z1Nlr4c.svg | 1 + .../iso_7010_m028.DOcqhIK0_Z1Nlr4c.svg | 1 + .../iso_7010_m032.CcVi0cPl_Z1Nlr4c.svg | 1 + .../iso_7010_m047.CNK9Z8H2_Z1Nlr4c.svg | 1 + .../iso_7010_m048.ywiY7UXB_ZbP5cz.svg | 1 + .../thumbs/iso_7010_m007.DHP0HkAt_Z1Nlr4c.png | Bin 0 -> 1692 bytes .../thumbs/iso_7010_m008.Dfei-e-a_Z1Nlr4c.png | Bin 0 -> 1456 bytes .../thumbs/iso_7010_m009.C4a05Z0P_Z1Nlr4c.png | Bin 0 -> 1592 bytes .../thumbs/iso_7010_m010.CMjDResu_Z1Nlr4c.png | Bin 0 -> 1405 bytes .../thumbs/iso_7010_m013.BDtEfWhj_Z1Nlr4c.png | Bin 0 -> 1658 bytes .../thumbs/iso_7010_m014.c5DcfJ-P_Z1Nlr4c.png | Bin 0 -> 1393 bytes .../thumbs/iso_7010_m015.BJlzi0Nd_Z1Nlr4c.png | Bin 0 -> 1257 bytes .../thumbs/iso_7010_m016.DqW2aYya_Z1Nlr4c.png | Bin 0 -> 1603 bytes .../thumbs/iso_7010_m017.CR8IskbW_Z1Nlr4c.png | Bin 0 -> 1878 bytes .../thumbs/iso_7010_m018.DiKYd9EM_Z1Nlr4c.png | Bin 0 -> 1516 bytes .../thumbs/iso_7010_m019.Bh-p8kFN_Z1Nlr4c.png | Bin 0 -> 1338 bytes .../thumbs/iso_7010_m023.CyBpQefX_Z1Nlr4c.png | Bin 0 -> 1620 bytes .../thumbs/iso_7010_m027.BAVX0WYY_Z1Nlr4c.png | Bin 0 -> 1895 bytes .../thumbs/iso_7010_m028.DOcqhIK0_Z1Nlr4c.png | Bin 0 -> 1489 bytes .../thumbs/iso_7010_m032.CcVi0cPl_Z1Nlr4c.png | Bin 0 -> 1614 bytes .../thumbs/iso_7010_m047.CNK9Z8H2_Z1Nlr4c.png | Bin 0 -> 1733 bytes .../thumbs/iso_7010_m048.ywiY7UXB_ZbP5cz.png | Bin 0 -> 1753 bytes src/sga/label_engine/assets/Escuela.png | Bin 0 -> 55758 bytes src/sga/label_engine/assets/imagen2.png | Bin 0 -> 38413 bytes .../assets/pictogramas_osha/corrosion.eps | Bin 0 -> 634342 bytes .../assets/pictogramas_osha/corrosion.svg | 149 + .../assets/pictogramas_osha/environment.eps | Bin 0 -> 1797718 bytes .../assets/pictogramas_osha/environment.svg | 69 + .../assets/pictogramas_osha/exclamation.eps | Bin 0 -> 603278 bytes .../assets/pictogramas_osha/exclamation.svg | 59 + .../pictogramas_osha/exploding_bomb.eps | Bin 0 -> 600918 bytes .../pictogramas_osha/exploding_bomb.svg | 274 + .../assets/pictogramas_osha/flame.eps | Bin 0 -> 613082 bytes .../assets/pictogramas_osha/flame.svg | 59 + .../pictogramas_osha/flame_over_circle.eps | Bin 0 -> 618254 bytes .../pictogramas_osha/flame_over_circle.svg | 64 + .../assets/pictogramas_osha/gas_cylinder.eps | Bin 0 -> 602874 bytes .../assets/pictogramas_osha/gas_cylinder.svg | 54 + .../assets/pictogramas_osha/health_hazard.eps | Bin 0 -> 636666 bytes .../assets/pictogramas_osha/health_hazard.svg | 119 + .../pictogramas_osha/skull_crossbones.eps | Bin 0 -> 646154 bytes .../pictogramas_osha/skull_crossbones.svg | 129 + src/sga/label_engine/config.py | 153 + src/sga/label_engine/engine.py | 75 + src/sga/label_engine/layout/__init__.py | 5 + src/sga/label_engine/layout/box.py | 32 + src/sga/label_engine/layout/canvas.py | 303 + src/sga/label_engine/layout/measurer.py | 97 + src/sga/label_engine/layout/planner.py | 822 ++ src/sga/label_engine/models.py | 111 + src/sga/label_engine/pdf.py | 91 + src/sga/label_engine/phrases_catalog.py | 277 + src/sga/label_engine/renderers/__init__.py | 31 + src/sga/label_engine/renderers/accent.py | 33 + src/sga/label_engine/renderers/border.py | 32 + src/sga/label_engine/renderers/logos.py | 27 + src/sga/label_engine/renderers/phrases.py | 39 + src/sga/label_engine/renderers/pictogram.py | 27 + src/sga/label_engine/renderers/qr.py | 37 + src/sga/label_engine/renderers/registry.py | 49 + src/sga/label_engine/renderers/rule.py | 17 + src/sga/label_engine/renderers/text.py | 152 + src/sga/label_engine/resources.py | 312 + src/sga/label_engine/utils.py | 31 + src/sga/label_generator.py | 606 -- src/sga/label_render.py | 65 + src/sga/templates/sgalabel/step_two.html | 12 + src/sga/tests/test_label_engine.py | 166 + src/sga/urls.py | 5 + src/sga/views/editor.py | 48 +- src/sga/views/substance/views.py | 71 +- 116 files changed, 17022 insertions(+), 6828 deletions(-) create mode 100644 plans/AGENT_BRIEFING.md create mode 100644 src/auth_and_perms/locale/en/LC_MESSAGES/django.po create mode 100644 src/sga/label_blueprint.py create mode 100644 src/sga/label_engine/__init__.py create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m002.GIKfkOch_Z1Nlr4c.eps.svg create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m003.DYrcitbT_Z1Nlr4c.eps.svg create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m004.BZLCx1Al_Z1Nlr4c.eps.svg create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m007.DHP0HkAt_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m008.Dfei-e-a_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m009.C4a05Z0P_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m010.CMjDResu_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m013.BDtEfWhj_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m014.c5DcfJ-P_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m015.BJlzi0Nd_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m016.DqW2aYya_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m017.CR8IskbW_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m018.DiKYd9EM_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m019.Bh-p8kFN_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m023.CyBpQefX_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m027.BAVX0WYY_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m028.DOcqhIK0_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m032.CcVi0cPl_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m047.CNK9Z8H2_Z1Nlr4c.eps create mode 100644 src/sga/label_engine/assets/EPP/iso_7010_m048.ywiY7UXB_ZbP5cz.eps create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m007.DHP0HkAt_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m008.Dfei-e-a_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m009.C4a05Z0P_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m010.CMjDResu_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m013.BDtEfWhj_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m014.c5DcfJ-P_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m015.BJlzi0Nd_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m016.DqW2aYya_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m017.CR8IskbW_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m018.DiKYd9EM_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m019.Bh-p8kFN_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m023.CyBpQefX_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m027.BAVX0WYY_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m028.DOcqhIK0_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m032.CcVi0cPl_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m047.CNK9Z8H2_Z1Nlr4c.svg create mode 100644 src/sga/label_engine/assets/EPP/svg_from_web/iso_7010_m048.ywiY7UXB_ZbP5cz.svg create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m007.DHP0HkAt_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m008.Dfei-e-a_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m009.C4a05Z0P_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m010.CMjDResu_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m013.BDtEfWhj_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m014.c5DcfJ-P_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m015.BJlzi0Nd_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m016.DqW2aYya_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m017.CR8IskbW_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m018.DiKYd9EM_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m019.Bh-p8kFN_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m023.CyBpQefX_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m027.BAVX0WYY_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m028.DOcqhIK0_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m032.CcVi0cPl_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m047.CNK9Z8H2_Z1Nlr4c.png create mode 100644 src/sga/label_engine/assets/EPP/thumbs/iso_7010_m048.ywiY7UXB_ZbP5cz.png create mode 100644 src/sga/label_engine/assets/Escuela.png create mode 100644 src/sga/label_engine/assets/imagen2.png create mode 100644 src/sga/label_engine/assets/pictogramas_osha/corrosion.eps create mode 100644 src/sga/label_engine/assets/pictogramas_osha/corrosion.svg create mode 100644 src/sga/label_engine/assets/pictogramas_osha/environment.eps create mode 100644 src/sga/label_engine/assets/pictogramas_osha/environment.svg create mode 100644 src/sga/label_engine/assets/pictogramas_osha/exclamation.eps create mode 100644 src/sga/label_engine/assets/pictogramas_osha/exclamation.svg create mode 100644 src/sga/label_engine/assets/pictogramas_osha/exploding_bomb.eps create mode 100644 src/sga/label_engine/assets/pictogramas_osha/exploding_bomb.svg create mode 100644 src/sga/label_engine/assets/pictogramas_osha/flame.eps create mode 100644 src/sga/label_engine/assets/pictogramas_osha/flame.svg create mode 100644 src/sga/label_engine/assets/pictogramas_osha/flame_over_circle.eps create mode 100644 src/sga/label_engine/assets/pictogramas_osha/flame_over_circle.svg create mode 100644 src/sga/label_engine/assets/pictogramas_osha/gas_cylinder.eps create mode 100644 src/sga/label_engine/assets/pictogramas_osha/gas_cylinder.svg create mode 100644 src/sga/label_engine/assets/pictogramas_osha/health_hazard.eps create mode 100644 src/sga/label_engine/assets/pictogramas_osha/health_hazard.svg create mode 100644 src/sga/label_engine/assets/pictogramas_osha/skull_crossbones.eps create mode 100644 src/sga/label_engine/assets/pictogramas_osha/skull_crossbones.svg create mode 100644 src/sga/label_engine/config.py create mode 100644 src/sga/label_engine/engine.py create mode 100644 src/sga/label_engine/layout/__init__.py create mode 100644 src/sga/label_engine/layout/box.py create mode 100644 src/sga/label_engine/layout/canvas.py create mode 100644 src/sga/label_engine/layout/measurer.py create mode 100644 src/sga/label_engine/layout/planner.py create mode 100644 src/sga/label_engine/models.py create mode 100644 src/sga/label_engine/pdf.py create mode 100644 src/sga/label_engine/phrases_catalog.py create mode 100644 src/sga/label_engine/renderers/__init__.py create mode 100644 src/sga/label_engine/renderers/accent.py create mode 100644 src/sga/label_engine/renderers/border.py create mode 100644 src/sga/label_engine/renderers/logos.py create mode 100644 src/sga/label_engine/renderers/phrases.py create mode 100644 src/sga/label_engine/renderers/pictogram.py create mode 100644 src/sga/label_engine/renderers/qr.py create mode 100644 src/sga/label_engine/renderers/registry.py create mode 100644 src/sga/label_engine/renderers/rule.py create mode 100644 src/sga/label_engine/renderers/text.py create mode 100644 src/sga/label_engine/resources.py create mode 100644 src/sga/label_engine/utils.py delete mode 100644 src/sga/label_generator.py create mode 100644 src/sga/label_render.py create mode 100644 src/sga/tests/test_label_engine.py diff --git a/plans/AGENT_BRIEFING.md b/plans/AGENT_BRIEFING.md new file mode 100644 index 000000000..303b9be39 --- /dev/null +++ b/plans/AGENT_BRIEFING.md @@ -0,0 +1,337 @@ +# Organilab — Agent Briefing + +> **Read this first.** This document exists so you don't have to explore the whole +> codebase before you can work in it. It maps the architecture and points you at the +> exact files where things live, so you can jump straight to the relevant code and +> search from there. It complements `CLAUDE.md` (which covers commands and a high-level +> overview); this briefing goes deeper on *where things are* and *how the pieces connect*. +> +> All paths are relative to the repo root. **All application code lives under `src/`.** + +--- + +## 1. What Organilab is + +A Django-based **laboratory management system** for Costa Rican institutions +(Spanish-first UI, version `2.0.0`). It supports multi-lab management, inventory +tracking (reactives / materials / equipment), reservations, chemical safety (SGA/GHS), +risk management, MSDS, academic procedures, and reporting. It is **multi-tenant**: data +is partitioned by organization. + +--- + +## 2. The mental model (internalize this) + +``` +OrganizationStructure (tree) + AbstractOrganizationRef (scoping) + ProfilePermission (RBAC) + = multi-tenant lab management +``` + +- **`OrganizationStructure`** is a tree of organizations (a `TreeNode`). Almost + everything belongs to an org. +- Most domain models inherit **`AbstractOrganizationRef`**, which gives them an + `organization` FK. Data access is scoped by filtering on `organization`. +- The current org is carried in the URL as **`org_pk`**: most routes look like + `///...`. Middleware reads `org_pk` and resolves what the user is + allowed to do *in that org*. +- Permissions are role-based per org via **`ProfilePermission`** + **`Rol`** (a custom + role model — **not** Django's `Group`). + +If you understand those four sentences, the rest of the codebase follows. + +--- + +## 3. Repository layout + +Everything is under `src/`. Django apps: + +| App | Purpose | +|-----|---------| +| `laboratory` | **Core domain.** Inventory: `Object`, `ShelfObject`, `Shelf`, `Furniture`, `LaboratoryRoom`, `Laboratory`, and the `OrganizationStructure` org tree. | +| `auth_and_perms` | User `Profile`, custom roles (`Rol`), fine-grained `ProfilePermission`, org-user management. | +| `presentation` | Abstract base models, main UI/landing, tutorials, feedback, QR. | +| `sga` | Chemical safety (Sistema Global Armonizado / GHS): substances, pictograms, danger indications, labels. | +| `risk_management` | Risk zones, incidents, buildings, regents, workdays. | +| `reservations_management` | Equipment/material reservation system. | +| `academic` | Academic procedures and training workflows. | +| `derb` | Dynamic form builder (Formio.js-based custom forms). | +| `msds` | Material Safety Data Sheet structure (doc tree + regulations). | +| `report` | Report generation & export pipeline. | +| `api` | Thin REST entry point; most API code lives in each app's `api/` submodule. | +| `authentication` | Auth backends (OIDC), request middleware, error handling. | +| `pending_tasks` | Workflow/pending-task management for users and roles. | + +Non-app directories: + +| Path | Purpose | +|------|---------| +| `src/organilab/` | Django project config: `settings.py`, `test_settings.py`, `urls.py`, `celery.py`, `wsgi.py`/`asgi.py`. `__init__.py` exports version + Celery app. | +| `src/locale/` | i18n translation files (`es`, `en`). | +| `src/organilab_test/` | Shared test infrastructure (base classes, fixtures). | +| `docs/` | Sphinx documentation. | +| `docker/` | `docker-compose.yml` and Dockerfile for local stack. | +| `Makefile` (root) | All dev commands (see §14). | + +--- + +## 4. Core domain models & where they live + +### `laboratory` — `src/laboratory/models.py` (the heart of the system) + +| Model | What it is | +|-------|------------| +| `Object` | Catalog entry for a reactive / material / equipment (type discriminator). Org-scoped. | +| `ShelfObject` | A physical instance of an `Object` on a `Shelf`: quantity, expiration, batch, status, concentration. | +| `Shelf` | Container inside `Furniture`; has capacity/units; can restrict object types; supports nesting. | +| `Furniture` | Cabinet/case holding shelves; belongs to a `LaboratoryRoom`; has grid position. | +| `LaboratoryRoom` | A room within a `Laboratory`. | +| `Laboratory` | A lab unit; FK to `OrganizationStructure`; responsible user, coordinates, geolocation. | +| `OrganizationStructure` | **The org tree** (`TreeNode`). Nested orgs; users via `UserOrganization`; roles via `Rol`. | +| `UserOrganization` | Through-model: User ↔ Organization with a membership type (administrator / manager / user). | +| `OrganizationStructureRelations` | Generic relation linking an org to any model (e.g. a `Laboratory`) via `ContentType`. | + +Related satellites worth knowing: `SustanceCharacteristics`, `ObjectFeatures`, +`ShelfObjectLog`, `ShelfObjectMaintenance`, `TranferObject`, `PrecursorReport`, +`Protocol`. + +### `auth_and_perms` — `src/auth_and_perms/models.py` + +| Model | What it is | +|-------|------------| +| `Profile` | One-to-one with Django `User`; phone, id_card, labs (M2M), workplace (M2M org), language, `show_tutorials`. | +| `Rol` | Custom role; M2M to Django `Permission`; name, color, description. **Not** a Django `Group`. | +| `ProfilePermission` | The RBAC join: `Profile` → `Rol` (M2M) → `OrganizationStructure`, plus a GenericFK to any object. This is how a user gets a role in a given org/object. | + +### `presentation` — `src/presentation/models.py` + +Home of the abstract base models (see §5), plus `Tutorial`/`TutorialStep`/ +`TutorialProgress`, `FeedbackEntry`, `Donation`, `QRModel`. + +### Other apps — key models (one-liners) + +| App | File | Key models | +|-----|------|-----------| +| `sga` | `src/sga/models.py` | `Substance` (+ `SubstanceCharacteristics`), `DangerIndication`, `WarningClass` (tree), `PrudenceAdvice`, `Pictogram`, `Label`, `TemplateSGA`. | +| `risk_management` | `src/risk_management/models.py` | `RiskZone`, `IncidentReport`, `Buildings`, `Regent`, `Workday`, `ZoneType`, `PriorityConstrain`. | +| `reservations_management` | `src/reservations_management/models.py` | `Reservations`, `ReservedProducts`, `ReservationTasks`, `ReservationRange`. | +| `academic` | `src/academic/models.py` | `Procedure`, `MyProcedure`, `ProcedureStep`, `CommentProcedureStep`. | +| `derb` | `src/derb/models.py` | `CustomForm`, `Section`/`Subsection`, `CustomFormField`, `FieldType`, `WidgetType`, `Validator`. | +| `msds` | `src/msds/models.py` | `OrganilabNode` (doc tree), `RegulationDocument`. | +| `report` | `src/report/models.py` | `TaskReport`, `DocumentReportStatus`, `ObjectChangeLogReport`, `RegencyReport`. | +| `pending_tasks` | `src/pending_tasks/models.py` | `PendingTask`, `PendingTaskManager`. | + +--- + +## 5. Base / abstract model patterns + +Recognize these three mixins — most models inherit one of them: + +| Mixin | Location | Provides | +|-------|----------|----------| +| `AbstractOrganizationRef` | `src/presentation/models.py` | `organization` (FK `OrganizationStructure`), `created_by`, `creation_date`, `last_update`. **The primary org-scoping mixin.** | +| `AbstractRegistry` | `src/presentation/models.py` | `created_by`, `creation_date`, `last_update`. Audit only, **no** org scope. | +| `BaseCreationObj` | `src/laboratory/models.py` | `created_by`, `creation_date`, `last_update`. Used by lab-domain models. | + +When adding a new model, ask: *should it be org-scoped?* If yes, inherit +`AbstractOrganizationRef`. + +--- + +## 6. Multi-tenancy & permission flow (end to end) + +How a request gets scoped and authorized: + +1. **URL carries `org_pk`** (and often `lab_pk`): `///...`. +2. **`ProfileMiddleware`** (`src/authentication/middleware.py`) runs `process_view()`: + reads `org_pk`/`lab_pk`, queries `ProfilePermission` for the user in that org, + collects all `Rol` permissions, and builds `request.user._perm_cache` (combined + ProfilePermission + direct user perms + group perms). This makes + `user.has_perm(...)` work for org-scoped permissions. +3. **Views validate access** with helpers — `user_is_allowed_on_organization(user, org)` + and `organization_can_change_laboratory(lab, org)` — and/or + `@permission_required(...)` decorators. +4. **ORM is filtered by org**, e.g. `Substance.objects.filter(organization_id=org_pk)`. + +Key supporting models: `ProfilePermission`, `Rol`, `UserOrganization` (membership +types: administrator / laboratory manager / laboratory user). Shared permission +utilities live in `src/auth_and_perms/organization_utils.py` (reused heavily across +the codebase — look here before writing a new access check). + +--- + +## 7. URL routing + +Root: **`src/organilab/urls.py`**. Apps are `include()`d, most under the +`//` convention. Main prefixes: + +| Prefix | App | +|--------|-----| +| `/laboratory/` | laboratory | +| `/sga//` | sga | +| `/risk//` | risk_management | +| `/msds//` | msds | +| `/derb//` | derb | +| `/academic//` | academic | +| `/reservations_management//` | reservations_management | +| `/perms/` | auth_and_perms (permission/org management) | +| `/report/` | report | +| `/pending_tasks/` | pending_tasks | +| `/api/` | api (per-app routers add their own paths) | + +Each app has its own `urls.py`. + +--- + +## 8. View layer conventions + +- **Class-based views** are the norm. Custom base views live in + **`src/laboratory/views/djgeneric.py`**: `CreateView`, `UpdateView`, `ListView`, + `DeleteView` — they extract `org_pk`/`lab_pk` from the URL and enforce access via + `user_is_allowed_on_organization()` / `organization_can_change_laboratory()`. Inherit + these to get org/lab checks for free. +- Views live in either `views.py` or a `views/` package per app (e.g. + `src/laboratory/views/` has 15+ modules: `furniture.py`, `shelfs.py`, + `organizations.py`, …; `src/sga/views/`, `src/derb/views/`, `src/auth_and_perms/views/`). +- Some **function-based views** remain, decorated with `@login_required` and + `@permission_required("app.perm")`. +- Reuse permission utilities from `src/auth_and_perms/organization_utils.py`. + +--- + +## 9. REST API (Django REST Framework) + +- The `api` app itself is thin (`src/api/`); the real API code is in each app's + **`api/` submodule**. The largest is **`src/laboratory/api/`** (e.g. `ObjectViewSet`, + `ProtocolViewSet`, `ProviderViewSet`, `LogEntryViewSet`). Others: `src/sga/api/`, + `src/derb/api/`, `src/reservations_management/api/`, `src/risk_management/api/`, + `src/academic/api/`, `src/report/api/`, `src/pending_tasks/api/`. +- ViewSets commonly extend **`AuthAllPermBaseObjectManagement`** from `djgentelella`. +- They are registered with DRF's `DefaultRouter()` inside each app's `urls.py`. +- Serializers live in each app's `api/serializers.py`. +- DRF config in `settings.py`: `TokenAuthentication` + `SessionAuthentication`, + `LimitOffsetPagination`, `PAGE_SIZE = 100`. `DjangoFilterBackend` / `SearchFilter` / + `OrderingFilter` are used. + +To add an endpoint: add a ViewSet/serializer under the app's `api/`, register it on a +router in the app's `urls.py`. + +--- + +## 10. Authentication + +- **Backends:** + - `src/auth_and_perms/authBackend.py` — `BCCRBackend`, Costa Rican digital-signature + auth (creates the user if missing). + - `src/authentication/oidc_backend.py` — `OrganiLabOIDCBackend` (extends + `mozilla_django_oidc`). On first login it creates a `Profile`, assigns a default org + (`DEFAULT_ORG_PK`) and default role (`DEFAULT_ROL_NAME`), and adds default groups. + Enabled when `OIDC_RP_CLIENT_ID` is configured. +- **Middleware highlights** (full stack in `settings.py`): + - `ProfileLanguageMiddleware` (`src/auth_and_perms/middleware.py`) — applies the + user's language; redirects users without a complete `Profile`. + - `ProfileMiddleware` (`src/authentication/middleware.py`) — caches org-scoped + permissions (see §6). + - `HandleErrorMiddleware` (`src/authentication/middleware.py`) — routes 403/404 to a + custom error page (skips JSON/XHR responses). + +--- + +## 11. Frontend + +- Admin UI uses **djgentelella** (Gentelella template). Base template: + `src/presentation/templates/base.html` (extends `gentelella/base.html`). +- Per-app `templates/` and `static/{css,js,img}` directories; shared assets in + `src/presentation/static/`. +- **Dynamic forms** use **Formio.js** in `derb`: `src/derb/static/formio/` + (`FormioController.js`, plus custom components `CustomSelect.js`, + `CustomTextInput.js`, `CustomSection.js`, and `formio.full.min.js`). +- **JS translations** via Django's `javascript-catalog` view; strings live in + `src/locale/{es,en}/LC_MESSAGES/djangojs.po` (compiled `.mo`). +- Custom template tags live in app-level `templatetags/` dirs. + +--- + +## 12. Settings & infrastructure + +- Settings: `src/organilab/settings.py`; test overrides in + `src/organilab/test_settings.py`. +- **Database:** PostgreSQL, configured via env vars — `DBNAME` (default `organilab`), + `DBUSER` (default `organilab_user`), `DBPASSWORD`, `DBHOST` (`127.0.0.1`), `DBPORT` + (`5432`). +- **Celery:** RabbitMQ broker (`BROKER_URL`), `django-db` result backend, + `django-celery-beat` for scheduling. In tests, `CELERY_TASK_ALWAYS_EAGER = True`. +- **Notable third-party libs:** `djgentelella` (UI + base object-management viewsets), + `djangorestframework`, `tree_queries` (TreeNode hierarchies), `mozilla_django_oidc`, + `django-otp` (TOTP), `weasyprint` (PDF), `location_field`, `async_notifications`, + Sentry/Glitchtip. +- **Scheduled tasks** (`CELERYBEAT_SCHEDULE`): daily emails, product-limit checks, + precursor reports (monthly), max-stock registration, shelf-object expiration emails, + establishment logs, org-lab relation cleanup. +- **i18n:** Spanish (`es`) is the production default; timezone `America/Costa_Rica`. + +--- + +## 13. Testing + +- Tests live in `src//tests/`. Most extend Django's `TestCase` (or a custom + `BaseLaboratorySetUpTest`). Shared base classes / fixtures in + `src/organilab_test/`. +- **Selenium** tests are tagged `@tag("selenium")` and excluded from normal runs; + base class `SeleniumBase` (`src/organilab_test/tests/base.py`). +- `test_settings.py`: MD5 password hasher (fast), Celery eager, logging suppressed, + appends `organilab_test` to `INSTALLED_APPS`, language `en`. +- Run: + - `make test` — all tests except selenium. + - `make single-test TEST=laboratory.tests.test_provider.ProviderViewTest` — one test. + - `make test-selenium` — selenium suite. +- Fixtures: `src/sga/fixtures/` (`catalog.json` GHS data, `sga_components.json`); loaded + via the `init_checks` management command. + +--- + +## 14. Tooling & entry points + +**Makefile** (root) — key targets: + +| Target | Does | +|--------|------| +| `make database_config` | Migrate + load permissions/fixtures (first-time setup). | +| `make migrate` | Run migrations. | +| `make test` / `make single-test TEST=...` | Run tests (see §13). | +| `make lint` | pycodestyle, max line length 200, migrations excluded. | +| `make messages` / `make trans` | Extract / compile translations. Run both after editing translatable strings. | +| `make run_celery` | Start a Celery worker. | +| `make build_docker` | Build the Docker image. | +| `make docs` | Build Sphinx docs. | + +**Management commands** (per-app `management/commands/`): `init_checks` (cache table + +fixtures), `load_urlname_permissions` (sync URL-based perms), and sga loaders +(`load_danger_substances`, `load_danger_indications`, `upload_pictograms`). + +**Docker:** `docker/docker-compose.yml` runs PostgreSQL (port 5431), RabbitMQ, Mailhog +(web UI on 8025), and the app. **CI:** `.github/workflows/tests.yml` runs `tox` +(Python 3.13 + PostgreSQL). + +--- + +## 15. Quick navigation cheat-sheet + +| I want to… | Start here | +|------------|-----------| +| Change inventory items | `src/laboratory/models.py` — `Object` / `ShelfObject` | +| Understand the org tree / multi-tenancy | `OrganizationStructure` + `UserOrganization` in `src/laboratory/models.py` | +| Add/modify a model | Pick a base mixin (§5); org-scoped → `AbstractOrganizationRef` (`src/presentation/models.py`) | +| Add a permission check | `src/auth_and_perms/organization_utils.py` + `ProfileMiddleware` (`src/authentication/middleware.py`) | +| Add a page/view | Inherit base views in `src/laboratory/views/djgeneric.py`; register in the app's `urls.py` | +| Add an API endpoint | App's `api/` submodule (viewset + serializer) + `DefaultRouter` in the app's `urls.py` | +| Touch chemical safety / labels | `src/sga/` | +| Touch dynamic forms | `src/derb/` + `src/derb/static/formio/` | +| Add a scheduled job | `CELERYBEAT_SCHEDULE` in `src/organilab/settings.py` + a task in the relevant app | +| Add/run tests | `src//tests/`; `make single-test TEST=...` | +| Edit user-facing strings | wrap in `gettext`, then `make messages` + `make trans` (`src/locale/es/...`) | +| Find a URL | Root `src/organilab/urls.py` → app `urls.py` | + +--- + +*Keep this file current as the architecture evolves — it's the first thing an agent +reads.* diff --git a/requirements.txt b/requirements.txt index 8af49e48e..cbb58df7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,7 @@ pyfva==0.0.43 django-otp==1.7.0 python-barcode==0.16.1 qrcode==8.2 +reportlab==5.0.0 sentry-sdk==2.49.0 legacy-cgi==2.6.4 setuptools<82 diff --git a/src/auth_and_perms/locale/en/LC_MESSAGES/django.po b/src/auth_and_perms/locale/en/LC_MESSAGES/django.po new file mode 100644 index 000000000..9e8bed9ae --- /dev/null +++ b/src/auth_and_perms/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,601 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-06-27 01:43-0600\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "No roles assigned" +msgstr "" + +msgid "Roles: manage the user's roles within the organization" +msgstr "" + +msgid "List Roles: show user's roles" +msgstr "" + +msgid "" +"Incognito mode: browse the system as this user without modifying your session" +msgstr "" + +msgid "" +"Inherit: propagate the user's profile and roles to all child organizations" +msgstr "" + +msgid "Remove: remove user from organization, can also disable platform access" +msgstr "" + +msgid "User not found, Sorry try to use add user button on organization list" +msgstr "" + +msgid "User exist on organization" +msgstr "" + +msgid "User not match with email" +msgstr "" + +msgid "Organization can't change this laboratory" +msgstr "" + +msgid "User doesn't have permissions" +msgstr "" + +msgid "Shelfobject does not belong to this laboratory." +msgstr "" + +msgid "Shelfobject does not belong to this organization." +msgstr "" + +msgid "Shelfobject does not belong to this shelf." +msgstr "" + +msgid "Shelf does not belong to this laboratory." +msgstr "" + +msgid "Shelf does not belong to this organization." +msgstr "" + +msgid "Organization cannot be inactive" +msgstr "" + +msgid "Roles" +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Laboratory modification not authorized" +msgstr "" + +#, python-format +msgid "Added the user %(user)r in the organization %(org)r" +msgstr "" + +#, python-format +msgid "Removed the groups %(groups)r from the profile %(profile)r" +msgstr "" + +#, python-format +msgid "Added the groups %(groups)r to the profile %(profile)r" +msgstr "" + +msgid "Profile was updated successfully." +msgstr "" + +msgid "You don't have permissions to access this section" +msgstr "" + +msgid "Organization parameter is required" +msgstr "" + +msgid "Rols" +msgstr "" + +msgid "Organization name" +msgstr "" + +msgid "Digital signature" +msgstr "" + +msgid "Validation method" +msgstr "" + +msgid "new-password" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "First name" +msgstr "" + +msgid "Last name" +msgstr "" + +msgid "Email address" +msgstr "" + +msgid "Phone" +msgstr "" + +msgid "Identification" +msgstr "" + +msgid "Job Position" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "It will used to login when you want to login with digital signature" +msgstr "" + +msgid "Laboratories" +msgstr "" + +msgid "User" +msgstr "" + +msgid "Laboratory" +msgstr "" + +msgid "Inactive organization" +msgstr "" + +msgid "Clone organization" +msgstr "" + +msgid "Change organization name" +msgstr "" + +msgid "Active organization" +msgstr "" + +msgid "Actions" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Profile" +msgstr "" + +msgid "Groups" +msgstr "" + +msgid "Organization" +msgstr "" + +msgid "Search by object" +msgstr "" + +msgid "Filter by roles" +msgstr "" + +msgid "Filter by organization" +msgstr "" + +msgid "Organization not found" +msgstr "" + +msgid "Address" +msgstr "" + +msgid "Show tutorials" +msgstr "" + +msgid "Enable automatic tutorial display on page visits" +msgstr "" + +msgid "Workplace" +msgstr "" + +msgid "Can add external user to organization" +msgstr "" + +msgid "Institution can access" +msgstr "" + +msgid "Can change own user/profile data" +msgstr "" + +msgid "permissions" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Rol" +msgstr "" + +msgid "Profile Rol" +msgstr "" + +msgid "Profile Rols" +msgstr "" + +msgid "Impostor" +msgstr "" + +msgid "Logged in as" +msgstr "" + +msgid "Impostor's IP address" +msgstr "" + +msgid "Logged on" +msgstr "" + +msgid "Logged out" +msgstr "" + +msgid "Token" +msgstr "" + +msgid "Impostor log" +msgstr "" + +msgid "Impostor logs" +msgstr "" + +#, python-format +msgid "User %(user)s not allowed on organization %(organization)r " +msgstr "" + +msgid "You can modify this laboratory" +msgstr "" + +msgid "Register with new organization" +msgstr "" + +msgid "Register you as organization administrator" +msgstr "" + +msgid "Create user" +msgstr "" + +msgid "" +"We need to validate that you are not a robot or spamer, so you need to " +"configure a Two-Factor Authentication" +msgstr "" + +msgid "" +"You need an OTP generator to login on the platform, you can use a free app " +"called FreeOTP:" +msgstr "" + +msgid "" +"For Costa Rica citicen, use your digital signature to login on organilab" +msgstr "" + +msgid "Configure your profile" +msgstr "" + +msgid "Validate user" +msgstr "" + +msgid "How use Digital Signature" +msgstr "" + +msgid "" +"If you don't have Digital signature, please request on your favorite bank" +msgstr "" + +msgid "Configure your machine using this link" +msgstr "" + +msgid "Download and install Gaudi" +msgstr "" + +msgid "Connect you card on computer and wait for connect success popup " +msgstr "" + +msgid "Your OTP Seed" +msgstr "" + +msgid "This image will not available in the future, please save it" +msgstr "" + +msgid "Profile create successfully" +msgstr "" + +msgid "" +"Your organization was created successfully, please login with your " +"credentials" +msgstr "" + +msgid "Login" +msgstr "" + +msgid "Laboratory and Organization List" +msgstr "" + +msgid "By laboratory" +msgstr "" + +msgid "By organization" +msgstr "" + +msgid "Organizations List" +msgstr "" + +msgid "Laboratories List" +msgstr "" + +msgid "User roles list" +msgstr "" + +msgid "Close" +msgstr "" + +msgid "Add organization" +msgstr "" + +msgid "Click to manage this organization" +msgstr "" + +msgid "Choose" +msgstr "" + +msgid "Add new rol" +msgstr "" + +msgid "Add" +msgstr "" + +msgid "Add User" +msgstr "" + +msgid "Related Users" +msgstr "" + +msgid "Add Laboratory" +msgstr "" + +msgid "Related Laboratory" +msgstr "" + +msgid "Disable child organizations filter" +msgstr "" + +msgid "Enable child organizations filter" +msgstr "" + +msgid "Log List" +msgstr "" + +msgid "Show Roles information" +msgstr "" + +msgid "View administrators" +msgstr "" + +msgid "Change parent of organization" +msgstr "" + +msgid "Rol List" +msgstr "" + +msgid "Delete organization" +msgstr "" + +msgid "By profile" +msgstr "" + +msgid "Rol by laboratory" +msgstr "" + +msgid "Add or restrict permission on this laboratory" +msgstr "" + +msgid "Relate user with this laboratory" +msgstr "" + +msgid "Rol in all organization" +msgstr "" + +msgid "Rol used when you don't have rol set on laboratory" +msgstr "" + +msgid "Relate user with this organization" +msgstr "" + +msgid "Help Text" +msgstr "" + +msgid "" +"This section will assign permissions groups to some profile who belongs to " +"selected organization. If you do not find the user in profile selector, " +"could you add it from By organization tab." +msgstr "" + +msgid "Show descriptions of groups" +msgstr "" + +msgid "Save changes" +msgstr "" + +msgid "Add New Rol" +msgstr "" + +msgid "Copy Rols" +msgstr "" + +msgid "Rol name" +msgstr "" + +msgid "Add a description" +msgstr "" + +msgid "Copy permissions from other roles?" +msgstr "" + +msgid "Select roles to copy" +msgstr "" + +msgid "Create Organization" +msgstr "" + +msgid "Linking laboratories of the parent organization" +msgstr "" + +msgid "How to deal with permission merge" +msgstr "" + +msgid "Append" +msgstr "" + +msgid "Sustract" +msgstr "" + +msgid "Only labs selected" +msgstr "" + +msgid "Organization parent:" +msgstr "" + +msgid "Select an organization to be parent of" +msgstr "" + +msgid "Relate user to laboratory" +msgstr "" + +msgid "From my organization" +msgstr "" + +msgid "External user" +msgstr "" + +msgid "Important" +msgstr "" + +msgid "" +"In this tab, you add the existing users in Organilab who are not associated " +"with the organization, so you need to know the user's email address to add " +"them." +msgstr "" + +msgid "" +"Once added, you can search for the profile in the organization tab to relate " +"user to organization" +msgstr "" + +msgid "Find user" +msgstr "" + +msgid "Actions for Organization" +msgstr "" + +msgid "Do you want to enable the child organizations filter?" +msgstr "" + +msgid "Administrators" +msgstr "" + +msgid "My laboratories" +msgstr "" + +msgid "Reports" +msgstr "" + +msgid "Risk Zones" +msgstr "" + +msgid "Admin Informs" +msgstr "" + +msgid "MSDS" +msgstr "" + +msgid "SGA" +msgstr "" + +msgid "Disposal" +msgstr "" + +msgid "Manage Rol" +msgstr "" + +msgid "Only rols selected" +msgstr "" + +msgid "Manage Users" +msgstr "" + +msgid "Delete Organization Rol" +msgstr "" + +msgid "Are you sure you want to delete" +msgstr "" + +msgid "Current permissions" +msgstr "" + +msgid "Confirm" +msgstr "" + +msgid "Roles registered on this organization" +msgstr "" + +msgid "Edit Role" +msgstr "" + +msgid "User List" +msgstr "" + +msgid "Clear filters" +msgstr "" + +msgid "User roles in organization" +msgstr "" + +msgid "User roles in laboratory" +msgstr "" + +msgid "Organization doesn't exists" +msgstr "" + +msgid "Element saved successfully" +msgstr "" + +msgid "Error, form is invalid" +msgstr "" + +#, python-format +msgid "" +"You have active impostor session Finish it" +msgstr "" + +msgid "Request User is the same as current user" +msgstr "" + +msgid "User not in organization" +msgstr "" + +msgid "Form data is wrong, you need to pass a valid laboratory as contenttype" +msgstr "" + +msgid "Role updated successfully" +msgstr "" + +msgid "Organization Management" +msgstr "" + +msgid "" +"You have no creation process, maybe it was expired, please try to register " +"again" +msgstr "" diff --git a/src/auth_and_perms/locale/es/LC_MESSAGES/django.po b/src/auth_and_perms/locale/es/LC_MESSAGES/django.po index b0f78424f..9325720b7 100644 --- a/src/auth_and_perms/locale/es/LC_MESSAGES/django.po +++ b/src/auth_and_perms/locale/es/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-04-10 14:02-0600\n" +"POT-Creation-Date: 2026-06-27 01:43-0600\n" "PO-Revision-Date: 2025-04-10 14:07-0600\n" "Last-Translator: \n" "Language-Team: \n" @@ -19,39 +19,8 @@ msgstr "" "1 : 2;\n" "X-Generator: Poedit 3.2.2\n" -# Common actions -msgid "Add" -msgstr "Agregar" - -msgid "Update" -msgstr "Actualizar" - -msgid "Delete" -msgstr "Eliminar" - -msgid "View" -msgstr "Ver" - -msgid "List" -msgstr "Listar" - -msgid "Manage" -msgstr "Gestionar" - -msgid "Create" -msgstr "Crear" - -msgid "Change" -msgstr "Actualizar" - -msgid "Switch to other user" -msgstr "Actualizar a otro usuario" - -msgid "Incognito mode: browse the system as this user without modifying your session" -msgstr "Modo incógnito: navega el sistema como este usuario sin modificar tu sesión" - -msgid "Inherit: propagate the user's profile and roles to all child organizations" -msgstr "Heredar: propaga el perfil y roles del usuario a todas las sub-organizaciones hijas" +msgid "No roles assigned" +msgstr "" msgid "Roles: manage the user's roles within the organization" msgstr "Roles: gestiona los roles del usuario dentro de la organización" @@ -59,1190 +28,731 @@ msgstr "Roles: gestiona los roles del usuario dentro de la organización" msgid "List Roles: show user's roles" msgstr "Listar Roles: muestra los roles del usuario" -msgid "Remove: remove user from organization, can also disable platform access" -msgstr "Eliminar: remueve al usuario de la organización, también puede deshabilitar el acceso a la plataforma" - -# Provider Laboratory -msgid "Add Provider Laboratory" -msgstr "Agregar Proveedor de Laboratorio" - -msgid "View Provider Laboratory" -msgstr "Ver Proveedor de Laboratorio" - -msgid "Change Provider Laboratory" -msgstr "Actualizar Proveedor de Laboratorio" - -msgid "Delete Provider Laboratory" -msgstr "Eliminar Proveedor de Laboratorio " +msgid "" +"Incognito mode: browse the system as this user without modifying your session" +msgstr "" +"Modo incógnito: navega el sistema como este usuario sin modificar tu sesión" -msgid "Provider Laboratory" -msgstr "Proveedor de Laboratorio" +msgid "" +"Inherit: propagate the user's profile and roles to all child organizations" +msgstr "" +"Heredar: propaga el perfil y roles del usuario a todas las sub-" +"organizaciones hijas" -# Provider SGA -msgid "Add Provider SGA" -msgstr "Agregar Proveedor de SGA" +msgid "Remove: remove user from organization, can also disable platform access" +msgstr "" +"Eliminar: remueve al usuario de la organización, también puede deshabilitar " +"el acceso a la plataforma" -msgid "View Provider SGA" -msgstr "Ver Proveedor de SGA" +msgid "User not found, Sorry try to use add user button on organization list" +msgstr "" -msgid "Change Provider SGA" -msgstr "Actualizar Proveedor de SGA" +#, fuzzy +#| msgid "Add External User in Organization" +msgid "User exist on organization" +msgstr "Agregar usuario externo a la organización" -msgid "Delete Provider SGA" -msgstr "Eliminar Proveedor de SGA " +msgid "User not match with email" +msgstr "" -msgid "Provider SGA" -msgstr "Proveedor de SGA" +msgid "Organization can't change this laboratory" +msgstr "" -# Laboratory -msgid "Add Laboratory" -msgstr "Agregar Laboratorio" +msgid "User doesn't have permissions" +msgstr "" -msgid "View Laboratory" -msgstr "Ver Laboratorio" +msgid "Shelfobject does not belong to this laboratory." +msgstr "" -msgid "Change Laboratory" -msgstr "Actualizar Laboratorio" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "Shelfobject does not belong to this organization." +msgstr "Haga clic para gestionar esta organización" -msgid "Delete Laboratory" -msgstr "Eliminar Laboratorio" +msgid "Shelfobject does not belong to this shelf." +msgstr "" -msgid "Laboratory" -msgstr "Laboratorio" +msgid "Shelf does not belong to this laboratory." +msgstr "" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "Shelf does not belong to this organization." +msgstr "Haga clic para gestionar esta organización" -# Permissions / Roles -msgid "Manage Permissions" -msgstr "Gestionar permisos" +# Organization +#, fuzzy +#| msgid "Organization Structure" +msgid "Organization cannot be inactive" +msgstr "Estructura organizacional" -msgid "Role" +#, fuzzy +#| msgid "Role" +msgid "Roles" msgstr "Rol" -msgid "Add Role" -msgstr "Agregar Rol" - -msgid "Delete Role" -msgstr "Eliminar Rol" - -msgid "View Role" -msgstr "Ver Rol" - -msgid "Change Role" -msgstr "Actualizar Rol" - -# Profile Permission -msgid "Add Profile Permission" -msgstr "Agregar permiso de perfil" - -msgid "Change Profile Permission" -msgstr "Actualizar permiso de perfil" - -msgid "Delete Profile Permission" -msgstr "Eliminar permiso de perfil" - -msgid "View Profile Permission" -msgstr "Ver permiso de perfil" - -msgid "Profile Permission" -msgstr "Permiso de perfil" - -# Profile -msgid "Add Profile" -msgstr "Agregar perfil" - -msgid "View Profile" -msgstr "Ver perfil" - -msgid "Change Profile" -msgstr "Actualizar perfil" - -msgid "Delete Profile" -msgstr "Eliminar perfil" - -msgid "Profile" -msgstr "Perfil" - - -# User -msgid "Add User" -msgstr "Agregar usuario" - -msgid "Change User" -msgstr "Actualizar usuario" - -msgid "View User" -msgstr "Ver usuario" - -msgid "Delete User" -msgstr "Eliminar usuario" - -msgid "User" +#, fuzzy +#| msgid "User" +msgid "Users" msgstr "Usuario" -msgid "Add External User in Organization" -msgstr "Agregar usuario externo a la organización" - -# Organization Structure -msgid "Add Organization Structure" -msgstr "Agregar estructura organizacional" - -msgid "Change Organization Structure" -msgstr "Actualizar estructura organizacional" - -msgid "View Organization Structure" -msgstr "Ver estructura organizacional" - -msgid "Delete Organization Structure" -msgstr "Eliminar estructura organizacional" - -# --- Organization Structure Relations --- -msgid "Add Organization Structure Relations" -msgstr "Agregar relaciones de estructura organizacional" +msgid "Laboratory modification not authorized" +msgstr "" -msgid "View Organization Structure Relations" -msgstr "Ver relaciones de estructura organizacional" +#, fuzzy, python-format +#| msgid "Add External User in Organization" +msgid "Added the user %(user)r in the organization %(org)r" +msgstr "Agregar usuario externo a la organización" -msgid "Organization Structure Relations" -msgstr "Relaciones de estructura organizacional" +#, python-format +msgid "Removed the groups %(groups)r from the profile %(profile)r" +msgstr "" -# Organization User Management -msgid "Add Organization Permissions Management" -msgstr "Agregar administración de permisos de organización" +#, python-format +msgid "Added the groups %(groups)r to the profile %(profile)r" +msgstr "" -msgid "Add Organization User Management" -msgstr "Agregar gestión de usuarios de la organización" +msgid "Profile was updated successfully." +msgstr "" -msgid "Change Organization User Management" -msgstr "Actualizar gestión de usuarios de la organización" +msgid "You don't have permissions to access this section" +msgstr "" -msgid "Delete Organization User Management" -msgstr "Eliminar gestión de usuarios de la organización" +# Organization +#, fuzzy +#| msgid "Organization Structure" +msgid "Organization parameter is required" +msgstr "Estructura organizacional" -msgid "View Organization User Management" -msgstr "Ver gestión de usuarios de la organización" +msgid "Rols" +msgstr "" -msgid "Organization User Management" +#, fuzzy +#| msgid "Organization User Management" +msgid "Organization name" msgstr "Gestión de usuarios de la organización" -# Disposal -msgid "Manage Disposal" -msgstr "Gestionar desecho" - -msgid "View Disposal" -msgstr "Ver desecho" - -msgid "Add Disposal" -msgstr "Agregar desecho" - -msgid "Delete Disposal" -msgstr "Eliminar desecho" - -msgid "Change Disposal" -msgstr "Actualizar desecho" - -# Shelf -msgid "Shelf" -msgstr "Estantería" +msgid "Digital signature" +msgstr "" -msgid "Add Shelf" -msgstr "Agregar Estantería" +msgid "Validation method" +msgstr "" -msgid "Delete Shelf" -msgstr "Eliminar Estantería" +msgid "new-password" +msgstr "" -msgid "Change Shelf" -msgstr "Actualizar Estantería" +msgid "Email" +msgstr "" -msgid "View Shelf" -msgstr "Ver Estantería" +msgid "First name" +msgstr "" -# Custom template -msgid "Custom template" -msgstr "Plantilla personalizada" +msgid "Last name" +msgstr "" -msgid "Add Custom template" -msgstr "Agregar Plantilla personalizada" +msgid "Email address" +msgstr "" -msgid "View Custom template" -msgstr "Ver Plantilla personalizada" +msgid "Phone" +msgstr "" -msgid "Delete Custom template" -msgstr "Eliminar Plantilla personalizada" +msgid "Identification" +msgstr "" -msgid "Change Custom template" -msgstr "Actualizar Plantilla personalizada" +msgid "Job Position" +msgstr "" -# Risk Management -msgid "View Risk Management" -msgstr "Ver gestión de riesgos" +msgid "Language" +msgstr "" -msgid "Risk Management" -msgstr "Gestión de riesgos" +msgid "It will used to login when you want to login with digital signature" +msgstr "" -# Recipient Size -msgid "Add Recipient Size" -msgstr "Agregar Tamaño de Recipiente" +#, fuzzy +#| msgid "Laboratory" +msgid "Laboratories" +msgstr "Laboratorio" -msgid "Change Recipient Size" -msgstr "Actualizar Tamaño de Recipiente" +msgid "User" +msgstr "Usuario" -msgid "Delete Recipient Size" -msgstr "Eliminar Tamaño de Recipiente" +msgid "Laboratory" +msgstr "Laboratorio" -msgid "View Recipient Size" -msgstr "Ver Tamaño de Recipiente" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "Inactive organization" +msgstr "Haga clic para gestionar esta organización" -msgid "Recipient Size" -msgstr "Tamaño de Recipiente" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "Clone organization" +msgstr "Haga clic para gestionar esta organización" +#, fuzzy +#| msgid "Change Organization User Management" +msgid "Change organization name" +msgstr "Actualizar gestión de usuarios de la organización" -# Laboratory -msgid "Laboratory Room" -msgstr "Sala de laboratorio" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "Active organization" +msgstr "Haga clic para gestionar esta organización" -msgid "Add Laboratory Room" -msgstr "Agregar Sala de laboratorio" +msgid "Actions" +msgstr "" -msgid "Delete Laboratory Room" -msgstr "Eliminar Sala de laboratorio" +msgid "Name" +msgstr "" -msgid "View Laboratory Room" -msgstr "Ver Sala de laboratorio" +msgid "Profile" +msgstr "Perfil" -msgid "Change Laboratory Room" -msgstr "Actualizar Sala de laboratorio" +msgid "Groups" +msgstr "" # Organization -msgid "Organization Structure" +#, fuzzy +#| msgid "Organization Structure" +msgid "Organization" msgstr "Estructura organizacional" -# Reservations -msgid "Reservations" -msgstr "Reservas" - -msgid "Reservation" -msgstr "Reserva" - -# Reserved Products -msgid "Reserved Products" -msgstr "Productos reservados" - -msgid "Add Reserved Products" -msgstr "Agregar Productos reservados" - -msgid "Change Reserved Products" -msgstr "Cambiar Productos reservados" - -msgid "Delete Reserved Products" -msgstr "Eliminar Productos reservados" - -msgid "View Reserved Products" -msgstr "Ver Productos reservados" - - -# Furniture -msgid "Furniture" -msgstr "Mueble" +msgid "Search by object" +msgstr "" -msgid "Add Furniture" -msgstr "Agregar Mueble" +msgid "Filter by roles" +msgstr "" -msgid "Delete Furniture" -msgstr "Eliminar Mueble" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "Filter by organization" +msgstr "Haga clic para gestionar esta organización" -msgid "View Furniture" -msgstr "Ver Mueble" +# Organization +#, fuzzy +#| msgid "Organization Structure" +msgid "Organization not found" +msgstr "Estructura organizacional" -msgid "Change Furniture" -msgstr "Actualizar Mueble" +msgid "Address" +msgstr "" -# Shelf -msgid "Shelf Object" -msgstr "Objeto de estantería" +msgid "Show tutorials" +msgstr "" -msgid "Add Shelf Object" -msgstr "Agregar Objeto de estantería" +msgid "Enable automatic tutorial display on page visits" +msgstr "" -msgid "Delete Shelf Object" -msgstr "Eliminar Objeto de estantería" +msgid "Workplace" +msgstr "" -msgid "Change Shelf Object" -msgstr "Actualizar Objeto de estantería" +#, fuzzy +#| msgid "Add External User in Organization" +msgid "Can add external user to organization" +msgstr "Agregar usuario externo a la organización" -msgid "View Shelf Object" -msgstr "Ver Objeto de estantería" +msgid "Institution can access" +msgstr "" -msgid "Add Shelf Object Status" -msgstr "Agregar Estado del objeto de estantería" +msgid "Can change own user/profile data" +msgstr "" -msgid "Shelf Object Status" -msgstr "Estado del objeto de estantería" +# Permissions / Roles +#, fuzzy +#| msgid "Manage Permissions" +msgid "permissions" +msgstr "Gestionar permisos" -# Shelf Object Observations -msgid "Shelf Object Observations" -msgstr "Observaciones del objeto de estantería" +#, fuzzy +#| msgid "Reservation" +msgid "Description" +msgstr "Reserva" -msgid "Add Shelf Object Observations" -msgstr "Agregar Observaciones del objeto de estantería" +#, fuzzy +#| msgid "Role" +msgid "Rol" +msgstr "Rol" -msgid "Delete Shelf Object Observations" -msgstr "Eliminar Observaciones del objeto de estantería" +#, fuzzy +#| msgid "Profile" +msgid "Profile Rol" +msgstr "Perfil" -msgid "Change Shelf Object Observations" -msgstr "Actualizar Observaciones del objeto de estantería" +#, fuzzy +#| msgid "Profile" +msgid "Profile Rols" +msgstr "Perfil" -msgid "View Shelf Object Observations" -msgstr "Ver Observaciones del objeto de estantería" +msgid "Impostor" +msgstr "" -# Shelf Object Calibration -msgid "Shelf Object Calibration" -msgstr "Calibración del objeto de estantería" +msgid "Logged in as" +msgstr "" -msgid "Add Shelf Object Calibration" -msgstr "Agregar Calibración del objeto de estantería" +msgid "Impostor's IP address" +msgstr "" -msgid "Delete Shelf Object Calibration" -msgstr "Eliminar Calibración del objeto de estantería" +msgid "Logged on" +msgstr "" -msgid "Change Shelf Object Calibration" -msgstr "Actualizar Calibración del objeto de estantería" +msgid "Logged out" +msgstr "" -msgid "View Shelf Object Calibration" -msgstr "Ver Calibración del objeto de estantería" +msgid "Token" +msgstr "" -# Shelf Object Guarantee -msgid "Shelf Object Guarantee" -msgstr "Garantía del objeto de estantería" +msgid "Impostor log" +msgstr "" -msgid "Add Shelf Object Guarantee" -msgstr "Agregar Garantía del objeto de estantería" +msgid "Impostor logs" +msgstr "" -msgid "Delete Shelf Object Guarantee" -msgstr "Eliminar Garantía del objeto de estantería" +#, python-format +msgid "User %(user)s not allowed on organization %(organization)r " +msgstr "" -msgid "View Shelf Object Guarantee" -msgstr "Ver Garantía del objeto de estantería" +msgid "You can modify this laboratory" +msgstr "" -msgid "Change Shelf Object Guarantee" -msgstr "Actualizar Garantía del objeto de estantería" +#, fuzzy +#| msgid "Roles: manage the user's roles within the organization" +msgid "Register with new organization" +msgstr "Roles: gestiona los roles del usuario dentro de la organización" -# Reports -msgid "Report" -msgstr "Reporte" +msgid "Register you as organization administrator" +msgstr "" -msgid "Add Report" -msgstr "Agregar Reporte" +#, fuzzy +#| msgid "Create" +msgid "Create user" +msgstr "Crear" -msgid "View Report" -msgstr "Ver Reporte" +msgid "" +"We need to validate that you are not a robot or spamer, so you need to " +"configure a Two-Factor Authentication" +msgstr "" -msgid "Delete Report" -msgstr "Eliminar Reporte" +msgid "" +"You need an OTP generator to login on the platform, you can use a free app " +"called FreeOTP:" +msgstr "" -msgid "Change Report" -msgstr "Actualizar Reporte" +msgid "" +"For Costa Rica citicen, use your digital signature to login on organilab" +msgstr "" -# Report Template -msgid "Report Template" -msgstr "Plantilla de reporte" +#, fuzzy +#| msgid "Change Profile" +msgid "Configure your profile" +msgstr "Actualizar perfil" -msgid "Add Report Template" -msgstr "Agregar Plantilla de reporte" +msgid "Validate user" +msgstr "" -msgid "Add Period Inform Scheduler" -msgstr "Agregar Agendador Periodo de informe" +msgid "How use Digital Signature" +msgstr "" -msgid "Change Report Template" -msgstr "Actualizar Plantilla de reporte" +msgid "" +"If you don't have Digital signature, please request on your favorite bank" +msgstr "" -msgid "Delete Report Template" -msgstr "Eliminar Plantilla de reporte" +msgid "Configure your machine using this link" +msgstr "" -msgid "View Report Template" -msgstr "Ver Plantilla de reporte" +msgid "Download and install Gaudi" +msgstr "" -msgid "List my reservations" -msgstr "Listar mis reservas" +msgid "Connect you card on computer and wait for connect success popup " +msgstr "" +msgid "Your OTP Seed" +msgstr "" -# Inform Observation -msgid "Inform Observation" -msgstr "Observación del informe" +msgid "This image will not available in the future, please save it" +msgstr "" -msgid "Add Inform Observation" -msgstr "Agregar Observación del informe" +msgid "Profile create successfully" +msgstr "" -msgid "Change Inform Observation" -msgstr "Actualizar Observación del informe" +msgid "" +"Your organization was created successfully, please login with your " +"credentials" +msgstr "" -msgid "Delete Inform Observation" -msgstr "Eliminar Observación del informe" +msgid "Login" +msgstr "" -msgid "View Inform Observation" -msgstr "Ver Observación del informe" +msgid "Laboratory and Organization List" +msgstr "" -# Inform -msgid "Inform" -msgstr "Informe" +#, fuzzy +#| msgid "Laboratory" +msgid "By laboratory" +msgstr "Laboratorio" -msgid "Add Inform" -msgstr "Agregar Informe" +msgid "By organization" +msgstr "" -msgid "Delete Inform" -msgstr "Eliminar Informe" +# Organization +#, fuzzy +#| msgid "Organization Structure" +msgid "Organizations List" +msgstr "Estructura organizacional" -msgid "Change Inform" -msgstr "Actualizar Informe" +# Laboratory Process +#, fuzzy +#| msgid "Laboratory Process" +msgid "Laboratories List" +msgstr "Proceso de Laboratorio" -msgid "View Inform" -msgstr "Ver Informe" +msgid "User roles list" +msgstr "" -msgid "Manage status Inform" -msgstr "Gestionar estado Informe" +msgid "Close" +msgstr "" -# Precursor Report -msgid "Precursor Report" -msgstr "Informe de precursores" +# Organization Structure +#, fuzzy +#| msgid "Add Organization Structure" +msgid "Add organization" +msgstr "Agregar estructura organizacional" -msgid "Add Precursor Report" -msgstr "Agregar Informe de precursores" +msgid "Click to manage this organization" +msgstr "Haga clic para gestionar esta organización" -msgid "Delete Precursor Report" -msgstr "Eliminar Informe de precursores" +msgid "Choose" +msgstr "Elegir" -msgid "Change Precursor Report" -msgstr "Actualizar Informe de precursores" +# Profile +#, fuzzy +#| msgid "Add Profile" +msgid "Add new rol" +msgstr "Agregar perfil" -msgid "View Precursor Report" -msgstr "Ver Informe de precursores" +# Common actions +msgid "Add" +msgstr "Agregar" -# Procedures -msgid "Procedure" -msgstr "Procedimiento" +# User +msgid "Add User" +msgstr "Agregar usuario" -msgid "Add Procedure" -msgstr "Agregar Procedimiento" +#, fuzzy +#| msgid "Delete User" +msgid "Related Users" +msgstr "Eliminar usuario" -msgid "Delete Procedure" -msgstr "Eliminar Procedimiento" +# Laboratory +msgid "Add Laboratory" +msgstr "Agregar Laboratorio" -msgid "View Procedure" -msgstr "Ver Procedimiento" +#, fuzzy +#| msgid "Delete Laboratory" +msgid "Related Laboratory" +msgstr "Eliminar Laboratorio" -msgid "Change Procedure" -msgstr "Actualizar Procedimiento" +msgid "Disable child organizations filter" +msgstr "" -# Procedures Step -msgid "Procedure Step" -msgstr "Paso del procedimiento" +msgid "Enable child organizations filter" +msgstr "" -msgid "Add Procedure Step" -msgstr "Agregar Paso del procedimiento" +#, fuzzy +#| msgid "List" +msgid "Log List" +msgstr "Listar" -msgid "Delete Procedure Step" -msgstr "Eliminar Paso del procedimiento" +msgid "Show Roles information" +msgstr "" -msgid "Change Procedure Step" -msgstr "Actualizar Paso del procedimiento" +#, fuzzy +#| msgid "View Contracts" +msgid "View administrators" +msgstr "Ver Contratos" -msgid "View Procedure Step" -msgstr "Ver Paso del procedimiento" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "Change parent of organization" +msgstr "Haga clic para gestionar esta organización" -msgid "Procedure Template" -msgstr "Plantilla de procedimiento" +#, fuzzy +#| msgid "List" +msgid "Rol List" +msgstr "Listar" -# My Procedure -msgid "My Procedure" -msgstr "Mi procedimiento" +#, fuzzy +#| msgid "Delete Organization Structure" +msgid "Delete organization" +msgstr "Eliminar estructura organizacional" -msgid "Add My Procedure" -msgstr "Agregar Mi procedimiento" +#, fuzzy +#| msgid "Profile" +msgid "By profile" +msgstr "Perfil" -msgid "Delete My Procedure" -msgstr "Eliminar Mi procedimiento" +#, fuzzy +#| msgid "Laboratory" +msgid "Rol by laboratory" +msgstr "Laboratorio" -msgid "Change My Procedure" -msgstr "Actualizar Mi procedimiento" +msgid "Add or restrict permission on this laboratory" +msgstr "" -msgid "View My Procedure" -msgstr "Ver Mi procedimiento" +msgid "Relate user with this laboratory" +msgstr "" -# Procedure Required Object -msgid "Procedure Required Object" -msgstr "Objeto Requerido por el Procedimiento" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "Rol in all organization" +msgstr "Haga clic para gestionar esta organización" -msgid "Add Procedure Required Object" -msgstr "Agregar Objeto Requerido por el Procedimiento" +msgid "Rol used when you don't have rol set on laboratory" +msgstr "" -msgid "Delete Procedure Required Object" -msgstr "Eliminar Objeto Requerido por el Procedimiento" +#, fuzzy +#| msgid "Roles: manage the user's roles within the organization" +msgid "Relate user with this organization" +msgstr "Roles: gestiona los roles del usuario dentro de la organización" -msgid "Update Procedure Required Object" -msgstr "Actualizar Objeto Requerido por el Procedimiento" +msgid "Help Text" +msgstr "" -msgid "View Procedure Required Object" -msgstr "Ver Objeto Requerido por el Procedimiento" +msgid "" +"This section will assign permissions groups to some profile who belongs to " +"selected organization. If you do not find the user in profile selector, " +"could you add it from By organization tab." +msgstr "" -# Procedure Observations -msgid "Procedure Observations" -msgstr "Observaciones del procedimiento" +msgid "Show descriptions of groups" +msgstr "" -msgid "Add Procedure Observations" -msgstr "Agregar Observaciones del procedimiento" +msgid "Save changes" +msgstr "" -msgid "Delete Procedure Observations" -msgstr "Eliminar Observaciones del procedimiento" +#, fuzzy +#| msgid "Add Role" +msgid "Add New Rol" +msgstr "Agregar Rol" -msgid "Update Procedure Observations" -msgstr "Actualizar Observaciones del procedimiento" +msgid "Copy Rols" +msgstr "" -msgid "View Procedure Observations" -msgstr "Ver Observaciones del procedimiento" +msgid "Rol name" +msgstr "" -# Risk Management -msgid "Risk Zone" -msgstr "Zona de Riesgo" +#, fuzzy +#| msgid "Add Reservation" +msgid "Add a description" +msgstr "Agregar Reservaciones" -msgid "Add Risk Zone" -msgstr "Agregar Zona de Riesgo" +msgid "Copy permissions from other roles?" +msgstr "" -msgid "Delete Risk Zone" -msgstr "Eliminar Zona de Riesgo" +msgid "Select roles to copy" +msgstr "" -msgid "Change Risk Zone" -msgstr "Actualizar Zona de Riesgo" +#, fuzzy +#| msgid "Delete Organization Structure" +msgid "Create Organization" +msgstr "Eliminar estructura organizacional" -msgid "View Risk Zone" -msgstr "Ver Zona de Riesgo" +msgid "Linking laboratories of the parent organization" +msgstr "" +msgid "How to deal with permission merge" +msgstr "" -# Incident Report -msgid "Incident Report" -msgstr "Reporte de incidente" +msgid "Append" +msgstr "" -msgid "Add Incident Report" -msgstr "Agregar Reporte de incidente" +msgid "Sustract" +msgstr "" -msgid "Delete Incident Report" -msgstr "Eliminar Reporte de incidente" +msgid "Only labs selected" +msgstr "" -msgid "Change Incident Report" -msgstr "Actualizar Reporte de incidente" +# Organization +#, fuzzy +#| msgid "Organization Structure" +msgid "Organization parent:" +msgstr "Estructura organizacional" -msgid "View Incident Report" -msgstr "Ver Reporte de incidente" +#, fuzzy +#| msgid "Delete Organization User Management" +msgid "Select an organization to be parent of" +msgstr "Eliminar gestión de usuarios de la organización" -# SGA / Safety -msgid "SGA" -msgstr "SGA" +#, fuzzy +#| msgid "Delete Provider Laboratory" +msgid "Relate user to laboratory" +msgstr "Eliminar Proveedor de Laboratorio " -msgid "View SGA" -msgstr "Ver SGA" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "From my organization" +msgstr "Haga clic para gestionar esta organización" -msgid "View SGA Label" -msgstr "Ver etiqueta SGA" +msgid "External user" +msgstr "" -# Label SGA -msgid "Label SGA" -msgstr "Etiqueta SGA" +msgid "Important" +msgstr "" -msgid "Add Label SGA" -msgstr "Agregar Etiqueta SGA" +msgid "" +"In this tab, you add the existing users in Organilab who are not associated " +"with the organization, so you need to know the user's email address to add " +"them." +msgstr "" -msgid "Delete Label SGA" -msgstr "Eliminar Etiqueta SGA" +msgid "" +"Once added, you can search for the profile in the organization tab to relate " +"user to organization" +msgstr "" -msgid "Change Label SGA" -msgstr "Actualizar Etiqueta SGA" +msgid "Find user" +msgstr "" -msgid "View Label SGA" -msgstr "Ver Etiqueta SGA" +#, fuzzy +#| msgid "Add External User in Organization" +msgid "Actions for Organization" +msgstr "Agregar usuario externo a la organización" -# Security Leaf -msgid "Security Leaf" -msgstr "Hoja de seguridad" +msgid "Do you want to enable the child organizations filter?" +msgstr "" -msgid "Add Security Leaf" -msgstr "Agregar Hoja de seguridad" +msgid "Administrators" +msgstr "" -msgid "Delete Security Leaf" -msgstr "Eliminar Hoja de seguridad" +#, fuzzy +#| msgid "Laboratory" +msgid "My laboratories" +msgstr "Laboratorio" -msgid "Change Security Leaf" -msgstr "Hoja de seguridad" +# Reports +#, fuzzy +#| msgid "Report" +msgid "Reports" +msgstr "Reporte" -msgid "View Security Leaf" -msgstr "Ver Hoja de seguridad" +# Risk Management +#, fuzzy +#| msgid "Risk Zone" +msgid "Risk Zones" +msgstr "Zona de Riesgo" +#, fuzzy +#| msgid "Add Inform" +msgid "Admin Informs" +msgstr "Agregar Informe" # MSDS msgid "MSDS" msgstr "MSDS" -# MSDS Object -msgid "MSDS Object" -msgstr "Objeto MSDS" - -msgid "Add MSDS Object" -msgstr "Agregar Objeto MSDS" - -msgid "Delete MSDS Object" -msgstr "Eliminar Objeto MSDS" - -msgid "View MSDS Object" -msgstr "Ver Objeto MSDS" - -msgid "Change MSDS Object" -msgstr "Actualizar Objeto MSDS" - - -msgid "View MSDS" -msgstr "Ver MSDS" - -# Equipment -msgid "Equipment Type" -msgstr "Tipo de equipo" - -msgid "Add Equipment Type" -msgstr "Agregar Tipo de equipo" - -msgid "Delete Equipment Type" -msgstr "Eliminar Tipo de equipo" - -msgid "Change Equipment Type" -msgstr "Actualizar Tipo de equipo" - -msgid "View Equipment Type" -msgstr "Ver Tipo de equipo" - -# Instrumental Family -msgid "Instrumental Family" -msgstr "Familia instrumental" - -msgid "Add Instrumental Family" -msgstr "Agregar Familia instrumental" - -msgid "View Instrumental Family" -msgstr "Ver Familia instrumental" - -msgid "Delete Instrumental Family" -msgstr "Eliminar Familia instrumental" - -msgid "Change Instrumental Family" -msgstr "Actualizar Familia instrumental" - -# Contracts -msgid "Contracts" -msgstr "Contratos" - -msgid "Add Contracts" -msgstr "Agregar Contratos" - -msgid "Delete Contracts" -msgstr "Eliminar Contratos" - -msgid "Change Contracts" -msgstr "Actualizar Contratos" - -msgid "View Contracts" -msgstr "Ver Contratos" - -# Objeto -msgid "Object" -msgstr "Objeto" - -msgid "Add Object" -msgstr "Agregar Objeto" - -msgid "Delete Object" -msgstr "Eliminar Objeto" - -msgid "View Object" -msgstr "Ver Objeto" - -msgid "Change Object" -msgstr "Actualizar Objeto" - -# Object Features -msgid "Object Features" -msgstr "Características del objeto" - -msgid "Add Object Features" -msgstr "Agregar Características del objeto" - -msgid "Delete Object Features" -msgstr "Eliminar Características del objeto" - -msgid "View Object Features" -msgstr "Ver Características del objeto" - -msgid "Change Object Features" -msgstr "Actualizar Características del objeto" - -# Protocol -msgid "Protocol" -msgstr "Protocolo" - -msgid "Add Protocol" -msgstr "Agregar Protocolo" - -msgid "Delete Protocol" -msgstr "Eliminar Protocolo" - -msgid "View Protocol" -msgstr "Ver Protocolo" - -msgid "Change Protocol" -msgstr "Actualizar Protocolo" - -# Register User QR Code -msgid "Register User QR Code" -msgstr "Código QR de registro de usuario" - -msgid "Add Register User QR Code" -msgstr "Agregar Código QR de registro de usuario" - -msgid "Change Register User QR Code" -msgstr "Actualizar Código QR de registro de usuario" - -msgid "Delete Register User QR Code" -msgstr "Eliminar Código QR de registro de usuario" - -msgid "View Register User QR Code" -msgstr "Ver Código QR de registro de usuario" - -# H Code Category -msgid "H Code Category" -msgstr "Categoría de código H" - -msgid "Add H Code Category" -msgstr "Agregar Categoría de código H" - -msgid "Delete H Code Category" -msgstr "Eliminar Categoría de código H" - -msgid "Change H Code Category" -msgstr "Actualizar Categoría de código H" - -msgid "View H Code Category" -msgstr "Ver Categoría de código H" - -# Structure -msgid "Structure" -msgstr "Estructura" - -msgid "Add Structure" -msgstr "Agregar estructura" - -msgid "Change Structure" -msgstr "Actualizar estructura" - -msgid "Delete Structure" -msgstr "Eliminar estructura" - -msgid "View Structure" -msgstr "Ver estructura" - -# Regent -msgid "Add Regent" -msgstr "Agregar regente" - -msgid "Change Regent" -msgstr "Actualizar regente" - -msgid "Delete Regent" -msgstr "Eliminar regente" - -msgid "View Regent" -msgstr "Ver regente" - -msgid "Regent" -msgstr "Regente" - -# Building -msgid "Add Building" -msgstr "Agregar edificio" - -msgid "Change Building" -msgstr "Actualizar edificio" - -msgid "Delete Building" -msgstr "Eliminar edificio" - -msgid "View Building" -msgstr "Ver edificio" - -msgid "Building" -msgstr "Edificio" - -# SGA Complement -msgid "Add SGA Complement" -msgstr "Agregar complemento SGA" - -msgid "Change SGA Complement" -msgstr "Actualizar complemento SGA" - -msgid "View SGA Complement" -msgstr "Ver complemento SGA" - -msgid "SGA Complement" -msgstr "Complemento SGA" - -# Display -msgid "Change Display Label" -msgstr "Actualizar etiqueta visible" - -msgid "Display Label" -msgstr "Etiqueta visible" - -# Label -msgid "Label" -msgstr "Etiqueta" - -msgid "Add Label" -msgstr "Agregar Etiqueta" - -msgid "Delete Label" -msgstr "Eliminar Etiqueta" - -msgid "Change Label" -msgstr "Actualizar Etiqueta" - -msgid "View Label" -msgstr "Ver Etiqueta" - -# Warning Word -msgid "Add Warning Word" -msgstr "Agregar palabra de advertencia" - -msgid "Change Warning Word" -msgstr "Actualizar palabra de advertencia" - -msgid "Delete Warning Word" -msgstr "Eliminar palabra de advertencia" - -msgid "View Warning Word" -msgstr "Ver palabra de advertencia" - -msgid "Warning Word" -msgstr "Palabra de advertencia" - -# Danger Indication -msgid "Add Danger Indication" -msgstr "Agregar indicación de peligro" - -msgid "Change Danger Indication" -msgstr "Actualizar indicación de peligro" - -msgid "Delete Danger Indication" -msgstr "Eliminar indicación de peligro" - -msgid "View Danger Indication" -msgstr "Ver indicación de peligro" - -msgid "Danger Indication" -msgstr "Indicación de peligro" - -# Prudence Advice -msgid "Add Prudence Advice" -msgstr "Agregar consejo de prudencia" - -msgid "Change Prudence Advice" -msgstr "Actualizar consejo de prudencia" - -msgid "Delete Prudence Advice" -msgstr "Eliminar consejo de prudencia" - -msgid "View Prudence Advice" -msgstr "Ver consejo de prudencia" - -msgid "Prudence Advice" -msgstr "Consejo de prudencia" - -# Substance Observation -msgid "Substance Observation" -msgstr "Observación de la sustancia" - -msgid "View Substance Observation" -msgstr "Ver Observación de la sustancia" - -msgid "Add Substance Observation" -msgstr "Agregar Observación de la sustancia" - -msgid "Delete Substance Observation" -msgstr "Eliminar Observación de la sustancia" - -msgid "Change Substance Observation" -msgstr "Actualizar Observación de la sustancia" - -# Substance Characteristics -msgid "Substance Characteristics" -msgstr "Características de la sustancia" - -msgid "View Substance Characteristics" -msgstr "Ver Características de la sustancia" - -msgid "Add Substance Characteristics" -msgstr "Agregar Características de la sustancia" - -msgid "Change Substance Characteristics" -msgstr "Actualizar Características de la sustancia" - -msgid "Delete Substance Characteristics" -msgstr "Eliminar Características de la sustancia" - -# Substance Characteristics -msgid "ZoneType" -msgstr "Tipo de Zona" - -msgid "Add ZoneType" -msgstr "Agregar Tipo de Zona" - -msgid "View ZoneType" -msgstr "Ver Tipo de Zona" - -msgid "Change ZoneType" -msgstr "Actualizar Tipo de Zona" - -msgid "Delete ZoneType" -msgstr "Eliminar Tipo de Zona" - -# Transfer -msgid "Transfer" -msgstr "Transferencia" - -msgid "Add Transfer" -msgstr "Agregar Transferencia" - -msgid "Change Transfer" -msgstr "Actualizar Transferencia" - -msgid "View Transfer" -msgstr "Ver Transferencia" - -msgid "Delete Transfer" -msgstr "Eliminar Transferencia" - -# Substance -msgid "Substance" -msgstr "Sustancia" - -msgid "Add Substance" -msgstr "Agregar Sustancia" - -msgid "Delete Substance" -msgstr "Eliminar Sustancia" - -msgid "Change Substance" -msgstr "Actualizar Sustancia" - -msgid "View Substance" -msgstr "Ver Sustancia" - -# Template SGA -msgid "Template SGA" -msgstr "Plantilla SGA" - -msgid "Add Template SGA" -msgstr "Agregar Plantilla SGA" - -msgid "Delete Template SGA" -msgstr "Eliminar Plantilla SGA" - -msgid "Change Template SGA" -msgstr "Actualizar Plantilla SGA" - -msgid "View Template SGA" -msgstr "Ver Plantilla SGA" - -# Step Comment -msgid "Step Comment" -msgstr "Comentario del paso" - -msgid "Add Step Comment" -msgstr "Agregar Comentario del paso" - -msgid "Delete Step Comment" -msgstr "Eliminar Comentario del paso" - -msgid "Change Step Comment" -msgstr "Actualizar Comentario del paso" - -msgid "View Step Comment" -msgstr "Ver Comentario del paso" - -# Shelf Object Training -msgid "Shelf Object Training" -msgstr "Capacitación sobre objeto de estantería" - -msgid "Add Shelf Object Training" -msgstr "Agregar Capacitación sobre objeto de estantería" - -msgid "Change Shelf Object Training" -msgstr "Actualizar Capacitación sobre objeto de estantería" - -msgid "Delete Shelf Object Training" -msgstr "Eliminar Capacitación sobre objeto de estantería" - -msgid "View Shelf Object Training" -msgstr "Ver Capacitación sobre objeto de estantería" - -# Shelf Object Log -msgid "Shelf Object Log" -msgstr "Registro del objeto de estantería" - -msgid "Add Shelf Object Log" -msgstr "Agregar Registro del objeto de estantería" - -msgid "Change Shelf Object Log" -msgstr "Actualizar Registro del objeto de estantería" - -msgid "Delete Shelf Object Log" -msgstr "Eliminar Registro del objeto de estantería" - -msgid "View Shelf Object Log" -msgstr "Ver Registro del objeto de estantería" - -# Regulation Document -msgid "Regulation Document" -msgstr "Documento normativo" - -msgid "View Regulation Document" -msgstr "Ver Documento normativo" - -msgid "Delete Regulation Document" -msgstr "Eliminar Documento normativo" - -msgid "Change Regulation Document" -msgstr "Actualizar Documento normativo" - -msgid "Add Regulation Document" -msgstr "Agregar Documento normativo" - -# Entry -msgid "Blog Entry" -msgstr "Entrada de Blog" - -msgid "Add Blog Entry" -msgstr "Agregar Entrada de Blog" - -msgid "Delete Blog Entry" -msgstr "Eliminar Entrada de Blog" - -msgid "Change Blog Entry" -msgstr "Actualizar Entrada de Blog" - -msgid "View Blog Entry" -msgstr "Ver Entrada de Blog" - -# Blog Category -msgid "Blog Category" -msgstr "Categoría del blog" - -msgid "Add Blog Category" -msgstr "Agregar Categoría del blog" - -msgid "Delete Blog Category" -msgstr "Eliminar Categoría del blog" - -msgid "Change Blog Category" -msgstr "Actualizar Categoría del blog" - -msgid "View Blog Category" -msgstr "Ver Categoría del blog" - -# Laboratory Process -msgid "Laboratory Process" -msgstr "Proceso de Laboratorio" - -msgid "Add Laboratory Process" -msgstr "Agregar Proceso de Laboratorio" - -msgid "Change Laboratory Process" -msgstr "Actualizar Proceso de Laboratorio" - -msgid "Delete Laboratory Process" -msgstr "Eliminar Proceso de Laboratorio" - -msgid "View Laboratory Process" -msgstr "Ver Proceso de Laboratorio" - -#Shelf Object Maintenance - -msgid "Update Shelf Object" -msgstr "Actualizar objeto de estantería" - -msgid "View Unit Measurement" -msgstr "Ver unidad de medida" +# SGA / Safety +msgid "SGA" +msgstr "SGA" -msgid "Unit Measurement" -msgstr "Unidad de medida" +#, fuzzy +#| msgid "Add Disposal" +msgid "Disposal" +msgstr "Agregar desecho" -msgid "Can manage organization permission structure" -msgstr "Puede gestionar la estructura de permisos de la organización" +#, fuzzy +#| msgid "Change Role" +msgid "Manage Rol" +msgstr "Actualizar Rol" -msgid "View Danger Substance" -msgstr "Ver Sustancia Peligrosa" +msgid "Only rols selected" +msgstr "" -msgid "Add Danger Substance" -msgstr "Agregar Sustancia Peligrosa" +#, fuzzy +#| msgid "Change User" +msgid "Manage Users" +msgstr "Actualizar usuario" -msgid "Change Danger Substance" -msgstr "Cambiar Sustancia Peligrosa" +#, fuzzy +#| msgid "Delete Organization Structure" +msgid "Delete Organization Rol" +msgstr "Eliminar estructura organizacional" -msgid "View Danger Substance Category" -msgstr "Ver categoría de sustancia peligrosa" +msgid "Are you sure you want to delete" +msgstr "" -msgid "Add Danger Substance Category" -msgstr "Agregar categoría de sustancia peligrosa" +# Permissions / Roles +#, fuzzy +#| msgid "Manage Permissions" +msgid "Current permissions" +msgstr "Gestionar permisos" -msgid "Change Danger Substance Category" -msgstr "Cambiar categoría de sustancia peligrosa" +msgid "Confirm" +msgstr "" -msgid "Delete Danger Substance Category" -msgstr "Eliminar categoría de sustancia peligrosa" +#, fuzzy +#| msgid "Click to manage this organization" +msgid "Roles registered on this organization" +msgstr "Haga clic para gestionar esta organización" -msgid "Add Precursor Report Value" -msgstr "Agregar valor del informe de precursores" +#, fuzzy +#| msgid "Add Role" +msgid "Edit Role" +msgstr "Agregar Rol" -msgid "Change Precursor Report Value" -msgstr "Modificar valor del informe de precursores" +#, fuzzy +#| msgid "List" +msgid "User List" +msgstr "Listar" -msgid "Delete Precursor Report Value" -msgstr "Eliminar valor del informe de precursores" +msgid "Clear filters" +msgstr "" -msgid "View Precursor Report Value" -msgstr "Ver valor del informe de precursores" +#, fuzzy +#| msgid "Add External User in Organization" +msgid "User roles in organization" +msgstr "Agregar usuario externo a la organización" -msgid "Precursor Report Value" -msgstr "Valor del informe de precursores" +#, fuzzy +#| msgid "Delete Laboratory" +msgid "User roles in laboratory" +msgstr "Eliminar Laboratorio" -msgid "View Catalog" -msgstr "Ver Catálogo" +#, fuzzy +#| msgid "Organization User Management" +msgid "Organization doesn't exists" +msgstr "Gestión de usuarios de la organización" -msgid "Catalog" -msgstr "Catálogo" +msgid "Element saved successfully" +msgstr "" -msgid "Add Reservation" -msgstr "Agregar Reservaciones" +msgid "Error, form is invalid" +msgstr "" -msgid "Can Request Reservations" -msgstr "Puede Solicitar Reservaciones" +#, python-format +msgid "" +"You have active impostor session Finish it" +msgstr "" -msgid "Danger Substance Category" -msgstr "Categoría de Sustancia Peligrosa" +msgid "Request User is the same as current user" +msgstr "" -msgid "View Register User" -msgstr "Ver Registro de Usuario" +#, fuzzy +#| msgid "Add External User in Organization" +msgid "User not in organization" +msgstr "Agregar usuario externo a la organización" -msgid "Additional resources" -msgstr "Recursos adicionales" +msgid "Form data is wrong, you need to pass a valid laboratory as contenttype" +msgstr "" -msgid "Dismissed" -msgstr "Omitido" +msgid "Role updated successfully" +msgstr "" -msgid "Choose" -msgstr "Elegir" +#, fuzzy +#| msgid "Organization User Management" +msgid "Organization Management" +msgstr "Gestión de usuarios de la organización" -msgid "Click to manage this organization" -msgstr "Haga clic para gestionar esta organización" +msgid "" +"You have no creation process, maybe it was expired, please try to register " +"again" +msgstr "" diff --git a/src/auth_and_perms/locale/es/LC_MESSAGES/djangojs.po b/src/auth_and_perms/locale/es/LC_MESSAGES/djangojs.po index fe010719a..cc89b5265 100644 --- a/src/auth_and_perms/locale/es/LC_MESSAGES/djangojs.po +++ b/src/auth_and_perms/locale/es/LC_MESSAGES/djangojs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-04-10 14:02-0600\n" +"POT-Creation-Date: 2026-06-27 01:43-0600\n" "PO-Revision-Date: 2025-04-10 14:07-0600\n" "Last-Translator: \n" "Language-Team: \n" @@ -19,3 +19,144 @@ msgstr "" "1 : 2;\n" "X-Generator: Poedit 3.2.2\n" +msgid "Name" +msgstr "" + +msgid "Associated laboratories" +msgstr "" + +msgid "Partner organizations" +msgstr "" + +msgid "User" +msgstr "" + +msgid "Roles" +msgstr "" + +msgid "No roles assigned" +msgstr "" + +msgid "Email" +msgstr "" + +msgid "Rols" +msgstr "" + +msgid "Actions" +msgstr "" + +msgid "Your rol was set successfully" +msgstr "" + +msgid "Now you can set this rol in laboratory user table" +msgstr "" + +msgid "User registered successfully" +msgstr "" + +msgid "" +"There was a problem performing your request. Please try again later or " +"contact the administrator." +msgstr "" + +msgid "" +"User successfully added, now you can search for it in the adjacent tab to " +"add it in the organization." +msgstr "" + +msgid "Success" +msgstr "" + +msgid "Failed to add user. Please try again" +msgstr "" + +msgid "Error" +msgstr "" + +msgid "Are you sure?" +msgstr "" + +msgid "will be part of your organization!" +msgstr "" + +msgid "Yes, add it!" +msgstr "" + +msgid "Relate user to organization" +msgstr "" + +msgid "Current laboratory" +msgstr "" + +msgid "Relate user to laboratory" +msgstr "" + +msgid "Laboratory needs to be selected" +msgstr "" + +msgid "Sorry you need to select laboratory before relate user to it" +msgstr "" + +msgid "Are you sure you want to delete?" +msgstr "" + +msgid " from " +msgstr "" + +msgid "Yes, delete it" +msgstr "" + +msgid "Also disable user login on platform" +msgstr "" + +msgid "No organization selected" +msgstr "" + +msgid "You need to select a organization before using this tab." +msgstr "" + +msgid "Are you sure you want to add this profile into others organizations?" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "This organization has no administrators assigned." +msgstr "" + +msgid "Do you want to disable the child organizations filter?" +msgstr "" + +msgid "Disable child organizations filter?" +msgstr "" + +msgid "Enable child organizations filter?" +msgstr "" + +msgid "Do you want to enable the child organizations filter?" +msgstr "" + +msgid "Shelf Object" +msgstr "" + +msgid "Shelf" +msgstr "" + +msgid "Quantity" +msgstr "" + +msgid "Laboratory" +msgstr "" + +msgid "Id" +msgstr "" + +msgid "User name" +msgstr "" + +msgid "Organization" +msgstr "" + +msgid "Laboratories" +msgstr "" diff --git a/src/laboratory/templates/laboratory/shelfobject_detail.html b/src/laboratory/templates/laboratory/shelfobject_detail.html index af6d6de28..592b34065 100644 --- a/src/laboratory/templates/laboratory/shelfobject_detail.html +++ b/src/laboratory/templates/laboratory/shelfobject_detail.html @@ -21,6 +21,19 @@