Using Odoo ERP (Python ORM)
To use the CubeMaster API, you need an API key (TokenID) for authentication. Here's how to get started:
TokenID (e.g., abc123xyz789). Copy this key and store it securely, as it will be used in the HTTP headers of your API requests.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.
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.
cubemaster.api_key (This exact name is important, as our code will look for it.)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.
__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',
'views/cubemaster_shipment_views.xml',
],
'installable': True,
'application': True,
}
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 = {
'TokenID': api_key,
'Content-Type': 'application/json'
}
endpoint = "https://api.cubemaster.net/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()
# 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)
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:
self.length = getattr(self.product_id, 'length', 0.0)
self.width = getattr(self.product_id, 'width', 0.0)
self.height = getattr(self.product_id, 'height', 0.0)
self.weight = self.product_id.weight or 0.0
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>
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
__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.
cubemaster_integration folder into your Odoo server's addons directory.load_plan_3d_url field to a fields.Html field and then write an HTML <img> tag to it in your Python code.