Mohamed ARKID logo

Command Palette

Search for a command to run...

Command Palette

Search for a command to run...

Blog

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.

Updated 7 min read

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:

  1. An engineer logs into the AWS Billing Console.
  2. They export a CSV of costs grouped by service.
  3. They email the CSV to the finance team.
  4. A bookkeeper manually enters the numbers into the ERP system (Odoo, QuickBooks, NetSuite).
  5. 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  │
  └─────────────────┘      └─────────────────────┘
StageWhat HappensTool
ExtractPull daily cost data from AWSAWS Cost Explorer API
TransformMap AWS services to Odoo analytic accountsPython script
LoadCreate journal entries in OdooOdoo XML-RPC API
ScheduleRun automatically every dayn8n / AWS Lambda
AlertNotify finance of anomaliesSlack / 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 costs

Step 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]]],
        ) > 0

Two 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>) with entry_exists before 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_id as 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.

Frequently Asked Questions

How much does the AWS Cost Explorer API cost?
Each GetCostAndUsage request costs $0.01. Running the sync once per day costs roughly $0.30 per month, which is negligible compared to the engineering hours spent on manual reconciliation.
Does this work with Odoo Community Edition?
Yes. The XML-RPC API is available in both Odoo Community and Enterprise editions. The only requirements are that the Accounting module is installed and your API user has write access to account.move.
How do I handle multi-currency billing (USD to EUR/MAD)?
AWS bills in USD by default. If your Odoo instance uses a different base currency, add a currency conversion step before creating the journal entry, using an exchange rate source such as the European Central Bank, or configure AWS to bill in your local currency in the Billing console.
Can I adapt this for GCP or Azure instead of AWS?
Yes. Replace the Cost Explorer call with the GCP BigQuery billing export or the Azure Cost Management API. The Odoo loading step stays the same; only the extraction source changes.

AWS Cost Audit · $250 fixed

AWS bill climbing and nobody knows why?

I go through your AWS bill and account (Cost Explorer or your Cost & Usage Report, VPC setup, compute and storage) and find where the money leaks.

  • Written findings, ranked by estimated monthly savings
  • Terraform patches for the top fixes, ready to review and apply
  • Notes on risk and rollout order for each change
  • Delivered within 48 hours

Not ready for a call? Estimate your waste in a minute with the free AWS cost waste calculator.

Mohamed ARKID
Mohamed ARKID

DevOps & Cloud Consultant

I build systems that run reliably, scale efficiently, and deploy intelligently. See how I can help your team.