FactuAPI
Que es FactuAPI
Sección titulada «Que es FactuAPI»FactuAPI es la API programática de Keirost. A diferencia de la API REST (orientada a CRUD), FactuAPI esta pensada para:
- Operaciones transaccionales (todo o nada)
- Crear documentos relacionados en una sola llamada
- Acceso directo a la base de datos del tenant
- Scripts de importación, exportación y automatización
- Plugins que necesitan lógica de negocio compleja
Cuando usar cada una
Sección titulada «Cuando usar cada una»| API REST | FactuAPI |
|---|---|
| CRUD individual sobre una entidad | Transacciones multi-documento |
| Frontend → Backend | Scripts, integraciones, plugins |
| Sin estado entre peticiones | Sesión con conexion persistente |
| HTTP request/response | TypeScript SDK directo |
| Operaciones simples | Lógica de negocio compleja |
Instalación
Sección titulada «Instalación»npm install @openfactu/plugin-sdkimport { FactuApi } from '@openfactu/plugin-sdk';Sesión y autenticación
Sección titulada «Sesión y autenticación»Login completo con session()
Sección titulada «Login completo con session()»El método session() hace login, selecciona tenant y devuelve todo listo para operar:
const ctx = await FactuApi.session( 'admin@keirost.es', // email o username 'password123', // password 'Mi Empresa' // nombre del tenant (opcional, usa el primero));
// ctx contiene:console.log(ctx.token); // JWT para API RESTconsole.log(ctx.tenantId); // ID del tenantconsole.log(ctx.tenantName); // Nombre del tenantconsole.log(ctx.db); // Cliente Drizzle del tenantconsole.log(ctx.api); // FactuApiTransaction lista para usarconsole.log(ctx.user); // { id, email, username, role }Login manual con login()
Sección titulada «Login manual con login()»Si necesitas controlar tu mismo la selección de tenant:
const session = await FactuApi.login('admin@keirost.es', 'password123');
// session contiene:console.log(session.user); // { id, email, username, role }console.log(session.tenants); // Array de tenants accesiblesconsole.log(session.token); // JWT string
// Seleccionar un tenant especificoconst tenant = session.tenants.find(t => t.tenantName === 'Mi Empresa');const db = await FactuApi.getTenantDb(tenant.tenantId);const api = FactuApi.connect(tenant.tenantId, db, session.user);Conectar con token existente
Sección titulada «Conectar con token existente»// Si ya tienes un token JWTconst db = await FactuApi.getTenantDb('tenant-id');const api = FactuApi.connect('tenant-id', db, { id: 'user-id', role: 'admin' });Sesión de desarrollo con Dev API Key
Sección titulada «Sesión de desarrollo con Dev API Key»Para desarrollo remoto de plugins:
const ctx = await FactuApi.devSession( 'tu-dev-api-key', 'Mi Empresa');Cerrar conexiones
Sección titulada «Cerrar conexiones»await FactuApi.disconnect();Crear documentos
Sección titulada «Crear documentos»FactuAPI soporta 6 tipos de documento. Cada uno tiene un método factory en FactuApiTransaction:
| Método | Tipo | Código | Stock | Estado inicial |
|---|---|---|---|---|
salesInvoice() |
Factura de venta | SINV |
Salida (OUT) |
Borrador (D) |
purchaseInvoice() |
Factura de compra | PINV |
Entrada (IN) |
Borrador (D) |
salesOrder() |
Pedido de venta | SO |
Ninguno | Abierto (O) |
purchaseOrder() |
Pedido de compra | PO |
Ninguno | Abierto (O) |
salesDeliveryNote() |
Albaran de venta | SDN |
Salida (OUT) |
Abierto (O) |
purchaseDeliveryNote() |
Albaran de compra | PDN |
Entrada (IN) |
Abierto (O) |
Factura de venta completa
Sección titulada «Factura de venta completa»const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
// 1. Crear documentoconst invoice = ctx.api.salesInvoice();
// 2. Rellenar camposinvoice.partnerId = 'bp_cliente_001';invoice.seriesId = 'series_factura_venta';invoice.periodId = 'period_2026_05';invoice.date = new Date();invoice.warehouseId = 'wh_principal';invoice.billToAddress = 'Calle Mayor 1, Madrid';invoice.shipToAddress = 'Calle Mayor 1, Madrid';
// 3. Anadir lineasinvoice.addLine({ itemId: 'item_producto_a', quantity: 10, price: 99.90, taxGroupId: 'tg_iva21', warehouseId: 'wh_principal',});
invoice.addLine({ itemId: 'item_producto_b', quantity: 5, price: 149.00, taxGroupId: 'tg_iva21', warehouseId: 'wh_principal',});
// 4. Guardarconst result = await ctx.api.save(invoice);
console.log(result.id); // UUID pre-asignadoconsole.log(result.docNum); // Numero de documento asignadoCampos disponibles en documentos
Sección titulada «Campos disponibles en documentos»Todos los documentos comparten estos campos:
| Campo | Tipo | Descripción |
|---|---|---|
id |
string |
UUID pre-asignado (read-only) |
partnerId |
string |
ID del tercero (cliente/proveedor) |
seriesId |
string |
ID de la serie documental |
periodId |
string |
ID del periodo contable |
date |
Date | string |
Fecha del documento |
warehouseId |
string? |
Almacén (opcional) |
billToAddress |
string? |
Dirección de facturación |
shipToAddress |
string? |
Dirección de envío |
deliveryDate |
Date | string? |
Fecha de entrega |
orderId |
string? |
ID del pedido base |
customFields |
Record<string, any> |
Campos personalizados |
lines |
DiLine[] |
Líneas del documento (read-only) |
Líneas de documento
Sección titulada «Líneas de documento»invoice.addLine({ itemId: 'item_001', // ID del articulo quantity: 10, // Cantidad price: 100.00, // Precio unitario taxGroupId: 'tg_iva21', // ID del grupo de impuestos warehouseId: 'wh_001', // Almacen (opcional) zoneId: 'zone_a1', // Zona (opcional) uomId: 'uom_unidad', // Unidad de medida (opcional) uomFactor: 1, // Factor de conversion (opcional) batchDetails: [ // Lotes (opcional) { batchNum: 'LOT001', quantity: 10, expiryDate: new Date('2026-12-31'), zoneId: 'zone_a1' } ], pluginData: { // Datos de plugin (opcional) 'mi-plugin': { customField: 'valor' } },});Crear desde body JSON (REST)
Sección titulada «Crear desde body JSON (REST)»const doc = FactuApi.create('SINV'); // o PINV, SO, PO, SDN, PDNdoc.fromBody({ partnerId: 'bp_001', seriesId: 'series_001', periodId: 'period_001', date: '2026-05-08', warehouseId: 'wh_001', lines: [ { itemId: 'item_001', quantity: 5, price: 100, taxGroupId: 'tg_iva21' } ], customFields: { campo1: 'valor1' }});const result = await doc.save(tenantId, db, user);Transacciones atomicas
Sección titulada «Transacciones atomicas»El método transaction() ejecuta un bloque de código dentro de una transacción de base de datos. Si falla cualquier operación, se hace rollback automático de todo.
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => { // Todas las operaciones aqui son atomicas const invoice = tx.salesInvoice(); invoice.partnerId = 'bp_001'; invoice.addLine({ itemId: 'item_001', quantity: 10, price: 100, taxGroupId: 'tg_iva21' }); await tx.save(invoice);
const payment = await tx.registerPayment({ invoiceId: invoice.id, direction: 'sales', amount: 1210, reference: 'PAGO-001', });});// Si llega aqui, todo se confirmo. Si lanza error, rollback total.Conexion sin transacción
Sección titulada «Conexion sin transacción»const api = FactuApi.connect(tenantId, db, user);// Usa api directamente sin transaccion explicitaIDs pre-asignados
Sección titulada «IDs pre-asignados»Los IDs se generan al crear la instancia del documento, antes de guardar:
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => { // 1. Crear albaran — el ID se genera ANTES de guardar const albaran = tx.salesDeliveryNote(); albaran.partnerId = 'bp_001'; albaran.seriesId = 'series_albaran'; albaran.addLine({ itemId: 'item_001', quantity: 10, price: 100, taxGroupId: 'tg_iva21' });
console.log(albaran.id); // "a1b2c3d4-..." — ya disponible
// 2. Crear factura que referencia el albaran por su ID const factura = tx.salesInvoice(); factura.partnerId = 'bp_001'; factura.seriesId = 'series_factura'; factura.orderId = albaran.id; // <-- vinculacion antes de guardar nada factura.addLine({ itemId: 'item_001', quantity: 10, price: 100, taxGroupId: 'tg_iva21' });
// 3. Guardar ambos juntos: si falla uno, ninguno se persiste await tx.save(albaran); await tx.save(factura);
console.log('Albaran guardado:', albaran.id); console.log('Factura guardada:', factura.id, 'vinculada a', factura.orderId);});Asentar y cancelar documentos
Sección titulada «Asentar y cancelar documentos»Asentar factura (Borrador → Abierto)
Sección titulada «Asentar factura (Borrador → Abierto)»Solo facturas (SINV, PINV) soportan asentamiento:
// Via REST endpointPOST /api/factuapi/documents/SINV/{id}/post
// Via FactuApiTransaction (no disponible directamente, usar endpoint REST o db.update)await db.update(schema.salesInvoices) .set({ status: 'O' }) .where(eq(schema.salesInvoices.id, invoiceId));Cancelar documento
Sección titulada «Cancelar documento»// Via REST endpointPOST /api/factuapi/documents/SINV/{id}/cancelPOST /api/factuapi/documents/PINV/{id}/cancelPOST /api/factuapi/documents/SDN/{id}/cancelPOST /api/factuapi/documents/PDN/{id}/cancelPOST /api/factuapi/documents/SO/{id}/cancelPOST /api/factuapi/documents/PO/{id}/cancelHelpers de consulta — Maestros
Sección titulada «Helpers de consulta — Maestros»Terceros (Business Partners)
Sección titulada «Terceros (Business Partners)»// Buscar por ID o codigoconst partner = await ctx.api.getPartner('bp_001');const partner = await ctx.api.getPartner('CLI-001'); // por codigo
// Listar todosconst partners = await ctx.api.getPartners();Artículos
Sección titulada «Artículos»// Buscar por ID o codigoconst item = await ctx.api.getItem('item_001');const item = await ctx.api.getItem('PROD-A'); // por codigo
// Listar todosconst items = await ctx.api.getItems();Categorías
Sección titulada «Categorías»const categories = await ctx.api.getCategories();Almacenes
Sección titulada «Almacenes»const warehouses = await ctx.api.getWarehouses();Series documentales
Sección titulada «Series documentales»// Todas las seriesconst allSeries = await ctx.api.getSeries();
// Series de un tipo de documentoconst invoiceSeries = await ctx.api.getSeries('SINV');Impuestos
Sección titulada «Impuestos»const taxGroups = await ctx.api.getTaxGroups();Monedas
Sección titulada «Monedas»const currencies = await ctx.api.getCurrencies();Formas de pago
Sección titulada «Formas de pago»const paymentMethods = await ctx.api.getPaymentMethods();Condiciones de pago
Sección titulada «Condiciones de pago»const paymentTerms = await ctx.api.getPaymentTerms();Tipos de documento
Sección titulada «Tipos de documento»const docTypes = await ctx.api.getDocumentTypes();Helpers de consulta — Contabilidad
Sección titulada «Helpers de consulta — Contabilidad»Periodos contables
Sección titulada «Periodos contables»// Periodos abiertosconst openPeriods = await ctx.api.getOpenPeriods();
// Todos los periodosconst periods = await ctx.api.getPeriods();Plan contable
Sección titulada «Plan contable»// Listar todas las cuentasconst accounts = await ctx.api.getChartOfAccounts();
// Buscar cuenta por ID o codigoconst account = await ctx.api.getAccount('43000001');const account = await ctx.api.getAccount('a1b2c3d4'); // por IDCentros de coste y beneficio
Sección titulada «Centros de coste y beneficio»const cc = await ctx.api.getCostCenter('CC-001');const pc = await ctx.api.getProfitCenter('PC-001');Ordenes internas (proyectos)
Sección titulada «Ordenes internas (proyectos)»const order = await ctx.api.getInternalOrder('OI-001');Reglas de dimensión analítica
Sección titulada «Reglas de dimensión analítica»const rule = await ctx.api.getDimensionRule('account-id');Registrar un cobro o pago
Sección titulada «Registrar un cobro o pago»const payment = await ctx.api.registerPayment({ invoiceId: 'inv_001', // ID de la factura direction: 'sales', // 'sales' = cobro, 'purchase' = pago amount: 1210.00, // Importe date: new Date(), // Fecha (opcional, por defecto hoy) reference: 'TRANS-001', // Referencia obligatoria paymentMethodId: 'pm_transfer', // Metodo de pago (opcional) notes: 'Pago cliente XYZ', // Notas (opcional)});
console.log(payment.id); // ID del pagoconsole.log(payment.journalEntryId); // ID del asiento generado (o null)Si la factura esta asentada (status: 'O') y hay mapeos contables configurados, se genera y postea un asiento automáticamente.
Asientos contables
Sección titulada «Asientos contables»Crear asiento manual
Sección titulada «Crear asiento manual»const je = ctx.api.journalEntry();je.date = new Date();je.periodId = 'period_2026_05';je.description = 'Asiento de regularizacion';
je.addLine({ accountId: '43000001', debit: 1210, partnerId: 'bp_001', description: 'Cliente XYZ',});
je.addLine({ accountId: '70000001', credit: 1000, description: 'Ventas',});
je.addLine({ accountId: '47700001', credit: 210, description: 'IVA repercutido',});
const result = await je.save(ctx.db, ctx.user.id);console.log(result.id); // ID del asiento
// Postear el asientoawait ctx.api.postJournalEntry(result.id);Postear un asiento borrador
Sección titulada «Postear un asiento borrador»await ctx.api.postJournalEntry('je_001');Revertir un asiento posteado
Sección titulada «Revertir un asiento posteado»await ctx.api.reverseJournalEntry('je_001', 'Reversion por error');Resolver cuenta por tipo
Sección titulada «Resolver cuenta por tipo»const account = await ctx.api.resolveAccount('customer', 'bp_001');const account = await ctx.api.resolveAccount('sales', null);const account = await ctx.api.resolveAccount('vat_output', 'tg_iva21');Generar asiento desde factura
Sección titulada «Generar asiento desde factura»// Desde factura de ventaconst invoice = await ctx.api.getSalesInvoice('inv_001'); // usar db.selectawait ctx.api.createEntryFromSalesInvoice(invoice, invoiceLines);
// Desde factura de compraawait ctx.api.createEntryFromPurchaseInvoice(invoice, invoiceLines);Generar asiento desde pago
Sección titulada «Generar asiento desde pago»const payment = await ctx.api.registerPayment({ ... });await ctx.api.createEntryFromPayment( payment, { id: invoice.id, partnerId: invoice.partnerId, periodId: invoice.periodId, docNum: invoice.docNum }, 'sales');Cierre de periodo
Sección titulada «Cierre de periodo»Previsualizar cierre
Sección titulada «Previsualizar cierre»const preview = await ctx.api.previewPeriodClose('period_2026_05');console.log(preview.errors); // Errores que impedirian cerrarconsole.log(preview.warnings); // Advertenciasconsole.log(preview.summary); // Resumen de saldosCerrar periodo
Sección titulada «Cerrar periodo»await ctx.api.closePeriod('period_2026_05');RRHH — Empleados y Nóminas
Sección titulada «RRHH — Empleados y Nóminas»Empleados
Sección titulada «Empleados»// Listar todosconst employees = await ctx.api.getEmployees();
// Buscar por ID o codigoconst emp = await ctx.api.getEmployee('emp_001');const emp = await ctx.api.getEmployee('E-001'); // por codigo
// Buscar departamentoconst dept = await ctx.api.getDepartment('dept_001');Contratos
Sección titulada «Contratos»// Todos los contratosconst contracts = await ctx.api.getContracts();
// De un empleado especificoconst empContracts = await ctx.api.getContracts({ employeeId: 'emp_001' });
// Solo activosconst activeContracts = await ctx.api.getContracts({ activeOnly: true });Conceptos de nómina
Sección titulada «Conceptos de nómina»// Todosconst concepts = await ctx.api.getPayrollConcepts();
// Solo activosconst activeConcepts = await ctx.api.getPayrollConcepts({ activeOnly: true });Nóminas
Sección titulada «Nóminas»// Listar con filtrosconst payrolls = await ctx.api.getPayrolls();const empPayrolls = await ctx.api.getPayrolls({ employeeId: 'emp_001' });const mayPayrolls = await ctx.api.getPayrolls({ year: 2026, month: 5 });
// Obtener una nomina con sus lineasconst payroll = await ctx.api.getPayroll('pay_001');console.log(payroll.lines); // Array de lineas
// Anadir linea a nomina existenteawait ctx.api.addPayrollLine('pay_001', { conceptId: 'pc_sal_base', concept: 'Salario Base', type: 'earning', amount: 1500, quantity: 30, rate: 50, baseAmount: 1500, accountId: '64000001',});
// Aprobar nomina y generar asientoawait ctx.api.approvePayroll('pay_001', 'period_2026_05');Fichajes
Sección titulada «Fichajes»// Leer fichajes con filtrosconst entries = await ctx.api.getTimeclockEntries();const empEntries = await ctx.api.getTimeclockEntries({ employeeId: 'emp_001' });const rangeEntries = await ctx.api.getTimeclockEntries({ from: '2026-05-01', to: '2026-05-31', kind: 'in',});
// Registrar fichaje administrativoawait ctx.api.addTimeclockEntry({ employeeId: 'emp_001', kind: 'in', // 'in' | 'out' | 'break_start' | 'break_end' at: new Date(), // opcional, por defecto ahora source: 'admin', // 'web' | 'kiosk' | 'admin' notes: 'Entrada manual',});// Plantillas de turnoconst templates = await ctx.api.getShiftTemplates();
// Asignaciones en un rangoconst assignments = await ctx.api.getShiftAssignments({ employeeId: 'emp_001', from: '2026-05-01', to: '2026-05-31',});Incidencias
Sección titulada «Incidencias»// Tipos de incidenciaconst types = await ctx.api.getIncidentTypes();
// Incidencias con filtrosconst incidents = await ctx.api.getIncidents();const pending = await ctx.api.getIncidents({ status: 'pending' });const empIncidents = await ctx.api.getIncidents({ employeeId: 'emp_001', from: '2026-05-01', to: '2026-05-31',});Convenios colectivos
Sección titulada «Convenios colectivos»const agreements = await ctx.api.getCollectiveAgreements();Evaluaciones de desempeno
Sección titulada «Evaluaciones de desempeno»const evaluations = await ctx.api.getEvaluations();const cycleEvals = await ctx.api.getEvaluations({ cycleId: 'cycle_001' });const empEvals = await ctx.api.getEvaluations({ employeeId: 'emp_001' });Objetivos
Sección titulada «Objetivos»const objectives = await ctx.api.getObjectives();const empObjectives = await ctx.api.getObjectives({ employeeId: 'emp_001' });const activeObjectives = await ctx.api.getObjectives({ status: 'active' });Comisiones
Sección titulada «Comisiones»// Reglas de comisionconst rules = await ctx.api.getCommissionRules();
// Devengos de comision con filtrosconst accruals = await ctx.api.getCommissionAccruals();const empAccruals = await ctx.api.getCommissionAccruals({ employeeId: 'emp_001' });const pendingAccruals = await ctx.api.getCommissionAccruals({ status: 'pending' });const monthlyAccruals = await ctx.api.getCommissionAccruals({ year: 2026, month: 5 });Tareas y Proyectos
Sección titulada «Tareas y Proyectos»// Listar con filtrosconst tasks = await ctx.api.getTasks();const myTasks = await ctx.api.getTasks({ assigneeId: 'emp_001' });const projectTasks = await ctx.api.getTasks({ projectId: 'oi_001' });const todoTasks = await ctx.api.getTasks({ status: 'todo' });
// Crear tareaconst task = await ctx.api.createTask({ title: 'Implementar informe trimestral', description: 'Detalles de la tarea...', status: 'todo', // 'todo' | 'in_progress' | 'done' | 'cancelled' priority: 'high', // 'low' | 'normal' | 'high' | 'urgent' assigneeId: 'emp_001', internalOrderId: 'oi_001', // Proyecto asociado departmentId: 'dept_001', startDate: '2026-05-01', dueDate: '2026-05-15', estimatedHours: 16,});
// Actualizar tareaawait ctx.api.updateTask(task.id, { status: 'in_progress', assigneeId: 'emp_002' });const gantt = await ctx.api.getGantt({ from: '2026-05-01', to: '2026-05-31', projectId: 'oi_001', // opcional});
console.log(gantt.tasks); // Array de tareasconsole.log(gantt.dependencies); // Array de dependenciasCampos personalizados (pluginData)
Sección titulada «Campos personalizados (pluginData)»Los documentos soportan campos personalizados a nivel de cabecera y línea:
const invoice = ctx.api.salesInvoice();invoice.customFields = { projectCode: 'PROJ-001', deliveryInstructions: 'Dejar en recepcion',};
invoice.addLine({ itemId: 'item_001', quantity: 5, price: 100, taxGroupId: 'tg_iva21', pluginData: { 'mi-plugin': { serialNumber: 'SN123456', warrantyMonths: 24, }, },});
await ctx.api.save(invoice);Conexion directa a la base de datos
Sección titulada «Conexion directa a la base de datos»FactuAPI expone el cliente Drizzle para consultas directas cuando necesitas algo que los helpers no cubren:
const db = ctx.db;
// Consulta directa con Drizzleconst result = await db .select() .from(schema.items) .where(eq(schema.items.code, 'PROD-A'));
// Consulta complejaconst sales = await db .select({ month: sql`EXTRACT(MONTH FROM ${schema.salesInvoices.date})`, total: sql`SUM(${schema.salesInvoices.total})`, }) .from(schema.salesInvoices) .where(eq(schema.salesInvoices.status, 'O')) .groupBy(sql`EXTRACT(MONTH FROM ${schema.salesInvoices.date})`);
// Dentro de una transaccionawait FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => { const db = tx.db; // Consultas aqui usan la misma transaccion});Manejo de errores
Sección titulada «Manejo de errores»try { await ctx.api.save(invoice);} catch (error: any) { if (error.message.includes('VALIDATION')) { console.error('Campos invalidos:', error.message); } else if (error.message.includes('stock')) { console.error('Error de stock:', error.message); } else if (error.message.includes('periodo')) { console.error('Error de periodo:', error.message); } else if (error.message.includes('no encontrado')) { console.error('Entidad no existe:', error.message); } else { console.error('Error:', error.message); }}En transacciones, cualquier error hace rollback automático:
try { await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => { await tx.save(invoice); await tx.save(deliveryNote); // Si esto falla, invoice tambien se revierte });} catch (error: any) { console.error('Rollback automatico:', error.message);}Ejemplos completos listos para copiar
Sección titulada «Ejemplos completos listos para copiar»Ejemplo 1: Importar clientes desde CSV
Sección titulada «Ejemplo 1: Importar clientes desde CSV»import { FactuApi } from '@openfactu/plugin-sdk';import { parse } from 'csv-parse/sync';import * as fs from 'fs';
async function importClients() { const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa'); const csv = fs.readFileSync('clientes.csv', 'utf-8'); const records = parse(csv, { columns: true });
for (const row of records) { const id = crypto.randomUUID(); await ctx.db.insert(schema.businessPartners).values({ id, code: row.codigo, name: row.nombre, nif: row.nif, email: row.email, phone: row.telefono, groupId: 'gp_clientes', isCustomer: true, isSupplier: false, status: 'active', }); }
console.log(`Importados ${records.length} clientes`);}
importClients().catch(console.error);Ejemplo 2: Albaran + Factura en transacción atómica
Sección titulada «Ejemplo 2: Albaran + Factura en transacción atómica»async function albaranYFactura() { const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => { // 1. Crear albaran const dn = tx.salesDeliveryNote(); dn.partnerId = 'bp_cliente_001'; dn.seriesId = 'series_albaran'; dn.periodId = 'period_2026_05'; dn.addLine({ itemId: 'item_producto_a', quantity: 10, price: 99.90, taxGroupId: 'tg_iva21', warehouseId: 'wh_principal', }); await tx.save(dn);
// 2. Crear factura desde albaran const inv = tx.salesInvoice(); inv.partnerId = 'bp_cliente_001'; inv.seriesId = 'series_factura'; inv.periodId = 'period_2026_05'; inv.addLine({ itemId: 'item_producto_a', quantity: 10, price: 99.90, taxGroupId: 'tg_iva21', warehouseId: 'wh_principal', }); await tx.save(inv);
console.log('Albaran:', dn.id, dn.docNum); console.log('Factura:', inv.id, inv.docNum); });}Ejemplo 3: Pedido → Albaran → Factura (pipeline completo)
Sección titulada «Ejemplo 3: Pedido → Albaran → Factura (pipeline completo)»async function pipelineCompleto() { const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => { // 1. Pedido de venta const order = tx.salesOrder(); order.partnerId = 'bp_cliente_001'; order.seriesId = 'series_pedido'; order.periodId = 'period_2026_05'; order.addLine({ itemId: 'item_a', quantity: 10, price: 100, taxGroupId: 'tg_iva21' }); await tx.save(order);
// 2. Albaran desde pedido const dn = tx.salesDeliveryNote(); dn.partnerId = 'bp_cliente_001'; dn.seriesId = 'series_albaran'; dn.periodId = 'period_2026_05'; dn.orderId = order.id; dn.addLine({ itemId: 'item_a', quantity: 10, price: 100, taxGroupId: 'tg_iva21' }); await tx.save(dn);
// 3. Factura desde albaran const inv = tx.salesInvoice(); inv.partnerId = 'bp_cliente_001'; inv.seriesId = 'series_factura'; inv.periodId = 'period_2026_05'; inv.addLine({ itemId: 'item_a', quantity: 10, price: 100, taxGroupId: 'tg_iva21' }); await tx.save(inv); });}Ejemplo 4: Facturación masiva mensual desde pedidos
Sección titulada «Ejemplo 4: Facturación masiva mensual desde pedidos»async function facturacionMensual() { const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
// Obtener pedidos abiertos del mes const pedidos = await ctx.db .select() .from(schema.salesOrders) .where( and( eq(schema.salesOrders.status, 'O'), gte(schema.salesOrders.date, '2026-04-01'), lte(schema.salesOrders.date, '2026-04-30'), ) );
for (const pedido of pedidos) { await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => { // Obtener lineas del pedido const lines = await tx.db .select() .from(schema.salesOrderLines) .where(eq(schema.salesOrderLines.orderId, pedido.id));
// Crear albaran const dn = tx.salesDeliveryNote(); dn.partnerId = pedido.partnerId; dn.seriesId = 'series_albaran'; dn.periodId = pedido.periodId; dn.orderId = pedido.id; for (const line of lines) { dn.addLine({ itemId: line.itemId, quantity: line.quantity, price: line.price, taxGroupId: line.taxGroupId, }); } await tx.save(dn);
// Crear factura const inv = tx.salesInvoice(); inv.partnerId = pedido.partnerId; inv.seriesId = 'series_factura'; inv.periodId = pedido.periodId; for (const line of lines) { inv.addLine({ itemId: line.itemId, quantity: line.quantity, price: line.price, taxGroupId: line.taxGroupId, }); } await tx.save(inv);
console.log(`Facturado pedido ${pedido.docNum} → albaran ${dn.docNum} → factura ${inv.docNum}`); }); }}Ejemplo 5: Generar nóminas del mes
Sección titulada «Ejemplo 5: Generar nóminas del mes»async function generarNominas() { const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
const empleados = await ctx.api.getEmployees();
for (const emp of empleados) { if (emp.status !== 'active') continue;
// Obtener contrato activo const contracts = await ctx.api.getContracts({ employeeId: emp.id, activeOnly: true }); const contract = contracts[0]; if (!contract) continue;
const grossSalary = parseFloat(contract.grossSalary);
// Crear nomina const payrollId = crypto.randomUUID(); await ctx.db.insert(schema.payrolls).values({ id: payrollId, employeeId: emp.id, periodYear: 2026, periodMonth: 5, status: 'draft', });
// Linea: salario base await ctx.api.addPayrollLine(payrollId, { concept: 'Salario Base', type: 'earning', amount: grossSalary / 12, });
// Linea: IRPF await ctx.api.addPayrollLine(payrollId, { concept: 'IRPF', type: 'deduction', amount: -(grossSalary / 12 * 0.19), });
// Linea: Seguridad Social (patronal) await ctx.api.addPayrollLine(payrollId, { concept: 'SS Patronal', type: 'employer_cost', amount: grossSalary / 12 * 0.3, });
console.log(`Nomina creada para ${emp.name}: ${payrollId}`); }}Ejemplo 6: Exportar facturas a JSON (previo a Excel)
Sección titulada «Ejemplo 6: Exportar facturas a JSON (previo a Excel)»async function exportarFacturas() { const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
const facturas = await ctx.db .select() .from(schema.salesInvoices) .where( and( gte(schema.salesInvoices.date, '2026-01-01'), lte(schema.salesInvoices.date, '2026-03-31'), eq(schema.salesInvoices.status, 'O'), ) );
const rows = await Promise.all( facturas.map(async (f) => { const [partner] = await ctx.db .select() .from(schema.businessPartners) .where(eq(schema.businessPartners.id, f.partnerId));
return { numero: f.docNum, fecha: f.date, cliente: partner?.name || f.partnerId, total: f.total, estado: f.status, }; }) );
fs.writeFileSync('facturas_q1.json', JSON.stringify(rows, null, 2)); console.log(`Exportadas ${rows.length} facturas`);}Ejemplo 7: Asiento contable manual completo
Sección titulada «Ejemplo 7: Asiento contable manual completo»async function asientoManual() { const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
const je = ctx.api.journalEntry(); je.date = new Date(); je.periodId = 'period_2026_05'; je.description = 'Asiento de regularizacion IVA';
je.addLine({ accountId: '47700001', debit: 2100, description: 'Haber IVA' }); je.addLine({ accountId: '47200001', credit: 1800, description: 'Debe IVA' }); je.addLine({ accountId: '47500001', credit: 300, description: 'A pagar' });
const result = await je.save(ctx.db, ctx.user.id); await ctx.api.postJournalEntry(result.id);
console.log('Asiento posteado:', result.id);}Ejemplo 8: Pipeline de cobro con asiento automático
Sección titulada «Ejemplo 8: Pipeline de cobro con asiento automático»async function cobroConAsiento() { const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
await FactuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (tx) => { // 1. Crear factura const inv = tx.salesInvoice(); inv.partnerId = 'bp_cliente_001'; inv.seriesId = 'series_factura'; inv.periodId = 'period_2026_05'; inv.addLine({ itemId: 'item_001', quantity: 1, price: 1000, taxGroupId: 'tg_iva21' }); const saved = await tx.save(inv);
// 2. Asentar la factura (necesario para que el pago genere asiento) await tx.db.update(schema.salesInvoices) .set({ status: 'O' }) .where(eq(schema.salesInvoices.id, saved.id));
// 3. Registrar cobro const payment = await tx.registerPayment({ invoiceId: saved.id, direction: 'sales', amount: 1210, reference: 'COBRO-001', paymentMethodId: 'pm_transfer', notes: 'Cobro factura prueba', });
console.log('Factura:', saved.id); console.log('Pago:', payment.id); console.log('Asiento:', payment.journalEntryId); });}Referencia rápida de la clase FactuApi
Sección titulada «Referencia rápida de la clase FactuApi»Métodos estaticos de la clase FactuApi:
| Método | Parámetros | Retorno | Descripción |
|---|---|---|---|
create(docType) |
DocType |
DiDocument |
Crea documento por tipo (SINV, PINV, SO, PO, SDN, PDN) |
salesInvoice() |
— | SalesInvoice |
Factory de factura venta |
purchaseInvoice() |
— | PurchaseInvoice |
Factory de factura compra |
salesOrder() |
— | SalesOrder |
Factory de pedido venta |
purchaseOrder() |
— | PurchaseOrder |
Factory de pedido compra |
salesDeliveryNote() |
— | SalesDeliveryNote |
Factory de albaran venta |
purchaseDeliveryNote() |
— | PurchaseDeliveryNote |
Factory de albaran compra |
transaction(tenantId, db, user, fn) |
string, any, any, callback |
Promise<T> |
Ejecuta callback en transacción atómica |
connect(tenantId, db, user) |
string, any, any |
FactuApiTransaction |
Conexion sin transacción explicita |
getTenants() |
— | Promise<Tenant[]> |
Lista todos los tenants |
getTenant(tenantId) |
string |
Promise<Tenant | null> |
Obtener tenant por ID |
getTenantByName(name) |
string |
Promise<Tenant | null> |
Buscar tenant por nombre |
getTenantDb(tenantId) |
string |
Promise<db> |
Cliente Drizzle de un tenant |
login(emailOrUsername, password) |
string, string |
Promise<Session> |
Login y lista de tenants |
session(emailOrUsername, password, tenantName?) |
string, string, string? |
Promise<Context> |
Login + tenant + conexion lista |
disconnect() |
— | Promise<void> |
Cierra todas las conexiones |
Referencia rápida de FactuApiTransaction
Sección titulada «Referencia rápida de FactuApiTransaction»Métodos disponibles en ctx.api (instancia de FactuApiTransaction):
Factory de documentos
Sección titulada «Factory de documentos»salesInvoice()→SalesInvoicepurchaseInvoice()→PurchaseInvoicesalesOrder()→SalesOrderpurchaseOrder()→PurchaseOrdersalesDeliveryNote()→SalesDeliveryNotepurchaseDeliveryNote()→PurchaseDeliveryNotecreate(docType)→DiDocumentjournalEntry()→JournalEntry
Persistencia
Sección titulada «Persistencia»save(doc)→Promise<{ id: string; docNum: number }>
Maestros
Sección titulada «Maestros»getPartner(idOrCode)→Promise<Partner \| null>getPartners()→Promise<Partner[]>getItem(idOrCode)→Promise<Item \| null>getItems()→Promise<Item[]>getCategories()→Promise<Category[]>getWarehouses()→Promise<Warehouse[]>getSeries(docType?)→Promise<Series[]>getTaxGroups()→Promise<TaxGroup[]>getCurrencies()→Promise<Currency[]>getPaymentMethods()→Promise<PaymentMethod[]>getPaymentTerms()→Promise<PaymentTerm[]>getDocumentTypes()→Promise<DocumentType[]>
Contabilidad
Sección titulada «Contabilidad»getOpenPeriods()→Promise<Period[]>getPeriods()→Promise<Period[]>getChartOfAccounts()→Promise<Account[]>getAccount(idOrCode)→Promise<Account \| null>getCostCenter(idOrCode)→Promise<CostCenter \| null>getProfitCenter(idOrCode)→Promise<ProfitCenter \| null>getInternalOrder(idOrCode)→Promise<InternalOrder \| null>getDimensionRule(accountId)→Promise<DimensionRule \| null>
registerPayment(opts)→Promise<{ id: string; journalEntryId: string \| null }>
Asientos contables
Sección titulada «Asientos contables»postJournalEntry(entryId)→Promise<void>reverseJournalEntry(entryId, description?)→Promise<void>resolveAccount(kind, key?)→Promise<Account \| null>createEntryFromSalesInvoice(invoice, lines?)→Promise<JournalEntry \| null>createEntryFromPurchaseInvoice(invoice, lines?)→Promise<JournalEntry \| null>createEntryFromPayment(payment, invoice, direction)→Promise<JournalEntry \| null>
Cierre de periodo
Sección titulada «Cierre de periodo»previewPeriodClose(periodId)→Promise<PreviewResult>closePeriod(periodId)→Promise<void>
getEmployees()→Promise<Employee[]>getEmployee(idOrCode)→Promise<Employee \| null>getDepartment(idOrCode)→Promise<Department \| null>getContracts(opts?)→Promise<Contract[]>getPayrollConcepts(opts?)→Promise<PayrollConcept[]>getPayrolls(opts?)→Promise<Payroll[]>getPayroll(id)→Promise<Payroll & { lines } \| null>addPayrollLine(payrollId, line)→Promise<PayrollLine>approvePayroll(payrollId, periodId?)→Promise<JournalEntry \| null>getTimeclockEntries(opts?)→Promise<TimeclockEntry[]>addTimeclockEntry(entry)→Promise<TimeclockEntry>getShiftAssignments(opts?)→Promise<ShiftAssignment[]>getShiftTemplates()→Promise<ShiftTemplate[]>getIncidentTypes()→Promise<IncidentType[]>getIncidents(opts?)→Promise<Incident[]>getCollectiveAgreements()→Promise<CollectiveAgreement[]>getEvaluations(opts?)→Promise<Evaluation[]>getObjectives(opts?)→Promise<Objective[]>getCommissionRules()→Promise<CommissionRule[]>getCommissionAccruals(opts?)→Promise<CommissionAccrual[]>
Tareas y proyectos
Sección titulada «Tareas y proyectos»getTasks(opts?)→Promise<Task[]>createTask(task)→Promise<Task>updateTask(id, patch)→Promise<Task>getGantt(opts?)→Promise<{ tasks; dependencies }>