import os import pandas as pd import datetime import xml.etree.ElementTree as ET import csv from fpdf import FPDF from pyafipws.wsaa import WSAA from pyafipws.wsfev1 import WSFEv1 # Rutas EXCEL_PATH = "input/facturas.xlsx" CONFIG_DIR = "config" OUTPUT_DIR = "output" LOG_FILE = os.path.join(OUTPUT_DIR, "log_facturacion.csv") os.makedirs(OUTPUT_DIR, exist_ok=True) # Iniciar archivo log with open(LOG_FILE, mode='w', newline='') as f: writer = csv.writer(f) writer.writerow(["Nro Factura", "CUIT Emisor", "CUIT Cliente", "Fecha", "Total", "CAE", "Estado", "Mensaje"]) # Función TA def obtener_ta_si_no_existe(cuit_emisor): emisor_path = os.path.join(CONFIG_DIR, cuit_emisor) ta_file = os.path.join(emisor_path, "ta.xml") cert = os.path.join(emisor_path, "cert.crt") key = os.path.join(emisor_path, "clave.key") os.makedirs(emisor_path, exist_ok=True) wsaa = WSAA() try: tree = ET.parse(ta_file) venc = tree.find(".//expirationTime").text venc_dt = datetime.datetime.strptime(venc, "%Y-%m-%dT%H:%M:%S.%f%z") if venc_dt > datetime.datetime.now(datetime.timezone.utc): print(f"🕒 TA vigente para CUIT {cuit_emisor}") return open(ta_file, 'r').read() else: raise Exception("TA vencido") except Exception: print(f"🔁 Generando nuevo TA para CUIT {cuit_emisor}...") ta_string = wsaa.Autenticar( service="wsfe", crt=cert, key=key, wsdl="https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl", cache=ta_file ) print(f"✅ TA generado y guardado en {ta_file}") return ta_string # Leer Excel df = pd.read_excel(EXCEL_PATH) conexiones = {} print("🧾 Iniciando emisión de facturas electrónicas...\n") for _, row in df.iterrows(): try: cuit_emisor = str(int(row['CUIT Emisor'])) nro_factura = int(row['Nro Factura']) cuit_cliente = int(row['CUIT Cliente']) concepto = int(row['Concepto']) doc_tipo = int(row['Tipo Documento']) cbte_tipo = int(row['Tipo Factura']) punto_vta = int(row['Punto de Venta']) total = float(str(row['Total']).replace(',', '.')) neto = float(str(row['Neto']).replace(',', '.')) iva = float(str(row['IVA']).replace(',', '.')) fecha_cbte = row['Fecha'].strftime('%Y%m%d') fecha_vto_pago = row['F. Vto. Pago'].strftime('%Y%m%d') fecha_serv_desde = row['F. Servicio Desde'].strftime('%Y%m%d') fecha_serv_hasta = row['F. Servicio Hasta'].strftime('%Y%m%d') moneda_id = 'PES' aliquota_id = int(row['Alícuota']) except Exception as e: print(f"❌ Datos inválidos en fila: {e}") continue cert_file = os.path.join(CONFIG_DIR, cuit_emisor, "cert.crt") key_file = os.path.join(CONFIG_DIR, cuit_emisor, "clave.key") if not (os.path.exists(cert_file) and os.path.exists(key_file)): print(f"❌ Faltan certificados para CUIT {cuit_emisor}") continue ta_xml = obtener_ta_si_no_existe(cuit_emisor) if cuit_emisor not in conexiones: wsfe = WSFEv1() wsfe.SetTicketAcceso(ta_xml) wsfe.Cuit = int(cuit_emisor) wsfe.URL = "https://wswhomo.afip.gov.ar/wsfev1/service.asmx" wsfe.Conectar() conexiones[cuit_emisor] = wsfe else: wsfe = conexiones[cuit_emisor] # Generar lista de IVA válida iva_array = [] if iva > 0.0 or aliquota_id in [2, 3, 4, 5, 6, 8, 9]: iva_array = [{"Id": aliquota_id, "BaseImp": neto, "Importe": iva}] print("📤 Enviando a AFIP:", { "concepto": concepto, "doc_tipo": doc_tipo, "doc_nro": cuit_cliente, "cbte_tipo": cbte_tipo, "punto_vta": punto_vta, "cbt_desde": nro_factura, "imp_total": total, "imp_neto": neto, "imp_iva": iva, "fecha_cbte": fecha_cbte, "fecha_venc_pago": fecha_vto_pago, "fecha_serv_desde": fecha_serv_desde, "fecha_serv_hasta": fecha_serv_hasta, "moneda_id": moneda_id, "moneda_ctz": 1, "iva": iva_array, "tributos": [] }) try: wsfe.CrearFactura( concepto=concepto, doc_tipo=doc_tipo, doc_nro=cuit_cliente, cbte_tipo=cbte_tipo, punto_vta=punto_vta, cbt_desde=nro_factura, cbt_hasta=nro_factura, imp_total=total, imp_neto=neto, imp_iva=iva, fecha_cbte=fecha_cbte, fecha_venc_pago=fecha_vto_pago, fecha_serv_desde=fecha_serv_desde, fecha_serv_hasta=fecha_serv_hasta, moneda_id=moneda_id, moneda_ctz=1, iva=iva_array, tributos=[] ) print("\n🔍 XML que se está enviando a AFIP:\n") print(wsfe.XmlRequest) print("\n📤 Ejecutando CAESolicitar...\n") wsfe.CAESolicitar() if wsfe.CAE: print(f"✅ Factura #{nro_factura} emitida. CAE: {wsfe.CAE}") with open(LOG_FILE, mode='a', newline='') as f: writer = csv.writer(f) writer.writerow([ nro_factura, cuit_emisor, cuit_cliente, row['Fecha'].strftime('%Y-%m-%d'), total, wsfe.CAE, "Emitida", "OK" ]) pdf = FPDF() pdf.add_page(); pdf.set_font("Arial", size=12) pdf.cell(200, 10, txt="Factura Electrónica", ln=True, align='C') pdf.cell(200, 10, txt=f"CUIT Emisor: {cuit_emisor}", ln=True) pdf.cell(200, 10, txt=f"Nro Factura: {nro_factura}", ln=True) pdf.cell(200, 10, txt=f"Fecha: {row['Fecha'].strftime('%d/%m/%Y')}", ln=True) pdf.cell(200, 10, txt=f"CUIT Cliente: {cuit_cliente}", ln=True) pdf.cell(200, 10, txt=f"Importe Neto: ${neto:.2f}", ln=True) pdf.cell(200, 10, txt=f"IVA: ${iva:.2f}", ln=True) pdf.cell(200, 10, txt=f"Total: ${total:.2f}", ln=True) pdf.cell(200, 10, txt=f"CAE: {wsfe.CAE}", ln=True) pdf.cell(200, 10, txt=f"Vto CAE: {wsfe.Vencimiento}", ln=True) pdf.output(os.path.join(OUTPUT_DIR, f"{nro_factura}_{cuit_emisor}.pdf")) else: msg = wsfe.ErrMsg or wsfe.Obs or "Desconocido" print(f"❌ Error en factura #{nro_factura}: {msg}") with open(LOG_FILE, mode='a', newline='') as f: writer = csv.writer(f) writer.writerow([ nro_factura, cuit_emisor, cuit_cliente, row['Fecha'].strftime('%Y-%m-%d'), total, "", "Error", msg ]) except Exception as e: print(f"❌ Error emitiendo factura #{nro_factura}: {e}") print("\n🏁 Proceso finalizado.")