// /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 = '';
for (const acct of children) {
const hasChildren = byParent[acct.id] && byParent[acct.id].length > 0;
const balClass = acct.balance < 0 ? 'negative' : '';
html += '';
if (hasChildren) {
html += ``;
} else {
html += '';
}
html += `${acct.code} ${acct.name}`;
html += `$${fmt(acct.balance)} `;
html += ' ';
if (hasChildren) {
html += `${buildUl(acct.id)}
`;
}
html += ' ';
}
html += ' ';
return html;
}
container.innerHTML = buildUl('root');
}
function showNewAccountModal() {
const sel = document.getElementById('na-parent');
sel.innerHTML = '-- Sin padre -- ';
accounts.forEach(a => {
sel.innerHTML += `${a.code} - ${a.name} `;
});
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 = `
# Fecha Tipo Descripcion
Referencia Monto Auto
`;
for (const e of entries) {
html += `
${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'}
`;
}
html += '
';
container.innerHTML = html;
}
async function showEntryDetail(entryId) {
try {
const entry = await api(`/entries/${entryId}`);
let html = `Poliza #${entry.entry_number}
${entry.description || ''}
Cuenta Nombre Cargo
Abono Nota
`;
for (const l of entry.lines) {
html += `
${l.account_code}
${l.account_name}
${l.debit ? '$' + fmt(l.debit) : ''}
${l.credit ? '$' + fmt(l.credit) : ''}
${l.description || ''}
`;
}
html += `
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 = 'Seleccionar ';
accounts.forEach(a => {
if (a.parent_id) { // Only leaf accounts
acctOptions += `${a.code} - ${a.name} `;
}
});
tr.innerHTML = `
${acctOptions}
x `;
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 = `
Codigo Cuenta
Saldo Inicial Cargos
Abonos Saldo Final
`;
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 += `
${r.code} ${r.name}
$${fmt(r.saldo_inicial)}
$${fmt(r.cargos)}
$${fmt(r.abonos)}
$${fmt(r.saldo_final)}
`;
}
html += `
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 += 'INGRESOS ';
for (const item of data.ingresos.items) {
html += `${item.code} - ${item.name} $${fmt(item.amount)} `;
}
html += `Total Ingresos $${fmt(data.ingresos.total)} `;
// Costos
html += 'COSTOS ';
for (const item of data.costos.items) {
html += `${item.code} - ${item.name} $${fmt(item.amount)} `;
}
html += `Total Costos $${fmt(data.costos.total)} `;
// Utilidad bruta
html += `UTILIDAD BRUTA $${fmt(data.utilidad_bruta)} `;
// Gastos
html += 'GASTOS ';
for (const item of data.gastos.items) {
html += `${item.code} - ${item.name} $${fmt(item.amount)} `;
}
html += `Total Gastos $${fmt(data.gastos.total)} `;
// Utilidad neta
const netColor = data.utilidad_neta >= 0 ? 'var(--success)' : 'var(--danger)';
html += `UTILIDAD NETA
$${fmt(data.utilidad_neta)} `;
html += '
';
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 += 'ACTIVO ';
for (const item of data.activo.items) {
html += `${item.code} - ${item.name} $${fmt(item.balance)} `;
}
html += `Total Activo $${fmt(data.activo.total)} `;
// Pasivo
html += 'PASIVO ';
for (const item of data.pasivo.items) {
html += `${item.code} - ${item.name} $${fmt(item.balance)} `;
}
html += `Total Pasivo $${fmt(data.pasivo.total)} `;
// Capital
html += 'CAPITAL ';
for (const item of data.capital.items) {
html += `${item.code} - ${item.name} $${fmt(item.balance)} `;
}
html += `Total Capital $${fmt(data.capital.total)} `;
html += `Pasivo + Capital
$${fmt(data.pasivo.total + data.capital.total)} `;
html += '
';
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 = `
Cliente Corriente
1-30d 31-60d
61-90d 90+d
Total
`;
for (const r of rows) {
html += `
${r.name}
$${fmt(r.corriente)}
$${fmt(r.d1_30)}
$${fmt(r.d31_60)}
$${fmt(r.d61_90)}
$${fmt(r.d90_plus)}
$${fmt(r.total)}
`;
}
const t = data.totals || {};
html += `
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 = `
Periodo Estado Cerrado por Fecha cierre `;
for (const p of periods) {
const badge = p.status === 'closed'
? 'Cerrado '
: 'Abierto ';
html += `
${months[p.month]} ${p.year}
${badge}
${p.closed_by_name || '-'}
${p.closed_at ? new Date(p.closed_at).toLocaleDateString('es-MX') : '-'}
`;
}
html += '
';
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,
};
})();