diff --git a/backend/Dockerfile b/backend/Dockerfile index cff9b109..89d5822d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,6 +9,31 @@ RUN apt-get update && apt-get install -y \ curl \ && rm -rf /var/lib/apt/lists/* +# Instalar dependencias para wkhtmltopdf y reportes PDF +RUN apt-get update \ + && apt-get install -y \ + xvfb \ + fontconfig \ + fonts-dejavu-core \ + libfontconfig1 \ + libxrender1 \ + libxtst6 \ + libxi6 \ + libxrandr2 \ + ca-certificates \ + libjpeg62-turbo \ + libpng16-16 \ + && rm -rf /var/lib/apt/lists/* + +# Instalar wkhtmltopdf binario oficial con soporte para footers/headers +RUN curl -k -L -o /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb \ + && apt-get update \ + && apt-get install -y /tmp/wkhtmltox.deb \ + && rm /tmp/wkhtmltox.deb \ + && rm -rf /var/lib/apt/lists/* \ + && wkhtmltopdf --version + + # Copiar requirements COPY requirements.txt . diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py new file mode 100644 index 00000000..a7e765e1 --- /dev/null +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py @@ -0,0 +1,78 @@ +from decimal import Decimal +from typing import List, Optional, Union +from pydantic import BaseModel + +# Nota: Usamos Union[float, str] en los números para poder enviar +# strings formateados con comas (ej: "1,200.50") si lo deseamos, +# ya que tu HTML no tiene filtros de formato. + +class ClienteSchema(BaseModel): + header: str + nombre: str + direccion: str + num_exterior: str = "" + num_interior: str = "" + colonia: str = "" + codigo_postal: str + ciudad: str + estado: str + pais: str + tax_id: str + programa: str = "" + autorizacion: str = "" + +class FacturaSchema(BaseModel): + numero: str + fecha: str + tipo_cambio: float + moneda: str + # Campos de aduanas (Opcionales por si A76 aún no los tiene) + pedimento: str = "" + clave_pedimento: str = "" + remesa: str = "" + acuse_electronico: str = "" + agente_aduanal: str = "" + patente: str = "" + precinto: str = "" + regimen: str = "" + transportista: str = "" + scac: str = "" + caat: str = "" + incoterm: str = "" + transporte: str = "" + num_transporte: str = "" + placas: str = "" + aduana: str = "" + destino: str = "" + observaciones: str = "" + +class PartidaSchema(BaseModel): + numero_parte: str + descripcion: str + fraccion: str + origen: str + cantidad_importacion: Union[float, str] + unidad_medida: str + cantidad_bultos: int + clave_bultos: str + peso_neto: Union[float, str] + peso_bruto: Union[float, str] + valor_costo_unitario: Union[float, str] + valor_total: Union[float, str] + +class TotalesSchema(BaseModel): + cantidad_total: Union[float, str] + bultos_total: int + clave_bultos: str = "" + peso_neto_total: Union[float, str] + peso_bruto_total: Union[float, str] + valor_total_total: Union[float, str] + valor_total_dolares: Union[float, str] + +class FacturaImportacionCompleta(BaseModel): + cliente_proveedor: ClienteSchema + cliente_vendido: ClienteSchema + cliente_enviado: ClienteSchema + factura: FacturaSchema + partidas: List[PartidaSchema] + totales: TotalesSchema \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py new file mode 100644 index 00000000..c188fe09 --- /dev/null +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py @@ -0,0 +1,296 @@ +import shutil +import pdfkit +from pathlib import Path +from decimal import Decimal +from typing import Tuple, List + +from jinja2 import Environment, FileSystemLoader, select_autoescape +from fastapi import HTTPException +from sqlalchemy.orm import Session + +# --- IMPORTACIONES DE TUS MODELOS (Asegúrate que las rutas sean correctas) --- +from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics +from api.v1.modules.a76.items.models import ItemLines, LineQuantities, LineFinancials +from api.v1.modules.a76.clients.models import ( + ClientsAndProviders, + ClientsAndProvidersAddress, + ClientsAndProvidersPrograms +) +from api.v1.modules.a76.parts.models import Parts +from api.v1.modules.a76.pedimentos.models import Pedimentos +from api.v1.modules.a76.companies.models import Company # Tu empresa propia (Tenant) + +from .schemas import ( + ClienteSchema, PartidaSchema, TotalesSchema, + FacturaSchema, FacturaImportacionCompleta +) + +class FacturaImportacionMexService: + def __init__(self): + # Directorio de templates + self.template_dir = Path(__file__).parent.parent / "templates" + + self.jinja_env = Environment( + loader=FileSystemLoader(self.template_dir), + autoescape=select_autoescape(['html', 'xml']) + ) + self.template = self.jinja_env.get_template('factura_mex_ver.html') + + def _get_wkhtmltopdf_config(self): + path = shutil.which("wkhtmltopdf") + if not path: + common_paths = ["/usr/bin/wkhtmltopdf", "/usr/local/bin/wkhtmltopdf"] + for p in common_paths: + if Path(p).exists(): + path = p + break + if not path: + raise RuntimeError("wkhtmltopdf no encontrado.") + return pdfkit.configuration(wkhtmltopdf=path) + + def formatear_numero(self, valor, decimales: int = 2): + if valor is None: return 0.0 + try: + return round(float(valor), decimales) + except: return 0.0 + + def _obtener_datos_cliente(self, db: Session, client_id: int, rol: str) -> ClienteSchema: + """ + Busca en las 3 tablas de clientes (Main, Address, Programs) para armar el esquema. + """ + # 1. Tabla Principal + main = db.query(ClientsAndProviders).filter(ClientsAndProviders.id == client_id).first() + if not main: + return ClienteSchema( + header=rol, nombre="Desconocido", direccion="", codigo_postal="", + ciudad="", estado="", pais="", tax_id="" + ) + + # 2. Dirección + addr = db.query(ClientsAndProvidersAddress).filter(ClientsAndProvidersAddress.client_id == client_id).first() + + # 3. Programa / Tax ID + prog = db.query(ClientsAndProvidersPrograms).filter(ClientsAndProvidersPrograms.client_id == client_id).first() + + direccion_str = addr.streets if addr else "" + + return ClienteSchema( + header=rol, + nombre=main.name or main.short_name, + direccion=direccion_str, + num_exterior=addr.exterior_number if addr else "", + num_interior=addr.interior_number if addr else "", + colonia=addr.neighborhood if addr else "", + codigo_postal=addr.postal_code if addr else "", + ciudad=addr.city if addr else "", + estado=addr.state if addr else "", + pais=addr.country if addr else "MEX", + tax_id=prog.tax_id if prog else (main.rfc or ""), + programa="IMMEX" if prog and prog.program else "", + autorizacion=prog.program_number if prog else "" + ) + + def obtener_datos(self, db: Session, invoice_id: int) -> FacturaImportacionCompleta: + try: + # 1. CABECERA (InvoiceHeader) + header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id).first() + if not header: + raise HTTPException(status_code=404, detail="Factura no encontrada") + + # 2. LOGÍSTICA (InvoiceLogistics) + logistics = db.query(InvoiceLogistics).filter(InvoiceLogistics.invoice_id == header.id).first() + + # 3. PEDIMENTO (Pedimentos) + # Intentamos buscar por relation_doc_id o asumimos una búsqueda por ID si existiera columna pedimento_id + # Si no hay link directo, buscamos el pedimento asociado al cliente/fecha (Lógica aproximada) + pedimento = None + if header.related_doc_id: + pedimento = db.query(Pedimentos).filter(Pedimentos.id == header.related_doc_id).first() + + # 4. ACTORES (Clientes/Proveedores) + # Importación: Proveedor = Externo (Client ID?), Importador = Nosotros (Company ID) + # Nota: Asumo que en invoice_header hay un campo 'client_id' para el proveedor externo. + # Si no existe en el modelo, revisa qué campo guarda el ID del proveedor. + proveedor_id = getattr(header, 'client_id', None) + + # --- PROVEEDOR (Extranjero) --- + if proveedor_id: + cliente_proveedor = self._obtener_datos_cliente(db, proveedor_id, "Proveedor / Supplier") + else: + # Fallback si no encontramos ID de proveedor + cliente_proveedor = ClienteSchema( + header="Proveedor", nombre="No Asignado", direccion="", codigo_postal="", ciudad="", estado="", pais="", tax_id="" + ) + + # --- IMPORTADOR (Vendido A - Tu Empresa) --- + # Usamos 'company_id' que viene en invoice_header + # Ojo: Si tu empresa también está en clients_and_providers, úsala. Si está en 'companies', mapea desde ahí. + # Aquí asumo que está en 'companies' como Tenant. + company = db.query(Company).filter(Company.id == header.company_id).first() + + cliente_vendido = ClienteSchema( + header="Importador / Consignatario", + nombre=company.name if company else "Mi Empresa", + direccion=company.address_street if company else "", # Ajustar nombres de col company + num_exterior=str(company.address_number) if company else "", + colonia=company.neighborhood if company else "", + codigo_postal=company.zip_code if company else "", + ciudad=company.city if company else "", + estado=company.state if company else "", + pais=company.country if company else "MEX", + tax_id=company.tax_id if company else "", + programa="IMMEX", + autorizacion=company.immex_program if company else "" + ) + + cliente_enviado = cliente_vendido + + # 5. MAPEO FACTURA + factura_schema = FacturaSchema( + numero=header.invoice_number, + fecha=str(header.invoice_date) if header.invoice_date else "", + tipo_cambio=float(pedimento.exchange_rate) if pedimento and pedimento.exchange_rate else 1.0, + moneda="USD", # O derivar de header.invoice_type o logistics + incoterm=logistics.incoterm if logistics else "", + observaciones=header.observation_es or header.observation_en or "", + + # Datos Pedimento + pedimento=pedimento.pedimento_number if pedimento else "", + clave_pedimento=pedimento.pedimento_code if pedimento else "", + regimen=pedimento.regime if pedimento else "", + + # Datos Transporte + transportista="", # logistics.carrier_id (habría que hacer join con tabla carriers) + transporte=logistics.transport_type if logistics else "", + num_transporte=logistics.trailer_num if logistics else "", + placas=logistics.license_plate if logistics else "", + aduana=pedimento.customs_office if pedimento else "", + + # Otros + remesa="", # No vi campo remesa en header + acuse_electronico="", + agente_aduanal="", # pedimento.license + patente=pedimento.license if pedimento else "", + precinto=logistics.seal_number if logistics else "", + destino=logistics.destination_goods if logistics else "" + ) + + # 6. PARTIDAS (JOIN item_lines + line_quantities + line_financials) + lines = db.query(ItemLines).filter(ItemLines.item_id == header.id).all() # Ojo: item_id suele ser invoice_id en header + + partidas_list = [] + + for line in lines: + # Datos Cuantitativos + qty = db.query(LineQuantities).filter(LineQuantities.item_line_id == line.id).first() + # Datos Financieros + fin = db.query(LineFinancials).filter(LineFinancials.item_line_id == line.id).first() + + # Parte Maestra (Si line.part_number es ID, buscamos. Si es string, usamos directo) + # Tu tabla dice 'part_number' es integer, asumo que es ID hacia tabla 'parts' + part_master = db.query(Parts).filter(Parts.id == line.part_number).first() + + # Construcción de valores + descripcion = part_master.description_spanish if part_master else "S/D" + fraccion = part_master.fraction if part_master else "" + + cantidad = float(qty.quantity) if qty else 0.0 + # Costo: Unitario Comercial USD + precio_unitario = float(fin.commercial_unit_cost) if fin else 0.0 + valor_total = float(fin.total_commercial_value) if fin else (cantidad * precio_unitario) + + # Pesos y Bultos (LineQuantities tiene todo esto, ¡genial!) + peso_n = float(qty.net_weight) if qty else 0.0 + peso_b = float(qty.gross_weight) if qty else 0.0 + bultos = int(qty.package_quantity) if qty and qty.package_quantity else 0 + clave_b = qty.package_key if qty else "" + + partidas_list.append(PartidaSchema( + numero_parte=part_master.part_number if part_master else str(line.part_number), + descripcion=descripcion, + fraccion=fraccion, + origen="MEX", # line.country_origin? o part_master + cantidad_importacion=self.formatear_numero(cantidad), + unidad_medida=qty.weight_unit if qty else "KG", # O qty.package_key + cantidad_bultos=bultos, + clave_bultos=clave_b, + peso_neto=self.formatear_numero(peso_n), + peso_bruto=self.formatear_numero(peso_b), + valor_costo_unitario=self.formatear_numero(precio_unitario), + valor_total=self.formatear_numero(valor_total) + )) + + # 7. TOTALES + totales = self.calcular_totales(partidas_list, Decimal(factura_schema.tipo_cambio)) + + return FacturaImportacionCompleta( + cliente_proveedor=cliente_proveedor, + cliente_vendido=cliente_vendido, + cliente_enviado=cliente_enviado, + factura=factura_schema, + partidas=partidas_list, + totales=totales + ) + + except Exception as e: + print(f"Error Service A76: {e}") + raise HTTPException(status_code=500, detail=f"Error procesando datos: {str(e)}") + + def calcular_totales(self, partidas: List[PartidaSchema], tipo_cambio: Decimal) -> TotalesSchema: + cant = sum(float(p.cantidad_importacion) for p in partidas) + valor = sum(float(p.valor_total) for p in partidas) + peso_n = sum(float(p.peso_neto) for p in partidas) + peso_b = sum(float(p.peso_bruto) for p in partidas) + bultos = sum(p.cantidad_bultos for p in partidas) + + # Obtener clave de bulto más común + claves = [p.clave_bultos for p in partidas if p.clave_bultos] + clave_comun = max(set(claves), key=claves.count) if claves else "" + + tc = float(tipo_cambio) if tipo_cambio else 1.0 + + return TotalesSchema( + cantidad_total=self.formatear_numero(cant), + bultos_total=bultos, + clave_bultos=clave_comun, + peso_neto_total=self.formatear_numero(peso_n), + peso_bruto_total=self.formatear_numero(peso_b), + valor_total_total=self.formatear_numero(valor), + valor_total_dolares=self.formatear_numero(valor / tc if tc > 0 else 0) + ) + + # --- GENERACIÓN --- + def generar_html(self, datos: FacturaImportacionCompleta) -> str: + context = { + 'cliente_proveedor': datos.cliente_proveedor.model_dump(), + 'cliente_vendido': datos.cliente_vendido.model_dump(), + 'cliente_enviado': datos.cliente_enviado.model_dump(), + 'factura': datos.factura.model_dump(), + 'partidas': [p.model_dump() for p in datos.partidas], + 'totales': datos.totales.model_dump() + } + return self.template.render(**context) + + def generar_pdf(self, html_content: str) -> bytes: + options = { + 'page-size': 'Letter', + 'margin-top': '0.75in', + 'margin-right': '0.75in', + 'margin-bottom': '1.00in', + 'margin-left': '0.75in', + 'encoding': "UTF-8", + 'enable-local-file-access': None + } + config = self._get_wkhtmltopdf_config() + return pdfkit.from_string(html_content, False, options=options, configuration=config) + + def generar_factura_completa(self, db: Session, invoice_id: int, formato: str = "pdf") -> Tuple[bytes, str, str]: + datos = self.obtener_datos(db, invoice_id) + html = self.generar_html(datos) + nombre = f"Factura_{datos.factura.numero}.{formato}" + + if formato == "html": + return html.encode('utf-8'), nombre, "text/html" + + pdf = self.generar_pdf(html) + return pdf, nombre, "application/pdf" \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py b/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py new file mode 100644 index 00000000..0638ea41 --- /dev/null +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py @@ -0,0 +1,50 @@ +from enum import Enum +from fastapi import APIRouter, Depends, Query, Response, HTTPException +from sqlalchemy.orm import Session +from core.database import get_db + +# Importamos el servicio mexicano +from .mex.service import FacturaImportacionMexService + +router = APIRouter() +servicio_mex = FacturaImportacionMexService() + +class TipoFactura(str, Enum): + mexicana = "mex" + # americana = "usa" + +class Formato(str, Enum): + html = "html" + pdf = "pdf" + +@router.get("/{invoice_id}/download") +async def descargar_factura( + invoice_id: int, + tipo: TipoFactura = Query(TipoFactura.mexicana), + formato: Formato = Query(Formato.pdf), + db: Session = Depends(get_db) +): + """ + Descargar Factura de Importación (A76) + """ + if tipo == TipoFactura.mexicana: + servicio = servicio_mex + else: + raise HTTPException(status_code=501, detail="Tipo de factura no implementado") + + try: + contenido, nombre, media_type = servicio.generar_factura_completa( + db=db, + invoice_id=invoice_id, + formato=formato.value + ) + + return Response( + content=contenido, + media_type=media_type, + headers={ + "Content-Disposition": f"attachment; filename={nombre}" + } + ) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error generando reporte: {str(e)}") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/tem/schemas.py b/backend/api/v1/modules/a76/reports/importacion/facturas/tem/schemas.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/tem/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/tem/service.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html b/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html new file mode 100644 index 00000000..eff919ee --- /dev/null +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html @@ -0,0 +1,367 @@ + + +
+ +Factura de Importacion
+ + ++
{{ cliente_proveedor.nombre }}
+{{ cliente_proveedor.direccion }} + {% if cliente_proveedor.num_exterior %} Ext: {{ cliente_proveedor.num_exterior }}{% endif %} + {% if cliente_proveedor.num_interior %} Int: {{ cliente_proveedor.num_interior }}{% endif %} +
+{{ cliente_proveedor.colonia }} {% if cliente_proveedor.codigo_postal %} CP: {{ cliente_proveedor.codigo_postal }}{% endif %}
+{{ cliente_proveedor.ciudad }}, {{ cliente_proveedor.estado }}, {{ cliente_proveedor.pais }}
+TAX ID: {{ cliente_proveedor.tax_id }} + {% if cliente_proveedor.programa and cliente_proveedor.programa != 'Ninguno' %} + {{ cliente_proveedor.programa }}: {{ cliente_proveedor.autorizacion }} + {% endif %} +
+|
+ FACTURA: + |
+
+ {{ factura.numero }} + |
+ ||
|
+ Fecha: + |
+
+ {{ factura.fecha }} + |
+
+ T. Cambio: + |
+
+ {{ factura.tipo_cambio }} + |
+
|
+ Pedimento: + |
+
+ {{ factura.pedimento }} + |
+
+ Clave: + |
+
+ {{ factura.clave_pedimento }} + |
+
|
+ Remesa: + |
+
+ {{ factura.remesa }} + |
+
+ Acuse: + |
+
+ {{ factura.acuse_electronico or 'N/A' }} + |
+
|
+ Agente Aduanal: +{{ factura.agente_aduanal or '' }} + |
+ |||
|
+ Patente: {{ factura.patente or '' }} + |
+ + Regimen:{{ factura.regimen or '' }} + | +
+ INCOTERM: +{{ factura.incoterm or '' }} + |
+ |
|
+ {% if factura.precinto %}
+ Precinto: {{ factura.precinto }} + {% endif %} + |
+
+ Aduana: {{ factura.aduana }} + |
+
+ {% if factura.destino %}
+ Destino: {{ factura.destino }} + {% endif %} + |
+ |
{{ cliente_vendido.nombre }}
+{{ cliente_vendido.direccion }} + {% if cliente_vendido.num_exterior %} Ext: {{ cliente_vendido.num_exterior }}{% endif %} + {% if cliente_vendido.num_interior %} Int: {{ cliente_vendido.num_interior }}{% endif %} +
+{{ cliente_vendido.colonia }} {% if cliente_vendido.codigo_postal %} CP: {{ cliente_vendido.codigo_postal }}{% endif %}
+{{ cliente_vendido.ciudad }}, {{ cliente_vendido.estado }}, {{ cliente_vendido.pais }}
+RFC: {{ cliente_vendido.tax_id }} + {% if cliente_vendido.programa and cliente_vendido.programa != 'Ninguno' %} + {{ cliente_vendido.programa }}: {{ cliente_vendido.autorizacion }} + {% endif %} +
+{{ cliente_enviado.nombre }}
+{{ cliente_enviado.direccion }} + {% if cliente_enviado.num_exterior %} Ext: {{ cliente_enviado.num_exterior }}{% endif %} + {% if cliente_enviado.num_interior %} Int: {{ cliente_enviado.num_interior }}{% endif %} +
+{{ cliente_enviado.colonia }} {% if cliente_enviado.codigo_postal %} CP: {{ cliente_enviado.codigo_postal }}{% endif %}
+{{ cliente_enviado.ciudad }}, {{ cliente_enviado.estado }}, {{ cliente_enviado.pais }}
+RFC: {{ cliente_enviado.tax_id }}
+Transportista: |
+ {{ factura.transportista }} |
+ SCAC: {{ factura.scac }} |
+ INCOTERM: |
+ {{ factura.incoterm }} |
+
+ Aduana: {{ factura.aduana }} + |
+ ||||
|
+ Transporte: + |
+ {{ factura.transporte }}: {{ factura.num_transporte }} |
+ CAAT: {{ factura.caat }} |
+
+ Placas: {{ factura.placas or 'N/A' }} + |
+ ||||||
|
+ Línea + |
+
+ Número de Parte +Descripción + |
+
+ Comercial + |
+
+ Empaque + |
+
+ Peso (KGS) + |
+
+ Valores + |
+ ||||
|
+ Cantidad + |
+
+ U.M. + |
+
+ Tipo + |
+
+ Neto + |
+
+ Bruto + |
+
+ Unitario + |
+
+ Total + |
+ |||
|
+ {{ loop.index }} + |
+
+ {{ partida.numero_parte }} +{{ partida.descripcion }} +Frac: {{ partida.fraccion }} / Orig: {{ partida.origen or 'MEX' }} + |
+
+ {{ partida.cantidad_importacion }} + |
+
+ {{ partida.unidad_medida }} + |
+
+ + {% if partida.cantidad_bultos != 0 %}{{ partida.cantidad_bultos }}{% endif %} + {{ partida.clave_bultos }} + + |
+
+ {{ partida.peso_neto }} + |
+
+ {{ partida.peso_bruto }} + |
+
+ ${{ partida.valor_costo_unitario }} + |
+
+ ${{ partida.valor_total }} + |
+ |
|
+ + Observaciones: + TOTALES + + |
+
+ {{ totales.cantidad_total }} + |
+ + |
+ + {% if totales.bultos_total != 0 %}{{ totales.bultos_total }}{% endif %} + {{ totales.clave_bultos or '' }} + + |
+
+ {{ totales.peso_neto_total }} + |
+
+ {{ totales.peso_bruto_total }} + |
+ + |
+ ${{ totales.valor_total_total }} + |
+ ||
|
+ {{ factura.observaciones }} + |
+
+
+ {{ cliente_proveedor.nombre }} +Los valores expresados en esta factura son en: {{ factura.moneda }} + |
+ ||||||||
|
+ Normal Por Parte +Declaro bajo protesta de decir verdad que la información contenida en este documento es verdadera y me hago responsable de comprobar lo aquí declarado. + |
+ |||||||||
- Gestiona las facturas del sistema -
-+ Gestiona las facturas del sistema +
+