FactuAPI
Que es FactuAPI
FactuAPI es la API programatica 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 importacion, exportacion y automatizacion
- Plugins que necesitan logica de negocio compleja
Cuando usar cada una
| API REST | FactuAPI |
|---|---|
| CRUD individual sobre una entidad | Transacciones multi-documento |
| Frontend → Backend | Scripts, integraciones, plugins |
| Sin estado entre peticiones | Sesion con conexion persistente |
| HTTP request/response | TypeScript SDK directo |
| Operaciones simples | Logica de negocio compleja |
Instalacion
npm install @openfactu/sdkimport { FactuApi } from '@openfactu/sdk';Sesion y autenticacion
Login completo con session()
El metodo 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()
Si necesitas controlar tu mismo la seleccion 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
// Si ya tienes un token JWTconst db = await FactuApi.getTenantDb('tenant-id');const api = FactuApi.connect('tenant-id', db, { id: 'user-id', role: 'admin' });Sesion de desarrollo con Dev API Key
Para desarrollo remoto de plugins:
const ctx = await FactuApi.devSession( 'tu-dev-api-key', 'Mi Empresa');Cerrar conexiones
await FactuApi.disconnect();Crear documentos
FactuAPI soporta 6 tipos de documento. Cada uno tiene un metodo factory en FactuApiTransaction:
| Metodo | Tipo | Codigo | 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
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
Todos los documentos comparten estos campos:
| Campo | Tipo | Descripcion |
|---|---|---|
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? | Almacen (opcional) |
billToAddress | string? | Direccion de facturacion |
shipToAddress | string? | Direccion de envio |
deliveryDate | Date | string? | Fecha de entrega |
orderId | string? | ID del pedido base |
customFields | Record<string, any> | Campos personalizados |
lines | DiLine[] | Lineas del documento (read-only) |
Lineas 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)
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
El metodo transaction() ejecuta un bloque de codigo dentro de una transaccion de base de datos. Si falla cualquier operacion, se hace rollback automatico 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 transaccion
const api = FactuApi.connect(tenantId, db, user);// Usa api directamente sin transaccion explicitaIDs 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
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
// 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
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();Articulos
// 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();Categorias
const categories = await ctx.api.getCategories();Almacenes
const warehouses = await ctx.api.getWarehouses();Series documentales
// Todas las seriesconst allSeries = await ctx.api.getSeries();
// Series de un tipo de documentoconst invoiceSeries = await ctx.api.getSeries('SINV');Impuestos
const taxGroups = await ctx.api.getTaxGroups();Monedas
const currencies = await ctx.api.getCurrencies();Formas de pago
const paymentMethods = await ctx.api.getPaymentMethods();Condiciones de pago
const paymentTerms = await ctx.api.getPaymentTerms();Tipos de documento
const docTypes = await ctx.api.getDocumentTypes();Helpers de consulta — Contabilidad
Periodos contables
// Periodos abiertosconst openPeriods = await ctx.api.getOpenPeriods();
// Todos los periodosconst periods = await ctx.api.getPeriods();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
const cc = await ctx.api.getCostCenter('CC-001');const pc = await ctx.api.getProfitCenter('PC-001');Ordenes internas (proyectos)
const order = await ctx.api.getInternalOrder('OI-001');Reglas de dimension analitica
const rule = await ctx.api.getDimensionRule('account-id');Pagos
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 automaticamente.
Asientos contables
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
await ctx.api.postJournalEntry('je_001');Revertir un asiento posteado
await ctx.api.reverseJournalEntry('je_001', 'Reversion por error');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
// 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
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
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
await ctx.api.closePeriod('period_2026_05');RRHH — Empleados y Nominas
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
// 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 nomina
// Todosconst concepts = await ctx.api.getPayrollConcepts();
// Solo activosconst activeConcepts = await ctx.api.getPayrollConcepts({ activeOnly: true });Nominas
// 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
// 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',});Turnos
// 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
// 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
const agreements = await ctx.api.getCollectiveAgreements();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
const objectives = await ctx.api.getObjectives();const empObjectives = await ctx.api.getObjectives({ employeeId: 'emp_001' });const activeObjectives = await ctx.api.getObjectives({ status: 'active' });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
Tareas
// 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' });Gantt
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)
Los documentos soportan campos personalizados a nivel de cabecera y linea:
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
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
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 automatico:
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
Ejemplo 1: Importar clientes desde CSV
import { FactuApi } from '@openfactu/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 transaccion atomica
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)
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: Facturacion 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 nominas 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)
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
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 automatico
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 rapida de la clase FactuApi
Metodos estaticos de la clase FactuApi:
| Metodo | Parametros | Retorno | Descripcion |
|---|---|---|---|
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 transaccion atomica |
connect(tenantId, db, user) | string, any, any | FactuApiTransaction | Conexion sin transaccion 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 rapida de FactuApiTransaction
Metodos disponibles en ctx.api (instancia de FactuApiTransaction):
Factory de documentos
salesInvoice()→SalesInvoicepurchaseInvoice()→PurchaseInvoicesalesOrder()→SalesOrderpurchaseOrder()→PurchaseOrdersalesDeliveryNote()→SalesDeliveryNotepurchaseDeliveryNote()→PurchaseDeliveryNotecreate(docType)→DiDocumentjournalEntry()→JournalEntry
Persistencia
save(doc)→Promise<{ id: string; docNum: number }>
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
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>
Pagos
registerPayment(opts)→Promise<{ id: string; journalEntryId: string \| null }>
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
previewPeriodClose(periodId)→Promise<PreviewResult>closePeriod(periodId)→Promise<void>
RRHH
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
getTasks(opts?)→Promise<Task[]>createTask(task)→Promise<Task>updateTask(id, patch)→Promise<Task>getGantt(opts?)→Promise<{ tasks; dependencies }>