artaza/module-odoo-integration

Magento 2 endpoints for the Odoo connector: push a product tax class and show an order's negotiated total. Companion to the Odoo 19 module Magento 2 Connector.

Maintainers

Package info

github.com/martinartaza/magento_odoo_integration

Homepage

Type:magento2-module

pkg:composer/artaza/module-odoo-integration

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-08-15 02:17 UTC

This package is auto-updated.

Last update: 2026-08-15 02:18:46 UTC


README

Magento 2 endpoints for the Magento 2 Connector for Odoo 19.

Odoo talks to Magento over the native REST API for almost everything it syncs — stock, prices, orders, invoices, shipments, coupons. This module adds the two endpoints Magento Open Source does not provide, so Odoo can finish the loop:

Endpoint What it does
POST /V1/products/tax-class Assigns a product tax class to a batch of SKUs
POST /V1/orders/{orderId}/negotiation Stores an order's negotiated total and the reason, for display

Both are authenticated with the access token of a Magento integration and guarded by their own ACL resources, so you grant exactly these two and nothing else.

How it fits together

The ERP owns the numbers; Magento stores and displays them. Nothing here calls out — this module only ever answers.

flowchart LR
    subgraph ERP["ERP (e.g. Odoo)"]
        E1["tax rate per product"]
        E2["negotiated total"]
    end
    subgraph MAG["Magento 2"]
        direction TB
        A["Artaza_OdooIntegration<br/>2 REST endpoints"]
        P[("catalog_product_entity<br/>tax_class_id")]
        O[("sales_order<br/>adjustment_amount<br/>adjustment_reason<br/>negotiation_total")]
        V["storefront + admin<br/>order totals"]
        A -->|"updateAttributes"| P
        A -->|"saveAttribute"| O
        O --> V
    end
    E1 -->|"POST /V1/products/tax-class<br/>Bearer token"| A
    E2 -->|"POST /V1/orders/{id}/negotiation<br/>Bearer token"| A
Loading

The tax-class endpoint, step by step

The order of the checks is the point: the class is validated before anything is written, and SKUs Magento does not know are reported back instead of failing the whole batch silently.

flowchart TD
    S["POST /V1/products/tax-class<br/>{taxClassId, skus[]}"] --> E{"skus empty?"}
    E -->|yes| X1["InputException<br/>'At least one SKU is required'"]
    E -->|no| L["load tax class by id"]
    L --> N{"class exists?"}
    N -->|no| X2["NoSuchEntityException"]
    N -->|yes| T{"is it a PRODUCT<br/>tax class?"}
    T -->|"no (customer class)"| X3["InputException<br/>— never write a customer class"]
    T -->|yes| C["load product collection<br/>filtered by those SKUs"]
    C --> U["updateAttributes() mass-update<br/>writes ONLY tax_class_id"]
    U --> R["return the SKUs<br/>that were not found"]
Loading

updateAttributes is the native mass-update path: one query for the whole batch, and nothing else about the product is rewritten — no full save, no unrelated reindex.

The negotiation endpoint

sequenceDiagram
    participant ERP
    participant API as Artaza_OdooIntegration
    participant DB as sales_order
    participant Shopper

    ERP->>API: POST /V1/orders/125/negotiation<br/>{adjustmentAmount, adjustmentReason, negotiationTotal}
    API->>DB: saveAttribute() — only those 3 columns
    Note over DB: grand_total, invoices and<br/>balances are NOT recalculated
    Shopper->>DB: opens the order
    DB-->>Shopper: original total struck through<br/>+ agreed total + the reason
Loading

Writing with saveAttribute instead of OrderRepository::save is deliberate: a full save would fire the order's observers and recalculations. The agreed figure is display only — the document that carries fiscal weight is the one the ERP issues.

Install

composer require artaza/module-odoo-integration
bin/magento module:enable Artaza_OdooIntegration
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush

Grant the ACL

System ▸ Extensions ▸ Integrations ▸ your integration ▸ API, then tick:

  • Artaza_OdooIntegration::product_tax — under Catalog
  • Artaza_OdooIntegration::negotiation — under Sales

The tax-class endpoint also needs the native Magento_Tax::manage_tax, because the caller reads taxClasses / taxRules / taxRates to work out which class charges which rate.

⚠️ After adding an ACL resource you must reauthorize the integration, or its calls keep coming back 401 with the old token's permissions.

⚠️ This module changes one store-wide setting

Since Magento 2.4.4, an integration's access token cannot be used as a Bearer token on the REST API unless "Allow OAuth Access Tokens to be used as standalone Bearer tokens" is on — it defaults to off. Without it, every server-to-server call answers "The consumer isn't authorized to access %resources" even when the integration has been granted everything.

Because that is exactly how an ERP authenticates, this module ships etc/config.xml turning the flag on by default:

Stores ▸ Configuration ▸ Services ▸ OAuth ▸ Consumer Settings
  Allow OAuth Access Tokens to be used as standalone Bearer tokens = Yes

Know what that means: it is a store-wide default that re-enables the pre-2.4.4 behaviour for every integration token in the installation, not only this one. It is what makes the module work out of the box, and it is a decision you should make consciously. If your security policy forbids it, delete etc/config.xml from the package and set the value yourself, scoped where you need it.

The endpoints

Product tax class

POST /rest/all/V1/products/tax-class
Authorization: Bearer <integration token>

{ "taxClassId": 4, "skus": ["IPH16-128-BLK", "CASE-MAG-16"] }

Returns the SKUs it did not find, so a partial failure is reported instead of passing as a clean run:

["SKU-THAT-DOES-NOT-EXIST"]

Two deliberate choices:

  • It writes through Product\Action::updateAttributes (Magento's native mass-update), not ProductRepository::save. Only tax_class_id is touched — nothing else about the product is rewritten, and no unrelated indexer is dragged in.
  • It validates that the class exists and is a product tax class, not a customer one. Assigning a customer class would be accepted by the raw model and silently corrupt the catalogue.

Negotiated total

POST /rest/all/V1/orders/125/negotiation
Authorization: Bearer <integration token>

{
  "adjustmentAmount": -2000,
  "adjustmentReason": "Volume discount agreed by phone",
  "negotiationTotal": 95000
}

B2B totals get agreed off-site. The ERP owns that number; this endpoint only records it so the customer can see it on their order. It is written with saveAttribute, so Magento's balances, totals and invoices are never recalculated — the fiscal document is the one the ERP issues. The value is rendered on the storefront order view and in the admin order view.

Also inside

  • Plugin\GuestOrderLastname — makes the guest order lookup tolerant of the surname's spacing and capitalisation, which is a common source of "order not found" for guests.

Notes

  • This module does not talk to Odoo. It exposes endpoints and waits; the connector on the Odoo side is what calls them.
  • It adds no storefront output of its own beyond the negotiated total on the order view, and creates no CMS content: installing it changes nothing a shopper sees.

Requirements

  • Magento 2.4.x (Open Source or Commerce)
  • PHP 8.1+

License

OSL-3.0 — the same license as the Magento core.

Sebastian Artaza · artaza.net · martin.artaza@gmail.com