Saltearse al contenido

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 RESTFactuAPI
CRUD individual sobre una entidadTransacciones multi-documento
Frontend → BackendScripts, integraciones, plugins
Sin estado entre peticionesSesion con conexion persistente
HTTP request/responseTypeScript SDK directo
Operaciones simplesLogica de negocio compleja

Instalacion

Ventana de terminal
npm install @openfactu/sdk
import { 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 REST
console.log(ctx.tenantId); // ID del tenant
console.log(ctx.tenantName); // Nombre del tenant
console.log(ctx.db); // Cliente Drizzle del tenant
console.log(ctx.api); // FactuApiTransaction lista para usar
console.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 accesibles
console.log(session.token); // JWT string
// Seleccionar un tenant especifico
const 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 JWT
const 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:

MetodoTipoCodigoStockEstado inicial
salesInvoice()Factura de ventaSINVSalida (OUT)Borrador (D)
purchaseInvoice()Factura de compraPINVEntrada (IN)Borrador (D)
salesOrder()Pedido de ventaSONingunoAbierto (O)
purchaseOrder()Pedido de compraPONingunoAbierto (O)
salesDeliveryNote()Albaran de ventaSDNSalida (OUT)Abierto (O)
purchaseDeliveryNote()Albaran de compraPDNEntrada (IN)Abierto (O)

Factura de venta completa

const ctx = await FactuApi.session('admin@keirost.es', 'pass', 'Mi Empresa');
// 1. Crear documento
const invoice = ctx.api.salesInvoice();
// 2. Rellenar campos
invoice.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 lineas
invoice.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. Guardar
const result = await ctx.api.save(invoice);
console.log(result.id); // UUID pre-asignado
console.log(result.docNum); // Numero de documento asignado

Campos disponibles en documentos

Todos los documentos comparten estos campos:

CampoTipoDescripcion
idstringUUID pre-asignado (read-only)
partnerIdstringID del tercero (cliente/proveedor)
seriesIdstringID de la serie documental
periodIdstringID del periodo contable
dateDate | stringFecha del documento
warehouseIdstring?Almacen (opcional)
billToAddressstring?Direccion de facturacion
shipToAddressstring?Direccion de envio
deliveryDateDate | string?Fecha de entrega
orderIdstring?ID del pedido base
customFieldsRecord<string, any>Campos personalizados
linesDiLine[]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, PDN
doc.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 explicita

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

Asentar factura (Borrador → Abierto)

Solo facturas (SINV, PINV) soportan asentamiento:

// Via REST endpoint
POST /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 endpoint
POST /api/factuapi/documents/SINV/{id}/cancel
POST /api/factuapi/documents/PINV/{id}/cancel
POST /api/factuapi/documents/SDN/{id}/cancel
POST /api/factuapi/documents/PDN/{id}/cancel
POST /api/factuapi/documents/SO/{id}/cancel
POST /api/factuapi/documents/PO/{id}/cancel

Helpers de consulta — Maestros

Terceros (Business Partners)

// Buscar por ID o codigo
const partner = await ctx.api.getPartner('bp_001');
const partner = await ctx.api.getPartner('CLI-001'); // por codigo
// Listar todos
const partners = await ctx.api.getPartners();

Articulos

// Buscar por ID o codigo
const item = await ctx.api.getItem('item_001');
const item = await ctx.api.getItem('PROD-A'); // por codigo
// Listar todos
const items = await ctx.api.getItems();

Categorias

const categories = await ctx.api.getCategories();

Almacenes

const warehouses = await ctx.api.getWarehouses();

Series documentales

// Todas las series
const allSeries = await ctx.api.getSeries();
// Series de un tipo de documento
const 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 abiertos
const openPeriods = await ctx.api.getOpenPeriods();
// Todos los periodos
const periods = await ctx.api.getPeriods();

Plan contable

// Listar todas las cuentas
const accounts = await ctx.api.getChartOfAccounts();
// Buscar cuenta por ID o codigo
const account = await ctx.api.getAccount('43000001');
const account = await ctx.api.getAccount('a1b2c3d4'); // por ID

Centros 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 pago
console.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 asiento
await 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 venta
const invoice = await ctx.api.getSalesInvoice('inv_001'); // usar db.select
await ctx.api.createEntryFromSalesInvoice(invoice, invoiceLines);
// Desde factura de compra
await 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 cerrar
console.log(preview.warnings); // Advertencias
console.log(preview.summary); // Resumen de saldos

Cerrar periodo

await ctx.api.closePeriod('period_2026_05');

RRHH — Empleados y Nominas

Empleados

// Listar todos
const employees = await ctx.api.getEmployees();
// Buscar por ID o codigo
const emp = await ctx.api.getEmployee('emp_001');
const emp = await ctx.api.getEmployee('E-001'); // por codigo
// Buscar departamento
const dept = await ctx.api.getDepartment('dept_001');

Contratos

// Todos los contratos
const contracts = await ctx.api.getContracts();
// De un empleado especifico
const empContracts = await ctx.api.getContracts({ employeeId: 'emp_001' });
// Solo activos
const activeContracts = await ctx.api.getContracts({ activeOnly: true });

Conceptos de nomina

// Todos
const concepts = await ctx.api.getPayrollConcepts();
// Solo activos
const activeConcepts = await ctx.api.getPayrollConcepts({ activeOnly: true });

Nominas

// Listar con filtros
const 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 lineas
const payroll = await ctx.api.getPayroll('pay_001');
console.log(payroll.lines); // Array de lineas
// Anadir linea a nomina existente
await 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 asiento
await ctx.api.approvePayroll('pay_001', 'period_2026_05');

Fichajes

// Leer fichajes con filtros
const 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 administrativo
await 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 turno
const templates = await ctx.api.getShiftTemplates();
// Asignaciones en un rango
const assignments = await ctx.api.getShiftAssignments({
employeeId: 'emp_001',
from: '2026-05-01',
to: '2026-05-31',
});

Incidencias

// Tipos de incidencia
const types = await ctx.api.getIncidentTypes();
// Incidencias con filtros
const 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 comision
const rules = await ctx.api.getCommissionRules();
// Devengos de comision con filtros
const 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 filtros
const 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 tarea
const 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 tarea
await 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 tareas
console.log(gantt.dependencies); // Array de dependencias

Campos 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 Drizzle
const result = await db
.select()
.from(schema.items)
.where(eq(schema.items.code, 'PROD-A'));
// Consulta compleja
const 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 transaccion
await 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:

MetodoParametrosRetornoDescripcion
create(docType)DocTypeDiDocumentCrea documento por tipo (SINV, PINV, SO, PO, SDN, PDN)
salesInvoice()SalesInvoiceFactory de factura venta
purchaseInvoice()PurchaseInvoiceFactory de factura compra
salesOrder()SalesOrderFactory de pedido venta
purchaseOrder()PurchaseOrderFactory de pedido compra
salesDeliveryNote()SalesDeliveryNoteFactory de albaran venta
purchaseDeliveryNote()PurchaseDeliveryNoteFactory de albaran compra
transaction(tenantId, db, user, fn)string, any, any, callbackPromise<T>Ejecuta callback en transaccion atomica
connect(tenantId, db, user)string, any, anyFactuApiTransactionConexion sin transaccion explicita
getTenants()Promise<Tenant[]>Lista todos los tenants
getTenant(tenantId)stringPromise<Tenant | null>Obtener tenant por ID
getTenantByName(name)stringPromise<Tenant | null>Buscar tenant por nombre
getTenantDb(tenantId)stringPromise<db>Cliente Drizzle de un tenant
login(emailOrUsername, password)string, stringPromise<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()SalesInvoice
  • purchaseInvoice()PurchaseInvoice
  • salesOrder()SalesOrder
  • purchaseOrder()PurchaseOrder
  • salesDeliveryNote()SalesDeliveryNote
  • purchaseDeliveryNote()PurchaseDeliveryNote
  • create(docType)DiDocument
  • journalEntry()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 }>