Integrating Odoo ERP with the CubeMaster API
A step-by-step developer's guide to creating a custom Odoo module for load calculation.
To use the CubeMaster API, you need an API key (TokenID) for authentication. Here's how to get started:
- Visit the CubeMaster website: https://cubemaster.net.
- Locate the "Sign In" option (typically found in the top-right corner).
- Fill out the registration form with your details (e.g., name, email, password, company information).
- After signing up, log in to your account dashboard.
- Navigate to the "Settings" - "Integration" section to generate your API key (TokenID).
- Generate an API key. Once generated, you’ll receive a unique
TokenID
(e.g.,abc123xyz789
). Copy this key and store it securely, as it will be used in the HTTP headers of your API requests. - Copy the TokenID and store it securely.
Note: The TokenID will be used in the HTTP headers of your POST request for authentication.
To make HTTP requests from Odoo, the standard and most robust Python library is requests
. You must ensure it's installed in the Python environment that your Odoo server uses.
How to Install:
Connect to your Odoo server via the command line (SSH) and run:
pip3 install requests
If you are using a virtual environment for Odoo, make sure to activate it first.
The best practice for storing external keys in Odoo is using System Parameters. This keeps credentials out of your code and allows system administrators to manage them.
What You'll Do:
- In your Odoo UI, go to Settings.
- Scroll to the bottom and click "Activate the developer mode".
- A new menu, "Technical", will appear in the Settings menu. Click on it.
- In the Technical menu, find and click on "System Parameters" (under the Parameters section).
- Click the "Create" button.
-
Fill in the form:
- Key:
cubemaster.api_key
(This exact name is important, as our code will look for it.) - Value: Paste the CubeMaster API Key you copied in Step 1.
- Key:
- Click Save.
All custom logic in Odoo lives inside a module. Create the following folder and file structure in your Odoo `addons` directory:
custom_addons/
└── cubemaster_integration/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ └── cubemaster_shipment.py
└── views/
└── cubemaster_shipment_views.xml
Now, let's fill in these files.
1. The Manifest File (__manifest__.py)
This file tells Odoo about your module.
# cubemaster_integration/__manifest__.py
{
'name': 'CubeMaster Integration',
'version': '1.0',
'summary': 'Integrates Odoo with the CubeMaster Load Planning API.',
'author': 'Your Name',
'depends': ['base', 'product'],
'data': [
'security/ir.model.access.csv', # We will add this later
'views/cubemaster_shipment_views.xml',
],
'installable': True,
'application': True,
}
2. The Model File (models/cubemaster_shipment.py)
This is the core logic. It defines the data structure and the method that calls the API.
# cubemaster_integration/models/cubemaster_shipment.py
import json
import logging
import requests
from odoo import models, fields, api
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class CubeMasterShipment(models.Model):
_name = 'cubemaster.shipment'
_description = 'CubeMaster Shipment'
name = fields.Char(string='Title', required=True, copy=False, readonly=True, default=lambda self: 'New')
description = fields.Text(string='Description')
line_ids = fields.One2many('cubemaster.shipment.line', 'shipment_id', string='Cargoes')
# --- Result Fields ---
state = fields.Selection([
('draft', 'Draft'),
('calculated', 'Calculated'),
('error', 'Error'),
], string='Status', default='draft', readonly=True)
response_message = fields.Text(string='Response Message', readonly=True)
calculation_error = fields.Char(string='Calculation Error Type', readonly=True)
volume_utilization = fields.Float(string='Volume Utilization (%)', readonly=True)
weight_loaded = fields.Float(string='Weight Loaded', readonly=True)
load_plan_3d_url = fields.Char(string='3D Diagram URL', readonly=True)
@api.model
def create(self, vals):
if vals.get('name', 'New') == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code('cubemaster.shipment') or 'New'
return super(CubeMasterShipment, self).create(vals)
def action_calculate_load_plan(self):
self.ensure_one()
# 1. Get API Key from Odoo System Parameters
api_key = self.env['ir.config_parameter'].sudo().get_param('cubemaster.api_key')
if not api_key:
raise UserError("CubeMaster API Key is not set in System Parameters. Please contact your administrator.")
# 2. Build Request Payload
cargoes_list = []
for line in self.line_ids:
cargoes_list.append({
"Name": line.product_id.name,
"Length": line.length,
"Width": line.width,
"Height": line.height,
"Weight": line.weight,
"OrientationsAllowed": "OrientationsAll",
"TurnAllowedOnFloor": False,
"Qty": line.quantity,
"ColorKnownName": "Brown"
})
payload = {
"Title": self.name,
"Description": self.description or "Odoo Shipment",
"Cargoes": cargoes_list,
"Containers": [{
"VehicleType": "Dry",
"Name": "53FT-Intermodal",
"Length": 630,
"Width": 98,
"Height": 106,
"ColorKnownName": "Blue"
}],
"Rules": {
"IsWeightLimited": True,
"IsSequenceUsed": False,
"FillDirection": "FrontToRear",
"CalculationType": "MixLoad"
}
}
headers = {
'x-api-key': api_key,
'Content-Type': 'application/json'
}
endpoint = "https://api.cubemaster.net/v1/loads"
# 3. Make the API Call
try:
_logger.info("Sending request to CubeMaster: %s", json.dumps(payload))
response = requests.post(endpoint, headers=headers, data=json.dumps(payload), timeout=30)
response.raise_for_status() # Raises an exception for 4xx/5xx errors
# 4. Process the Response
res_data = response.json()
_logger.info("Received response from CubeMaster: %s", res_data)
if res_data.get('status') == 'succeed' and res_data.get('filledContainers'):
container_summary = res_data['filledContainers'][0]['loadSummary']
graphics = res_data['filledContainers'][0]['graphics']['images']
self.write({
'state': 'calculated',
'response_message': res_data.get('message'),
'calculation_error': res_data.get('calculationError'),
'volume_utilization': container_summary.get('volumeUtilization'),
'weight_loaded': container_summary.get('weightLoaded'),
'load_plan_3d_url': graphics.get('path3DDiagram'),
})
else:
self.write({
'state': 'error',
'response_message': res_data.get('message', 'An unknown error occurred.'),
'calculation_error': res_data.get('calculationError'),
})
except requests.exceptions.RequestException as e:
_logger.error("CubeMaster API call failed: %s", e)
raise UserError(f"Network error while contacting CubeMaster API: {e}")
except Exception as e:
_logger.error("An unexpected error occurred: %s", e)
raise UserError(f"An unexpected error occurred: {e}")
class CubeMasterShipmentLine(models.Model):
_name = 'cubemaster.shipment.line'
_description = 'CubeMaster Shipment Line (Cargo)'
shipment_id = fields.Many2one('cubemaster.shipment', string='Shipment', required=True, ondelete='cascade')
product_id = fields.Many2one('product.product', string='Product', required=True)
quantity = fields.Integer(string='Quantity', default=1, required=True)
# Dimensions can be pulled from the product or entered manually
length = fields.Float(string='Length')
width = fields.Float(string='Width')
height = fields.Float(string='Height')
weight = fields.Float(string='Weight')
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
# Assumes you have length/width/height/weight fields on the product model
# Odoo's standard 'volume' and 'weight' fields are good candidates
self.length = self.product_id.length or 0.0
self.width = self.product_id.width or 0.0
self.height = self.product_id.height or 0.0
self.weight = self.product_id.weight or 0.0
3. The View File (views/cubemaster_shipment_views.xml)
This XML file defines how the model looks in the Odoo user interface.
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Sequence for Shipment Name -->
<record id="seq_cubemaster_shipment" model="ir.sequence">
<field name="name">CubeMaster Shipment Sequence</field>
<field name="code">cubemaster.shipment</field>
<field name="prefix">SHIP/</field>
<field name="padding">5</field>
</record>
<!-- Form View -->
<record id="view_cubemaster_shipment_form" model="ir.ui.view">
<field name="name">cubemaster.shipment.form</field>
<field name="model">cubemaster.shipment</field>
<field name="arch" type="xml">
<form string="Shipment">
<header>
<button name="action_calculate_load_plan" type="object" string="Calculate Load Plan" class="oe_highlight" attrs="{'invisible': [('state', '!=', 'draft')]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,calculated,error"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<group>
<group>
<field name="description"/>
</group>
<group string="Results" attrs="{'invisible': [('state', '==', 'draft')]}">
<field name="volume_utilization"/>
<field name="weight_loaded"/>
<field name="load_plan_3d_url" widget="url"/>
<field name="response_message"/>
<field name="calculation_error"/>
</group>
</group>
<notebook>
<page string="Cargoes">
<field name="line_ids">
<tree editable="bottom">
<field name="product_id"/>
<field name="quantity"/>
<field name="length"/>
<field name="width"/>
<field name="height"/>
<field name="weight"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Tree View -->
<record id="view_cubemaster_shipment_tree" model="ir.ui.view">
<field name="name">cubemaster.shipment.tree</field>
<field name="model">cubemaster.shipment</field>
<field name="arch" type="xml">
<tree string="Shipments">
<field name="name"/>
<field name="description"/>
<field name="state"/>
<field name="volume_utilization"/>
</tree>
</field>
</record>
<!-- Action -->
<record id="action_cubemaster_shipment" model="ir.actions.act_window">
<field name="name">CubeMaster Shipments</field>
<field name="res_model">cubemaster.shipment</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Menu -->
<menuitem id="menu_cubemaster_root" name="CubeMaster" sequence="10"/>
<menuitem id="menu_cubemaster_shipments" name="Shipments" parent="menu_cubemaster_root" action="action_cubemaster_shipment" sequence="10"/>
</data>
</odoo>
4. The Security File (security/ir.model.access.csv)
This CSV file grants users permission to see and use your new models.
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_cubemaster_shipment_user,cubemaster.shipment user,model_cubemaster_shipment,base.group_user,1,1,1,1
access_cubemaster_shipment_line_user,cubemaster.shipment.line user,model_cubemaster_shipment_line,base.group_user,1,1,1,1
5. The Init Files (__init__.py)
These files tell Python to load your code.
# cubemaster_integration/__init__.py
from . import models
# cubemaster_integration/models/__init__.py
from . import cubemaster_shipment
With all the code in place, you can now install and use the module.
Installation and Workflow:
- Place the entire `cubemaster_integration` folder into your Odoo server's `addons` directory.
- Restart the Odoo server service.
- In the Odoo UI, go to the Apps menu.
- Click "Update Apps List" (you may need developer mode for this to be visible).
- Search for "CubeMaster" and click the "Install" button on your new module.
- Once installed, a new top-level menu named "CubeMaster" will appear.
- Click CubeMaster > Shipments and create a new record.
- Add products to the "Cargoes" tab, filling in their dimensions and quantities.
- Click the "Calculate Load Plan" button. The system will contact the API and, after a moment, the result fields will populate!