diff --git a/pos/static/js/accounting.js b/pos/static/js/accounting.js new file mode 100644 index 0000000..b3bb513 --- /dev/null +++ b/pos/static/js/accounting.js @@ -0,0 +1,593 @@ +// /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 += '
#FechaTipoDescripcionReferenciaMontoAuto
${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 += ` + + + + +
CuentaNombreCargoAbonoNota
${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 += ` + + + + + +
CodigoCuentaSaldo InicialCargosAbonosSaldo 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 += ` + + + + + + + +
ClienteCorriente1-30d31-60d61-90d90+dTotal
${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, + }; +})(); diff --git a/pos/templates/accounting.html b/pos/templates/accounting.html new file mode 100644 index 0000000..a75d9b3 --- /dev/null +++ b/pos/templates/accounting.html @@ -0,0 +1,292 @@ + + + + + + Contabilidad - Nexus POS + + + + +
+
+

Contabilidad

+
+ POS + Facturacion +
+
+ +
+
Catalogo de Cuentas
+
Polizas
+
Balanza
+
Estado de Resultados
+
Balance General
+
Antiguedad Saldos
+
Periodos
+
+ + +
+
+

Catalogo de Cuentas

+ +
+
+
+ + +
+
+

Polizas Contables

+ +
+
+ + + + + + +
+
+
+
+ + +
+

Balanza de Comprobacion

+
+ + + + + +
+
+
+ + +
+

Estado de Resultados

+
+ + + + + +
+
+
+ + +
+

Balance General

+
+ + + +
+
+
+ + +
+

Antiguedad de Saldos

+
+
+ + +
+

Periodos Fiscales

+
+
+ + + + +
+
+
+
+
+ + + + + +
+ +
+ + +
+ +
+ + + +