/* ============================================================ PDF GENERATION — Tipos: x (Pedido), a, b, c (Factura) Soporta CAE/ARCA cuando se pasa invoiceData con cae. ============================================================ */ const PDF_DOC_TYPES = { x: { label: 'PEDIDO', letter: 'X', cod: null, showIva: false }, a: { label: 'FACTURA', letter: 'A', cod: 'COD. 001', showIva: true }, b: { label: 'FACTURA', letter: 'B', cod: 'COD. 006', showIva: false }, c: { label: 'FACTURA', letter: 'C', cod: 'COD. 011', showIva: false } }; const IVA_LABEL = { responsable_inscripto: 'IVA Responsable Inscripto', monotributista: 'Monotributista', exento: 'IVA Exento', }; // Module-level cache for company/AFIP config data let _companyCache = null; let _companyCacheTime = 0; async function _getCompanyData() { const now = Date.now(); if (_companyCache && now - _companyCacheTime < 300000) return _companyCache; try { const data = await apiFetch('/api/afip/config'); _companyCache = data; _companyCacheTime = now; return data; } catch { return null; } } /** Genera QR como data URL usando qrcodejs (si disponible) */ async function _generateQRDataURL(text) { if (typeof QRCode === 'undefined') return null; return new Promise(resolve => { const wrapper = document.createElement('div'); wrapper.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:80px;height:80px'; document.body.appendChild(wrapper); new QRCode(wrapper, { text, width: 80, height: 80, correctLevel: QRCode.CorrectLevel.M }); setTimeout(() => { const canvas = wrapper.querySelector('canvas'); const url = canvas ? canvas.toDataURL('image/png') : null; document.body.removeChild(wrapper); resolve(url); }, 60); }); } /** * Genera y descarga el PDF de una factura/pedido. * * @param {object} client - Datos del cliente {name, address, cuit, tax} * @param {string} docType - Tipo: 'x', 'a', 'b', 'c' * @param {Array} items - Ítems [{name, qty, price}] * @param {number} total - Total del comprobante * @param {string} invoiceId - ID local de la factura (opcional) * @param {number} orderNumber - Número de orden (opcional) * @param {object} invoiceData - Datos AFIP {cae, cae_vencimiento, afip_numero, afip_punto_venta, qr_data} */ async function generateInvoicePDF(client, docType, items, total, invoiceId = null, orderNumber = null, invoiceData = null) { if (!window.jspdf) { showToast('jsPDF no está disponible', 'error'); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF({ unit: 'mm', format: 'a4' }); const typeInfo = PDF_DOC_TYPES[docType] || PDF_DOC_TYPES.x; const dateStr = getCurrentDate(); const isPresup = docType === 'x' || !PDF_DOC_TYPES[docType]; const cae = invoiceData?.cae ?? null; const caeVto = invoiceData?.cae_vencimiento ?? null; const afipNum = invoiceData?.afip_numero ?? null; const afipPv = invoiceData?.afip_punto_venta ?? null; const qrUrl = invoiceData?.qr_data ?? null; // Fetch company data for header (cached) const company = await _getCompanyData(); const co = { name: (company?.razon_social || CONFIG.APP_NAME || 'La Distribuidora').toUpperCase(), domicilio: (company?.domicilio_comercial || 'MAYORISTA DE ALIMENTOS - NECOCHEA').toUpperCase(), cuit: company?.cuit || null, ivaLabel: IVA_LABEL[company?.tipo_contribuyente] ?? null, iibb: company?.iibb || null, inicioAct: company?.inicio_actividades || null, }; // Número de comprobante: si tiene datos AFIP, usa ptoVta-número; si no, usa ID local let numDoc; if (afipPv && afipNum) { numDoc = String(afipPv).padStart(4, '0') + '-' + String(afipNum).padStart(8, '0'); } else { const rawNum = orderNumber ? String(orderNumber).padStart(8, '0') : invoiceId ? invoiceId.split('-').pop().substring(0, 8).padEnd(8, '0') : String(Math.floor(Math.random() * 99999999)).padStart(8, '0'); numDoc = `0001-${rawNum}`; } // Para Factura A mostramos precios netos (sin IVA) en los ítems const showNetPrices = (docType === 'a'); const netAmount = typeInfo.showIva ? total / 1.21 : total; const ivaAmount = typeInfo.showIva ? total - netAmount : 0; // QR image data URL (async, solo si hay URL y librería) let qrImgData = null; if (qrUrl) { qrImgData = await _generateQRDataURL(qrUrl); } // Layout constants const lM = 7; const pageW = 196; const rEdge = lM + pageW; const qX = lM + 3; const dX = lM + 16; const puX = 152; const stX = 181; // ── Dibuja una mitad de hoja (original o duplicado) ─────────────── function drawHalf(oY, copyLabel) { const boxH = 115; doc.setDrawColor(0); doc.setLineWidth(0.4); doc.rect(lM, oY + 4, pageW, boxH); // ── Cabecera empresa (izquierda, hasta x=92) ───────────────────── doc.setFont('helvetica', 'bold'); doc.setFontSize(11); doc.text(co.name.substring(0, 40), lM + 3, oY + 11); doc.setFont('helvetica', 'normal'); doc.setFontSize(6.5); doc.text(co.domicilio.substring(0, 52), lM + 3, oY + 15); // CUIT de la empresa let companyRow2 = []; if (co.cuit) companyRow2.push(`CUIT: ${co.cuit}`); if (co.ivaLabel) companyRow2.push(co.ivaLabel); if (companyRow2.length) { doc.setFontSize(6); doc.text(companyRow2.join(' | '), lM + 3, oY + 18.5); } // IIBB e inicio de actividades let companyRow3 = []; if (co.iibb) companyRow3.push(`IIBB: ${co.iibb}`); if (co.inicioAct) { const [y, m, d] = (co.inicioAct + '').split('-'); const fmtDate = (d && m && y) ? `${d}/${m}/${y}` : co.inicioAct; companyRow3.push(`Inicio Actividades: ${fmtDate}`); } if (companyRow3.length) { doc.setFontSize(6); doc.text(companyRow3.join(' | '), lM + 3, oY + 22); } // ── Cuadro central con letra del comprobante ───────────────────── doc.setLineWidth(0.6); doc.rect(92, oY + 4, 26, 18); doc.setLineWidth(0.4); doc.setFont('helvetica', 'bold'); doc.setFontSize(24); doc.text(typeInfo.letter, 105, oY + 17.5, { align: 'center' }); if (typeInfo.cod) { doc.setFontSize(6); doc.setFont('helvetica', 'normal'); doc.text(typeInfo.cod, 105, oY + 22, { align: 'center' }); } // ── Cabecera documento (derecha) ───────────────────────────────── doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.text(typeInfo.label, 121, oY + 10); doc.setFont('helvetica', 'normal'); doc.setFontSize(7.5); doc.text(`N°: ${numDoc}`, 121, oY + 14.5); doc.text(`FECHA: ${dateStr}`, 121, oY + 18.5); doc.setFont('helvetica', 'italic'); doc.setFontSize(6.5); doc.text(copyLabel.toUpperCase(), 121, oY + 22); // ── Separador 1 ────────────────────────────────────────────────── doc.setFont('helvetica', 'normal'); doc.line(lM, oY + 26.5, rEdge, oY + 26.5); // ── Datos del cliente ──────────────────────────────────────────── doc.setFontSize(8); const col2 = 112; doc.setFont('helvetica', 'bold'); doc.text('Cliente:', lM + 3, oY + 30); doc.setFont('helvetica', 'normal'); const displayName = (client.name || '').toUpperCase(); doc.text(displayName.substring(0, 38), lM + 22, oY + 30); doc.setFont('helvetica', 'bold'); doc.text('COND.:', col2, oY + 30); doc.setFont('helvetica', 'normal'); const displayTax = docType === 'x' ? '—' : (client.tax || '—').toUpperCase(); doc.text(displayTax.substring(0, 15), col2 + 15, oY + 30); doc.setFont('helvetica', 'bold'); doc.text('DOMICILIO:', lM + 3, oY + 33); doc.setFont('helvetica', 'normal'); doc.text((client.address || '—').toUpperCase().substring(0, 38), lM + 22, oY + 33); doc.setFont('helvetica', 'bold'); doc.text('CUIT:', col2, oY + 33); doc.setFont('helvetica', 'normal'); doc.text(client.cuit || '—', col2 + 15, oY + 33); // ── Separador 2 ────────────────────────────────────────────────── doc.line(lM, oY + 36, rEdge, oY + 36); // ── Cabecera tabla de ítems ────────────────────────────────────── doc.setFont('helvetica', 'bold'); doc.setFontSize(7.5); doc.text('CANT.', qX, oY + 39.5); doc.text('DESCRIPCIÓN', dX, oY + 39.5); // Para Factura A: columna "P. NETO" en lugar de "P. UNIT." doc.text(showNetPrices ? 'P. NETO' : 'P. UNIT.', puX, oY + 39.5); doc.text('SUBTOTAL', stX, oY + 39.5); doc.line(lM, oY + 41, rEdge, oY + 41); // ── Ítems ──────────────────────────────────────────────────────── doc.setFont('helvetica', 'normal'); doc.setFontSize(9); const rowH = 4.5; const maxDescW = puX - dX - 4; const maxItemY = oY + 100; let rowY = oY + 44.5; items.forEach(item => { if (rowY > maxItemY) return; const nameLine = doc.splitTextToSize(String(item.name || '').toUpperCase(), maxDescW)[0]; const unitPrice = showNetPrices ? item.price / 1.21 : item.price; const subtotal = unitPrice * item.qty; doc.text(String(item.qty), qX, rowY); doc.text(nameLine, dX, rowY); doc.text(fmt(unitPrice), puX, rowY); doc.text(fmt(subtotal), stX, rowY); rowY += rowH; }); // ── Separador totales ──────────────────────────────────────────── doc.line(lM, oY + 100.5, rEdge, oY + 100.5); if (typeInfo.showIva) { doc.setFont('helvetica', 'normal'); doc.setFontSize(6.5); doc.text('NETO GRAVADO:', 140, oY + 102.5); doc.text(fmt(netAmount), stX, oY + 102.5); doc.text('IVA 21%:', 140, oY + 105); doc.text(fmt(ivaAmount), stX, oY + 105); doc.setLineWidth(0.2); doc.line(138, oY + 106.2, rEdge, oY + 106.2); doc.setFont('helvetica', 'bold'); doc.setFontSize(14); doc.text(`TOTAL: ${fmt(total)}`, 105, oY + 108.5, { align: 'center' }); } else { doc.setFont('helvetica', 'bold'); doc.setFontSize(14); doc.text(`TOTAL: ${fmt(total)}`, 105, oY + 105.5, { align: 'center' }); } // ── Pie: CAE / autorización ARCA o nota de validez ─────────────── if (cae) { // Comprobante autorizado → mostrar CAE + QR const fmtVto = caeVto ? caeVto.replace(/(\d{4})-?(\d{2})-?(\d{2})/, '$3/$2/$1') : ''; doc.setFont('helvetica', 'bold'); doc.setFontSize(5.5); doc.text('COMPROBANTE AUTORIZADO POR ARCA', lM + 3, oY + 110.5); doc.setFont('helvetica', 'normal'); doc.setFontSize(5.5); doc.text(`CAE: ${cae} Vto: ${fmtVto}`, lM + 3, oY + 113); if (qrImgData) { doc.addImage(qrImgData, 'PNG', rEdge - 20, oY + 108.5, 18, 18); } } else if (!isPresup) { // Factura A/B/C sin CAE todavía doc.setFont('helvetica', 'italic'); doc.setFontSize(5); doc.text('PENDIENTE DE AUTORIZACIÓN AFIP/ARCA', lM + 3, oY + 114.5); } else { // Pedido/presupuesto - nota de no validez doc.setFont('helvetica', 'italic'); doc.setFontSize(5); doc.text('DOCUMENTO NO VÁLIDO COMO COMPROBANTE FISCAL. USO INTERNO.', lM + 3, oY + 114.5); } } // ── Página: ORIGINAL arriba, DUPLICADO abajo ────────────────────── drawHalf(0, 'ORIGINAL'); doc.setDrawColor(140); doc.setLineDashPattern([2.5, 2.5], 0); doc.line(0, 148.5, 210, 148.5); doc.setLineDashPattern([], 0); doc.setDrawColor(0); drawHalf(148.5, 'DUPLICADO'); // ── Guardar archivo ─────────────────────────────────────────────── const prefix = isPresup ? 'Pedido' : `Factura-${typeInfo.letter}`; const safeName = (client.name || 'cliente').replace(/[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑ]/g, '_'); const safeDate = dateStr.replace(/\//g, '-'); const suffix = cae ? `_CAE${cae.slice(-6)}` : ''; doc.save(`${prefix}_${safeName}_${safeDate}${suffix}.pdf`); }