WooCommerce: de la comanda pagada a la factura emesa
Un ganxo, sis crides i una capçalera que impedeix el duplicat. En PHP, que és on viu WooCommerce, i amb les tres decisions espanyoles que la botiga no porta de fàbrica.
L'API està en marxa. Això és el que hi ha avui i el que no
Funciona. Les rutes d'aquesta documentació estan desplegades i responent a https://erp.cairos.es/api/v1/. Els exemples d'aquestes pàgines es poden copiar i executar. L'especificació completa és a openapi.json, que és el que importen Make i n8n.
Les claus te les crees tu, des de Desenvolupadors al teu compte de Cairos. Comença amb una cai_test_: treballa contra les teves dades de veritat però no registra a VeriFactu ni envia correus, així que pots muntar la teva integració sense embrutar una sèrie de facturació.
I el que encara NO hi ha, dit sense adorns: cap connector oficial. Ni app de Shopify, ni plugin de WooCommerce, ni mòdul publicat a Make o n8n. Amb l'API i la seva especificació es poden construir —per això hi ha les guies d'aquesta secció— però construir-los és feina, i aquesta feina no està feta.
Si muntes alguna cosa amb això, escriu-nos a hola@cairos.es. Ens interessa especialment el que et falti del contracte: és el que decideix què s'amplia primer.
Ja no cal programar això
Cairos connecta amb WooCommerce des de dins del programa: encens el mòdul de botigues en línia, enganxes les credencials que et dóna la teva botiga i cada comanda es converteix en factura amb la teva sèrie, el teu IVA i la teva numeració. No cal instal·lar cap plugin a WordPress: la connexió es fa des de Cairos amb una clau de l'API REST de WooCommerce, que es crea en dues pantalles.
Aquesta guia es queda per a qui prefereixi muntar-s'ho a la seva manera, tingui un flux que no encaixi, o vulgui saber què fa el connector per dins abans de fiar-se'n.
El que fa la integració, en tres moviments
L'ordre importa: és el que evita que un tall de xarxa acabi en dues factures.
La comanda es paga
WooCommerce dispara woocommerce_payment_complete. Aquí, i no abans, hi ha alguna cosa per facturar.
Es comprova i es crida
Si la comanda ja té factura desada, no es fa res. Si no, es crida amb Idempotency-Key: wc-1042.
Es desa abans de continuar
L'identificador de la factura s'escriu a la comanda abans d'emetre. Així una errada posterior no esborra la pista.
El primer: decidir quan es factura
És la decisió que més conseqüències té i la que gairebé ningú no pensa. WooCommerce mou una comanda per diversos estats, i només un d'ells és el bo per emetre una factura.
| Ganxo de WooCommerce | Facturar aquí? | Per què |
|---|---|---|
woocommerce_new_order | No | La comanda existeix però pot no arribar a pagar-se mai. Facturaries carretons abandonats amb la passarel·la oberta. |
woocommerce_payment_complete | Sí | Els diners estan cobrats. És el moment natural: hi ha fet imposable i hi ha cobrament per registrar. |
woocommerce_order_status_completed | També serveix | Si véns producte físic i prefereixes facturar en enviar. Tria'n un dels dos, no tots dos. |
Si enganxes els dos, una comanda passarà per tots dos i cridaràs dues vegades. Amb Idempotency-Key derivada de la comanda no passa res; sense ella, tens dues factures. Aquesta és tota la diferència.
El NIF, que WooCommerce no et demana
Una botiga de WooCommerce acabada d'instal·lar pregunta nom, adreça i correu. No pregunta el NIF, perquè el formulari de facturació és l'estàndard internacional i no sap res d'Espanya.
I sense NIF no hi ha factura completa: hi ha factura simplificada, que té topall d'import i que el teu client empresa no es pot deduir. Així que abans d'escriure una línia d'integració cal afegir aquest camp al procés de compra, amb qualsevol dels complements de facturació espanyola que hi ha, o a mà amb el ganxo de camps de facturació de WooCommerce.
Després, al codi de sota, aquest camp és el que viatja a nif del contacte. Si arriba buit, decideix què fas: crear el contacte sense NIF i emetre simplificada, o parar i avisar. El que no s'ha de fer és inventar-se un NIF de farciment, que és el que acaba desquadrant el 347 al febrer.
La connexió, sencera
Això va en un plugin propi —un fitxer a wp-content/plugins/—, no al functions.php del tema: el dia que canviïs de tema no vols perdre la facturació.
<?php
/**
* Plugin Name: Cairos para WooCommerce
*/
define("CAIROS_BASE", "https://erp.cairos.es/api/v1");
function cairos_llamar($ruta, $cuerpo = null, $llave = null) {
$cabeceras = [
"Authorization" => "Bearer " . getenv("CAIROS_CLAVE"),
"Content-Type" => "application/json",
];
if ($llave) {
$cabeceras["Idempotency-Key"] = $llave;
}
$r = wp_remote_post(CAIROS_BASE . $ruta, [
"headers" => $cabeceras,
"body" => $cuerpo ? wp_json_encode($cuerpo) : null,
"timeout" => 20,
]);
if (is_wp_error($r)) {
throw new Exception($r->get_error_message());
}
$estado = wp_remote_retrieve_response_code($r);
$datos = json_decode(wp_remote_retrieve_body($r), true);
if ($estado >= 400) {
$codigo = $datos["error"]["codigo"] ?? "desconocido";
throw new Exception("Cairos " . $estado . ": " . $codigo);
}
return $datos;
}
add_action("woocommerce_payment_complete", "cairos_facturar_pedido");
function cairos_facturar_pedido($id_pedido) {
$pedido = wc_get_order($id_pedido);
// 1 · Si ya lo facturamos, no hay nada que hacer. Esta línea
// vale más que todo el resto del fichero.
if ($pedido->get_meta("_cairos_factura_id")) {
return;
}
$llave = "wc-" . $id_pedido;
try {
// 2 · Contacto. El NIF sale del campo que añadiste al
// proceso de compra.
$contacto = cairos_llamar("/contactos/upsert", [
"nombre" => trim($pedido->get_billing_company()
?: $pedido->get_formatted_billing_full_name()),
"nif" => $pedido->get_meta("_billing_nif"),
"email" => $pedido->get_billing_email(),
// "customer" o "supplier": el enumerado esta en
// ingles y un "cliente" devuelve 422.
"tipo" => "customer",
"buscar_por" => "email",
], $llave . "-contacto");
// 3 · Líneas. get_subtotal() es SIN impuestos, que es lo
// que espera la API por defecto. El campo del texto
// es "descripcion" y el del impuesto "tipo_iva":
// "concepto" e "iva" no existen y dan 422.
$lineas = [];
foreach ($pedido->get_items() as $item) {
$cantidad = $item->get_quantity();
$lineas[] = [
"descripcion" => $item->get_name(),
"cantidad" => $cantidad,
"precio" => round($item->get_subtotal() / max(1, $cantidad), 2),
"tipo_iva" => 21,
];
}
// 4 · Borrador. La fecha es "fecha_emision".
$factura = cairos_llamar("/facturas", [
"contacto_id" => $contacto["id"],
"serie" => "WEB",
"fecha_emision" => $pedido->get_date_paid()->date("Y-m-d"),
"lineas" => $lineas,
], $llave);
// 5 · Guardamos ANTES de emitir. Si algo falla después,
// al menos sabemos que este pedido ya tiene factura.
$pedido->update_meta_data("_cairos_factura_id", $factura["id"]);
$pedido->save();
// 6 · Emitir: aquí es donde toma número de serie.
$emitida = cairos_llamar(
"/facturas/" . $factura["id"] . "/emitir",
null,
$llave . "-emitir"
);
// 7 · Y el cobro, que en una tienda ya ha ocurrido.
// "metodo" es un enumerado cerrado: transfer, cash,
// card, direct_debit u other. El nombre bonito de la
// pasarela va en "notas", que es texto libre.
cairos_llamar("/cobros", [
"factura_id" => $factura["id"],
"fecha" => $pedido->get_date_paid()->date("Y-m-d"),
"importe" => (float) $pedido->get_total(),
"metodo" => "card",
"notas" => $pedido->get_payment_method_title(),
], $llave . "-cobro");
// "referencia" es lo que sale impreso ("WEB-0042").
// "numero" es un entero y por si solo no dice la serie.
$pedido->update_meta_data("_cairos_factura_ref", $emitida["referencia"]);
$pedido->add_order_note("Factura " . $emitida["referencia"]);
$pedido->save();
} catch (Exception $e) {
// Ni una excepción sin registrar: un fallo silencioso aquí
// se descubre en el cierre del trimestre.
$pedido->add_order_note("Cairos: " . $e->getMessage());
error_log("Cairos: " . $e->getMessage());
}
}Els cinc detalls que no es veuen al codi
- La clau, en una variable d'entorn. Al codi de dalt es llegeix amb
getenv. Si la poses al fitxer del plugin, viatja al teu repositori i a cada còpia de seguretat. I per descomptat no va a la base de dades de WordPress amb un camp d'opcions visible. - El
tipo_iva, de la línia i no fix. L'exemple hi posa21perquè es llegeixi. Si véns llibres, alimentació o serveis amb tipus diferents, treu el tipus dels impostos de cada línia de la comanda; posar 21 a tot és un error comptable, no de programació. I si la teva empresa és a Canàries, el tipus que toca és el de l'IGIC:GET /organizacionet diu en quin territori ets, aregion_fiscal. - Si la teva botiga treballa amb preus amb l'IVA a dins, no el desglossis tu. Envia els preus de l'etiqueta i afegeix
"precios_con_iva" => trueal cos de la factura: Cairos treu la base cap enrere en cèntims sencers, amb el mateix motor que fa servir el connector de botigues. El desglossament escrit a mà es fa gairebé sempre cap endavant, i aquest cèntim de menys s'acumula factura a factura fins que el 303 no quadra amb la caixa. - Les despeses d'enviament també facturen. No són a
get_items(): cal afegir-les com una línia més, amb el seu tipus d'IVA, que a Espanya és el del bé principal. - Les devolucions no es resolen aquí. Un reemborsament a WooCommerce no esborra una factura emesa: cal una rectificativa. Avui això es fa des de Cairos, a mà. És honest dir-ho abans que algú munti la botiga comptant que s'arregla sol.
Com provar-ho sense embrutar la teva facturació
Amb la clau cai_test_, que treballa sobre les teves mateixes dades però no registra a VeriFactu ni envia correus. I amb una sèrie de facturació a part —PROVES, per exemple— perquè els números de les teves proves no es barregin amb els de la teva sèrie de debò.
Després, una comanda real d'un euro amb la teva pròpia targeta i comprovar tres coses: que apareix la factura, que el número està desat a la comanda, i que repetir el ganxo no en crea una segona. Aquesta tercera és la que s'ha de provar de debò, i es prova cridant dues vegades la funció a mà.
Preguntes sobre WooCommerce
woocommerce_payment_complete, o en completar-la si véns producte físic. Mai en crear la comanda: un carretó amb la passarel·la oberta pot no pagar-se mai.Idempotency-Key: wc-1042. La primera evita el reprocés; la segona, el tall de xarxa. Està explicat a idempotència.Aquest codi funciona avui: crea la clau i prova'l
Està escrit contra els noms de camp que el servidor accepta de debò. Amb una clau de proves i una sèrie a part pots llançar-lo vint vegades sense embrutar res.
PHP sense dependències · Idempotency-Key · Sèrie de proves a part