// /home/Autopartes/pos/static/js/accounting.js // Accounting module: chart of accounts, journal entries, financial reports const Accounting = (() => { const API = '/pos/api/accounting'; let accounts = []; // cached for dropdowns let entryLineCtr = 0; // counter for entry line IDs function token() { return localStorage.getItem('pos_token') || ''; } function headers() { return { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' }; } async function api(path, opts = {}) { const res = await fetch(`${API}${path}`, { headers: headers(), ...opts }); if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); throw new Error(err.error || 'Request failed'); } return res.json(); } function fmt(n) { return parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } // ─── Tabs ────────────────────────────────────── function initTabs() { document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active')); tab.classList.add('active'); document.getElementById(`tab-${tab.dataset.tab}`).classList.add('active'); // Load data on tab switch const t = tab.dataset.tab; if (t === 'accounts') loadAccounts(); if (t === 'entries') loadEntries(); if (t === 'trial-balance') loadTrialBalance(); if (t === 'income-statement') loadIncomeStatement(); if (t === 'balance-sheet') loadBalanceSheet(); if (t === 'aging') loadAging(); if (t === 'periods') loadPeriods(); }); }); } // ─── Chart of Accounts ───────────────────────── async function loadAccounts() { try { const res = await api('/accounts'); accounts = res.data || []; renderAccountsTree(); } catch (e) { document.getElementById('accounts-tree').innerHTML = `

Error: ${e.message}

`; } } function renderAccountsTree() { const container = document.getElementById('accounts-tree'); if (!accounts.length) { container.innerHTML = '

No hay cuentas.

'; return; } // Build tree structure const byParent = {}; accounts.forEach(a => { const pid = a.parent_id || 'root'; if (!byParent[pid]) byParent[pid] = []; byParent[pid].push(a); }); function buildUl(parentId) { const children = byParent[parentId] || []; if (!children.length) return ''; let html = ''; return html; } container.innerHTML = buildUl('root'); } function showNewAccountModal() { const sel = document.getElementById('na-parent'); sel.innerHTML = ''; accounts.forEach(a => { sel.innerHTML += ``; }); document.getElementById('na-code').value = ''; document.getElementById('na-name').value = ''; document.getElementById('new-account-modal').classList.add('active'); } async function createAccount() { try { const parentId = document.getElementById('na-parent').value; await api('/accounts', { method: 'POST', body: JSON.stringify({ code: document.getElementById('na-code').value, name: document.getElementById('na-name').value, parent_id: parentId ? parseInt(parentId) : null, type: document.getElementById('na-type').value, }), }); closeModal('new-account-modal'); loadAccounts(); } catch (e) { alert('Error: ' + e.message); } } // ─── Journal Entries ─────────────────────────── async function loadEntries() { try { const from = document.getElementById('entries-from').value; const to = document.getElementById('entries-to').value; const type = document.getElementById('entries-type').value; let qs = '?per_page=50'; if (from) qs += `&date_from=${from}`; if (to) qs += `&date_to=${to}`; if (type) qs += `&type=${type}`; const res = await api(`/entries${qs}`); renderEntries(res.data || []); } catch (e) { document.getElementById('entries-list').innerHTML = `

Error: ${e.message}

`; } } function renderEntries(entries) { const container = document.getElementById('entries-list'); if (!entries.length) { container.innerHTML = '

No hay polizas en este periodo.

'; return; } let html = ``; for (const e of entries) { html += ``; } html += '
#FechaTipoDescripcion ReferenciaMontoAuto
${e.entry_number} ${e.date || ''} ${e.type || ''} ${e.description || ''} ${e.reference_type ? `${e.reference_type} #${e.reference_id}` : ''} $${fmt(e.total_amount)} ${e.is_auto ? 'Si' : 'No'}
'; container.innerHTML = html; } async function showEntryDetail(entryId) { try { const entry = await api(`/entries/${entryId}`); let html = `

Poliza #${entry.entry_number}

Fecha: ${entry.date} Tipo: ${entry.type} Estado: ${entry.status}

${entry.description || ''}

`; for (const l of entry.lines) { html += ``; } html += `
CuentaNombreCargo AbonoNota
${l.account_code} ${l.account_name} ${l.debit ? '$' + fmt(l.debit) : ''} ${l.credit ? '$' + fmt(l.credit) : ''} ${l.description || ''}
Totales $${fmt(entry.total_debit)} $${fmt(entry.total_credit)}
`; document.getElementById('entry-detail-content').innerHTML = html; document.getElementById('entry-detail-modal').classList.add('active'); } catch (e) { alert('Error: ' + e.message); } } // ─── Manual Entry ────────────────────────────── function showNewEntryModal() { document.getElementById('ne-date').value = new Date().toISOString().slice(0, 10); document.getElementById('ne-description').value = ''; document.getElementById('ne-lines').innerHTML = ''; document.getElementById('ne-balance').innerHTML = ''; entryLineCtr = 0; addEntryLine(); addEntryLine(); document.getElementById('new-entry-modal').classList.add('active'); } function addEntryLine() { const id = entryLineCtr++; const tbody = document.getElementById('ne-lines'); const tr = document.createElement('tr'); tr.id = `ne-line-${id}`; let acctOptions = ''; accounts.forEach(a => { if (a.parent_id) { // Only leaf accounts acctOptions += ``; } }); tr.innerHTML = ` `; tbody.appendChild(tr); } function updateEntryBalance() { const rows = document.querySelectorAll('#ne-lines tr'); let totalDebit = 0, totalCredit = 0; rows.forEach(row => { const inputs = row.querySelectorAll('input[type="number"]'); totalDebit += parseFloat(inputs[0].value) || 0; totalCredit += parseFloat(inputs[1].value) || 0; }); const diff = Math.round((totalDebit - totalCredit) * 100) / 100; const el = document.getElementById('ne-balance'); if (diff === 0) { el.innerHTML = `Cuadrada: Cargos $${fmt(totalDebit)} = Abonos $${fmt(totalCredit)}`; } else { el.innerHTML = `Descuadre: $${fmt(Math.abs(diff))} (Cargos $${fmt(totalDebit)} / Abonos $${fmt(totalCredit)})`; } } async function createEntry() { try { const rows = document.querySelectorAll('#ne-lines tr'); const lines = []; rows.forEach(row => { const sel = row.querySelector('select'); const inputs = row.querySelectorAll('input[type="number"]'); const acctId = parseInt(sel.value); const debit = parseFloat(inputs[0].value) || 0; const credit = parseFloat(inputs[1].value) || 0; if (acctId && (debit > 0 || credit > 0)) { lines.push({ account_id: acctId, debit, credit, description: '' }); } }); if (lines.length < 2) { alert('Se requieren al menos 2 lineas.'); return; } await api('/entries', { method: 'POST', body: JSON.stringify({ date: document.getElementById('ne-date').value, description: document.getElementById('ne-description').value, lines, }), }); closeModal('new-entry-modal'); loadEntries(); } catch (e) { alert('Error: ' + e.message); } } // ─── Trial Balance ───────────────────────────── async function loadTrialBalance() { try { const year = document.getElementById('tb-year').value; const month = document.getElementById('tb-month').value; const res = await api(`/trial-balance?year=${year}&month=${month}`); renderTrialBalance(res); } catch (e) { document.getElementById('trial-balance-content').innerHTML = `

Error: ${e.message}

`; } } function renderTrialBalance(data) { const rows = data.data || []; let html = ``; let totals = { si: 0, c: 0, a: 0, sf: 0 }; for (const r of rows) { totals.si += r.saldo_inicial; totals.c += r.cargos; totals.a += r.abonos; totals.sf += r.saldo_final; html += ``; } html += `
CodigoCuenta Saldo InicialCargos AbonosSaldo Final
${r.code}${r.name} $${fmt(r.saldo_inicial)} $${fmt(r.cargos)} $${fmt(r.abonos)} $${fmt(r.saldo_final)}
Totales $${fmt(totals.si)} $${fmt(totals.c)} $${fmt(totals.a)} $${fmt(totals.sf)}
`; document.getElementById('trial-balance-content').innerHTML = html; } // ─── Income Statement ────────────────────────── async function loadIncomeStatement() { try { const year = document.getElementById('is-year').value; const month = document.getElementById('is-month').value; const res = await api(`/income-statement?year=${year}&month=${month}`); renderIncomeStatement(res); } catch (e) { document.getElementById('income-statement-content').innerHTML = `

Error: ${e.message}

`; } } function renderIncomeStatement(data) { let html = ''; // Ingresos html += ''; for (const item of data.ingresos.items) { html += ``; } html += ``; // Costos html += ''; for (const item of data.costos.items) { html += ``; } html += ``; // Utilidad bruta html += ``; // Gastos html += ''; for (const item of data.gastos.items) { html += ``; } html += ``; // Utilidad neta const netColor = data.utilidad_neta >= 0 ? 'var(--success)' : 'var(--danger)'; html += ``; html += '
INGRESOS
${item.code} - ${item.name}$${fmt(item.amount)}
Total Ingresos$${fmt(data.ingresos.total)}
COSTOS
${item.code} - ${item.name}$${fmt(item.amount)}
Total Costos$${fmt(data.costos.total)}
UTILIDAD BRUTA$${fmt(data.utilidad_bruta)}
GASTOS
${item.code} - ${item.name}$${fmt(item.amount)}
Total Gastos$${fmt(data.gastos.total)}
UTILIDAD NETA $${fmt(data.utilidad_neta)}
'; document.getElementById('income-statement-content').innerHTML = html; } // ─── Balance Sheet ───────────────────────────── async function loadBalanceSheet() { try { const asOf = document.getElementById('bs-date').value; const res = await api(`/balance-sheet?date=${asOf}`); renderBalanceSheet(res); } catch (e) { document.getElementById('balance-sheet-content').innerHTML = `

Error: ${e.message}

`; } } function renderBalanceSheet(data) { const balancedBadge = data.balanced ? 'Cuadrado' : 'Descuadrado'; let html = `

Al ${data.as_of} ${balancedBadge}

`; // Activo html += ''; for (const item of data.activo.items) { html += ``; } html += ``; // Pasivo html += ''; for (const item of data.pasivo.items) { html += ``; } html += ``; // Capital html += ''; for (const item of data.capital.items) { html += ``; } html += ``; html += ``; html += '
ACTIVO
${item.code} - ${item.name}$${fmt(item.balance)}
Total Activo$${fmt(data.activo.total)}
PASIVO
${item.code} - ${item.name}$${fmt(item.balance)}
Total Pasivo$${fmt(data.pasivo.total)}
CAPITAL
${item.code} - ${item.name}$${fmt(item.balance)}
Total Capital$${fmt(data.capital.total)}
Pasivo + Capital $${fmt(data.pasivo.total + data.capital.total)}
'; document.getElementById('balance-sheet-content').innerHTML = html; } // ─── Aging ───────────────────────────────────── async function loadAging() { try { const res = await api('/aging'); renderAging(res); } catch (e) { document.getElementById('aging-content').innerHTML = `

Error: ${e.message}

`; } } function renderAging(data) { const rows = data.data || []; let html = ``; for (const r of rows) { html += ``; } const t = data.totals || {}; html += `
ClienteCorriente 1-30d31-60d 61-90d90+d Total
${r.name} $${fmt(r.corriente)} $${fmt(r.d1_30)} $${fmt(r.d31_60)} $${fmt(r.d61_90)} $${fmt(r.d90_plus)} $${fmt(r.total)}
Totales $${fmt(t.corriente)} $${fmt(t.d1_30)} $${fmt(t.d31_60)} $${fmt(t.d61_90)} $${fmt(t.d90_plus)} $${fmt(t.total)}
`; document.getElementById('aging-content').innerHTML = html; } // ─── Periods ─────────────────────────────────── async function loadPeriods() { try { const res = await api('/periods'); renderPeriods(res.data || []); } catch (e) { document.getElementById('periods-list').innerHTML = `

Error: ${e.message}

`; } } function renderPeriods(periods) { const months = ['', 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; let html = ``; for (const p of periods) { const badge = p.status === 'closed' ? 'Cerrado' : 'Abierto'; html += ``; } html += '
PeriodoEstadoCerrado porFecha cierre
${months[p.month]} ${p.year} ${badge} ${p.closed_by_name || '-'} ${p.closed_at ? new Date(p.closed_at).toLocaleDateString('es-MX') : '-'}
'; document.getElementById('periods-list').innerHTML = html; } async function closePeriod() { const year = parseInt(document.getElementById('cp-year').value); const month = parseInt(document.getElementById('cp-month').value); if (!confirm(`Cerrar periodo ${month}/${year}? Esta accion no se puede revertir.`)) return; try { await api('/periods/close', { method: 'POST', body: JSON.stringify({ year, month }), }); loadPeriods(); alert('Periodo cerrado exitosamente.'); } catch (e) { alert('Error: ' + e.message); } } // ─── Modal helpers ───────────────────────────── function closeModal(id) { document.getElementById(id).classList.remove('active'); } // ─── Init ────────────────────────────────────── function init() { initTabs(); // Set default period values const now = new Date(); ['tb-year', 'is-year', 'cp-year'].forEach(id => { const el = document.getElementById(id); if (el) el.value = now.getFullYear(); }); ['tb-month', 'is-month', 'cp-month'].forEach(id => { const el = document.getElementById(id); if (el) el.value = now.getMonth() + 1; }); const bsDate = document.getElementById('bs-date'); if (bsDate) bsDate.value = now.toISOString().slice(0, 10); // Set default entry date filters const firstDay = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10); document.getElementById('entries-from').value = firstDay; document.getElementById('entries-to').value = now.toISOString().slice(0, 10); loadAccounts(); } document.addEventListener('DOMContentLoaded', init); // Public API return { loadAccounts, loadEntries, loadTrialBalance, loadIncomeStatement, loadBalanceSheet, loadAging, loadPeriods, closePeriod, showNewAccountModal, createAccount, showNewEntryModal, addEntryLine, updateEntryBalance, createEntry, showEntryDetail, closeModal, }; })();