Sync AWS Bills to Odoo ERP: Python Integration Guide
Build a custom Python pipeline to sync AWS Cost Explorer billing data into Odoo journal entries automatically. Complete step-by-step code included.
On this page
TL;DR
Your CFO should never need to ask engineering "what did we spend on AWS last month?"
- The Stack: AWS Cost Explorer API (cost retrieval), Python (transformation), Odoo XML-RPC API (accounting entries), n8n or Lambda (scheduling).
- The Verdict: A single integration pipeline eliminates manual spreadsheet reconciliation, gives finance real-time cloud cost visibility, and satisfies auditors who demand traceable IT expenditure records.
Numbers in the books are step one; smaller numbers are step two. Once finance can see the AWS bill, the next question is why it is that big. The free 15-minute AWS bill review answers that with the top 3 fixes.
The Spreadsheet Problem
Every month, the same ritual plays out in a lot of companies:
- An engineer logs into the AWS Billing Console.
- They export a CSV of costs grouped by service.
- They email the CSV to the finance team.
- A bookkeeper manually enters the numbers into the ERP system (Odoo, QuickBooks, NetSuite).
- The CFO reviews the numbers two weeks later — long after the money is gone.
This process is slow, error-prone, and completely unauditable. If an auditor asks "can you prove that the $14,000 EC2 charge in March matches a specific cost center?", the answer is usually a shrug and a spreadsheet with manual formulas.
The Architecture
Instead of manual exports, we build an automated pipeline:
AWS Cost Explorer API Python Script Odoo XML-RPC API
┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ EXTRACT │ │ TRANSFORM │ │ LOAD │
│ │─────▶│ │─────▶│ │
│ Pull daily cost │ │ Map AWS services │ │ Create journal │
│ data by service │ │ to Odoo analytic │ │ entries with │
│ │ │ account codes │ │ debit/credit │
└─────────────────┘ └─────────────────────┘ └──────────────────┘
│
n8n / Lambda Slack / Email │
┌─────────────────┐ ┌─────────────────────┐ │
│ SCHEDULE │ │ ALERT │◀──────────────┘
│ │ │ │
│ Run daily at │ │ Notify finance of │
│ 6 AM via cron │ │ anomalies / totals │
└─────────────────┘ └─────────────────────┘
| Stage | What Happens | Tool |
|---|---|---|
| Extract | Pull daily cost data from AWS | AWS Cost Explorer API |
| Transform | Map AWS services to Odoo analytic accounts | Python script |
| Load | Create journal entries in Odoo | Odoo XML-RPC API |
| Schedule | Run automatically every day | n8n / AWS Lambda |
| Alert | Notify finance of anomalies | Slack / Email |
Step 1: Pull AWS Costs with the Cost Explorer API
The AWS Cost Explorer API gives you programmatic access to your spending data (for line-item detail, parse the Cost & Usage Report instead). Here is how to pull daily costs grouped by service:
aws_costs.py
import boto3
from datetime import datetime, timedelta
def get_daily_costs(days: int = 1) -> list[dict]:
"""Fetch AWS costs for the last N days, grouped by service."""
client = boto3.client("ce", region_name="us-east-1")
end = datetime.now().strftime("%Y-%m-%d")
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
response = client.get_cost_and_usage(
TimePeriod={"Start": start, "End": end},
Granularity="DAILY",
Metrics=["UnblendedCost"],
GroupBy=[
{"Type": "DIMENSION", "Key": "SERVICE"},
],
)
costs = []
for result in response["ResultsByTime"]:
date = result["TimePeriod"]["Start"]
for group in result["Groups"]:
service = group["Keys"][0]
amount = float(
group["Metrics"]["UnblendedCost"]["Amount"]
)
if amount > 0.01: # Skip negligible charges
costs.append(
{
"date": date,
"service": service,
"amount": round(amount, 2),
"currency": "USD",
}
)
return costsStep 2: Map AWS Services to Odoo Analytic Accounts
The critical piece of this integration is the mapping table. Every AWS service must correspond to an Odoo analytic account so costs are categorized correctly for financial reporting:
account_mapping.py
# Map AWS service names to Odoo analytic account codes
AWS_TO_ODOO_MAP: dict[str, dict] = {
"Amazon Elastic Compute Cloud - Compute": {
"account_code": "6100", # IT Infrastructure - Compute
"analytic_account": "AWS-EC2",
"description": "EC2 compute instances",
},
"Amazon Relational Database Service": {
"account_code": "6100", # IT Infrastructure - Compute
"analytic_account": "AWS-RDS",
"description": "RDS database instances",
},
"Amazon Simple Storage Service": {
"account_code": "6110", # IT Infrastructure - Storage
"analytic_account": "AWS-S3",
"description": "S3 object storage",
},
"Amazon Elastic Container Service for Kubernetes": {
"account_code": "6100", # IT Infrastructure - Compute
"analytic_account": "AWS-EKS",
"description": "EKS Kubernetes clusters",
},
"AWS Data Transfer": {
"account_code": "6120", # IT Infrastructure - Network
"analytic_account": "AWS-TRANSFER",
"description": "Cross-region and internet data transfer",
},
}
def map_cost_to_odoo(service: str) -> dict:
"""Resolve an AWS service name to Odoo account codes."""
return AWS_TO_ODOO_MAP.get(service, {
"account_code": "6199", # IT - Uncategorized
"analytic_account": "AWS-OTHER",
"description": f"Unmapped: {service}",
})When your auditor asks "where did the $8,000 in compute charges go?", the analytic account trail leads directly from the AWS API response to the Odoo journal entry. No spreadsheets. No guessing.
Step 3: Create Journal Entries in Odoo via XML-RPC
Odoo exposes an XML-RPC API that lets you programmatically create accounting journal entries. Here is how to push AWS costs directly into your books:
odoo_sync.py
import xmlrpc.client
class OdooClient:
def __init__(self, url: str, db: str, user: str, password: str):
self.url = url
self.db = db
self.uid = xmlrpc.client.ServerProxy(
f"{url}/xmlrpc/2/common"
).authenticate(db, user, password, {})
self.models = xmlrpc.client.ServerProxy(
f"{url}/xmlrpc/2/object"
)
self.password = password
def _execute(self, model: str, method: str, *args):
return self.models.execute_kw(
self.db, self.uid, self.password,
model, method, *args,
)
def create_journal_entry(
self,
date: str,
service: str,
amount: float,
account_code: str,
analytic_account: str,
description: str,
):
"""Create a journal entry for an AWS cost line item."""
# Find the expense and payable account IDs
expense_account = self._execute(
"account.account", "search",
[[["code", "=", account_code]]],
)
payable_account = self._execute(
"account.account", "search",
[[["code", "=", "2100"]]], # Accounts Payable
)
if not expense_account or not payable_account:
raise ValueError(
f"Account {account_code} or 2100 not found in Odoo"
)
entry = self._execute(
"account.move", "create",
[{
"journal_id": 2, # Vendor Bills journal
"date": date,
"ref": f"AWS-{service}-{date}",
"line_ids": [
(0, 0, {
"account_id": expense_account[0],
"name": f"AWS {description}",
"debit": amount,
"credit": 0,
}),
(0, 0, {
"account_id": payable_account[0],
"name": f"AWS {description} (Payable)",
"debit": 0,
"credit": amount,
}),
],
}],
)
return entry
def entry_exists(self, ref: str) -> bool:
"""True if a journal entry with this reference is already booked."""
return self._execute(
"account.move", "search_count",
[[["ref", "=", ref]]],
) > 0Two things to adjust for your own Odoo database: journal_id: 2 assumes the Vendor Bills journal has ID 2 (look it up by its code with a search on account.journal instead of hard-coding it), and 2100 is a placeholder for your Accounts Payable account code.
Step 4: The Orchestration Pipeline
The orchestration script connects all three pieces — pull from AWS, map to Odoo accounts, push journal entries:
sync_pipeline.py
import os
from aws_costs import get_daily_costs
from account_mapping import map_cost_to_odoo
from odoo_sync import OdooClient
def run_sync():
# Credentials come from the environment (or AWS Secrets Manager in
# Lambda), never from the source file.
odoo = OdooClient(
url=os.environ["ODOO_URL"],
db=os.environ["ODOO_DB"],
user=os.environ["ODOO_USER"],
password=os.environ["ODOO_API_KEY"],
)
# Re-read the last 3 days: Cost Explorer figures for recent days can
# still be revised, and the ref check below skips anything already booked.
costs = get_daily_costs(days=3)
for item in costs:
ref = f"AWS-{item['service']}-{item['date']}"
if odoo.entry_exists(ref):
continue
mapping = map_cost_to_odoo(item["service"])
odoo.create_journal_entry(
date=item["date"],
service=item["service"],
amount=item["amount"],
account_code=mapping["account_code"],
analytic_account=mapping["analytic_account"],
description=mapping["description"],
)
print(
f"Synced: {item['service']} "
f"${item['amount']} -> {mapping['account_code']}"
)
if __name__ == "__main__":
run_sync()Schedule this via a daily n8n cron workflow or an AWS Lambda triggered by EventBridge at 6 AM every morning. By the time your finance team opens their laptops, yesterday's cloud costs are already in the books.
The Operational Reality
- Currency and tax handling. AWS bills in USD. If your Odoo instance runs in EUR or MAD, you need a currency conversion step. Use the European Central Bank API or AWS's own billing currency settings.
- Duplicate prevention. The pipeline checks the entry reference (
AWS-<service>-<date>) withentry_existsbefore creating anything, so re-running it never double-books. - Late-arriving costs. Cost Explorer figures for the most recent days are estimates and can change as AWS finalizes usage. Re-reading a short window (the pipeline uses 3 days) catches late charges; for month-end close, reconcile against the final invoice.
- Multi-account organizations. If you run AWS Organizations with multiple linked accounts, add the
linked_account_idas a grouping dimension in the Cost Explorer query. Map each AWS account to a separate Odoo cost center. - Odoo permissions. The API user needs access to the Accounting module and write permissions on
account.move. Use a dedicated service account, not a personal login.
The Payoff
This integration eliminates the monthly "AWS bill reconciliation" ritual. Your CFO gets daily, automated, auditable cloud cost entries in the ERP system — categorized by service, mapped to cost centers, and ready for financial reporting.
More importantly, when an auditor asks "can you trace the $14,000 EC2 charge in Q1 to a specific cost center and business unit?", the answer is a database query, not a frantic search through email attachments.
This is one piece of a broader cloud cost optimization strategy. Once your costs are visible in your ERP, the next step is actually reducing them: start with a free 15-minute AWS bill review, or estimate the waste yourself with the AWS cost waste calculator.
Once the costs land in Odoo every morning, nobody has to ask engineering what AWS cost last month. The next question is why it cost that much.