Cairos
Facturació
Programa de facturacióPressupostosFactures recurrentsDespeses i proveïdorsCobraments i tresoreria
Comptabilitat i impostos
ComptabilitatModels d'HisendaLlibres registreImmobilitzatIGIC i Canàries
Operacions
Inventari i magatzemsCRMControl horariProjectesSubvencions i ajuts
Compliment
VeriFactuTicketBAIFactura electrònicaTota la normativaSeguretat i dades
Per tipus de negoci
AutònomsPimesAssessories i gestoriesEstrangers a EspanyaStartupsComerços i botigues
Per sector
Hostaleria i restaurantsConstrucció i reformesServeis professionalsComerç electrònicTots els sectors
Per forma jurídica
AssociacionsFundacionsCooperativesClubs esportiusTotes les formes jurídiques
Canviar de programa
ComparativesAlternativa a HoldedMigrar les teves dades
Eines gratis
Plantilla de facturaCalculadora d'IVACalculadora d'IRPFTotes les eines
Aprendre
GuiesGlossariCalendari fiscalBlog
Desenvolupadors
API i documentacióComença en cinc minutsReferència de recursosWebhooks
Ajuda
Centre d'ajudaContacte
Preus
Comença gratis Inicia la sessió
Integració

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.

Codi PHP completSense dependènciesSense facturar dues vegades

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 recorregut

El que fa la integració, en tres moviments

L'ordre importa: és el que evita que un tall de xarxa acabi en dues factures.

1

La comanda es paga

WooCommerce dispara woocommerce_payment_complete. Aquí, i no abans, hi ha alguna cosa per facturar.

2

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.

3

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 WooCommerceFacturar aquí?Per què
woocommerce_new_orderNoLa comanda existeix però pot no arribar a pagar-se mai. Facturaries carretons abandonats amb la passarel·la oberta.
woocommerce_payment_completeEls diners estan cobrats. És el moment natural: hi ha fet imposable i hi ha cobrament per registrar.
woocommerce_order_status_completedTambé serveixSi 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ó.

cairos-woocommerce.php
<?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

  1. 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.
  2. El tipo_iva, de la línia i no fix. L'exemple hi posa 21 perquè 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 /organizacion et diu en quin territori ets, a region_fiscal.
  3. 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" => true al 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.
  4. 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.
  5. 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

No. No existeix al repositori de WordPress ni enlloc més, i no insinuarem que sí. El que hi ha és l'API i aquesta guia amb la connexió escrita.
Quan la comanda està pagada, amb 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.
Amb dues coses alhora: desar l'identificador de la factura a la comanda i comprovar-lo en entrar, i enviar Idempotency-Key: wc-1042. La primera evita el reprocés; la segona, el tall de xarxa. Està explicat a idempotència.
Afegir-lo al procés de compra, amb un complement de facturació espanyola o amb el ganxo de camps de facturació. Sense NIF només pots emetre factura simplificada, que té topall d'import i que el teu client empresa no es dedueix.
Un reemborsament a WooCommerce no esborra la factura: cal una factura rectificativa, i avui s'emet des de Cairos a mà. És la part que encara no està automatitzada.

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

Suport