PHPackages                             artaza/module-odoo-integration - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. artaza/module-odoo-integration

ActiveMagento2-module

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.

00PHP

Since Aug 14Pushed todayCompare

[ Source](https://github.com/martinartaza/magento_odoo_integration)[ Packagist](https://packagist.org/packages/artaza/module-odoo-integration)[ RSS](/packages/artaza-module-odoo-integration/feed)WikiDiscussions main Synced today

READMEChangelogDependenciesVersions (1)Used By (0)

Artaza\_OdooIntegration
=======================

[](#artaza_odoointegration)

Magento 2 endpoints for the **[Magento 2 Connector for Odoo 19](https://www.artaza.net)**.

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:

EndpointWhat 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 displayBoth 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
--------------------

[](#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_OdooIntegration2 REST endpoints"]
        P[("catalog_product_entitytax_class_id")]
        O[("sales_orderadjustment_amountadjustment_reasonnegotiation_total")]
        V["storefront + adminorder totals"]
        A -->|"updateAttributes"| P
        A -->|"saveAttribute"| O
        O --> V
    end
    E1 -->|"POST /V1/products/tax-classBearer token"| A
    E2 -->|"POST /V1/orders/{id}/negotiationBearer token"| A
```

      Loading ### The tax-class endpoint, step by step

[](#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{taxClassId, skus[]}"] --> E{"skus empty?"}
    E -->|yes| X1["InputException'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 PRODUCTtax class?"}
    T -->|"no (customer class)"| X3["InputException— never write a customer class"]
    T -->|yes| C["load product collectionfiltered by those SKUs"]
    C --> U["updateAttributes() mass-updatewrites ONLY tax_class_id"]
    U --> R["return the SKUsthat 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

[](#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{adjustmentAmount, adjustmentReason, negotiationTotal}
    API->>DB: saveAttribute() — only those 3 columns
    Note over DB: grand_total, invoices andbalances are NOT recalculated
    Shopper->>DB: opens the order
    DB-->>Shopper: original total struck through+ 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
-------

[](#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
-------------

[](#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

[](#️-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
-------------

[](#the-endpoints)

### Product tax class

[](#product-tax-class)

```
POST /rest/all/V1/products/tax-class
Authorization: Bearer

{ "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

[](#negotiated-total)

```
POST /rest/all/V1/orders/125/negotiation
Authorization: Bearer

{
  "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
-----------

[](#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
-----

[](#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
------------

[](#requirements)

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

License
-------

[](#license)

[OSL-3.0](LICENSE.txt) — the same license as the Magento core.

Sebastian Artaza · [artaza.net](https://www.artaza.net) ·

###  Health Score

20

—

LowBetter than 12% of packages

Maintenance65

Regular maintenance activity

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/7785503?v=4)[Sebastián Martín, Artaza Saade](/maintainers/martinartaza)[@martinartaza](https://github.com/martinartaza)

### Embed Badge

![Health badge](/badges/artaza-module-odoo-integration/health.svg)

```
[![Health](https://phpackages.com/badges/artaza-module-odoo-integration/health.svg)](https://phpackages.com/packages/artaza-module-odoo-integration)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
