Odoo Online is a powerful ERP tool, but when it comes to invoicing, some businesses prefer using external invoicing services like Billingo. With a simple integration, Odoo Online users can now generate Billingo invoices directly from Invoice with a single click.
How the Integration Works
This is not a zero-click automation but a one-click integration, which means user need to click to start the process that ensures seamless invoice generation, retrieval, and attachment within Odoo. Here’s what happens when a user triggers the Billingo invoice generation:
- Call to Billingo API: Script sends the necessary data to Billingo, which then generates the invoice.
- Download the PDF: The Billingo-generated invoice is automatically downloaded into Odoo.
- Attach to Odoo Chatter: The invoice is added as an attachment to the respective Odoo invoice’s chatter for reference.
- Download for Local Storage: The PDF is also downloaded to the user’s system for external use.
- Send with Odoo Invoice: When sending the Odoo invoice to a customer, the Billingo-generated invoice can be attached as well.
Key Benefits of This Integration
- Simplicity & Efficiency: A single click is all it takes to generate and retrieve a Billingo invoice within Odoo.
- Automated Attachment: The invoice gets stored in Odoo’s chatter, ensuring all invoice-related documents are in one place.
- Enhanced Customer Experience: Users can attach the Billingo-generated invoice when sending out the Odoo invoice, ensuring consistency and compliance.
- No Complex Configuration Needed: The integration works seamlessly within Odoo Online, requiring no external scripts or any external apps, also doesn't need Odoo.sh
How to Use the Integration
It is recommended to do this once you have generated the Invoice in Odoo, and then before sending the invoice use this feature.
- Open an Invoice in Odoo you can reach to Invoices from many ways use the one best suited in your case, either use Sales → Invoices or Accounting → Invoices or Invoicing → Dashboard → Invoices
- Open an Invoice, and click on the Action Icon(Gear Icon after Invoice number)
- Click the “Create Billingo Invoice” Button (or what ever name you have given to your custom server action added to Odoo).
- Wait for the API Call to Execute – Billingo generates the invoice instantly.
- See the PDF in Chatter – The generated Billingo invoice appears as an attachment.
- Download the File Automatically – The PDF is saved to the user’s system.
- Attach the Billingo Invoice When Sending – When emailing the invoice from Odoo, users can select the Billingo invoice as an attachment.
Technical Implementation
To achieve this, the integration leverages:
- Odoo Server Actions to call the Billingo API
- Odoo’s ir.attachment model to store and attach the invoice
- Base64 Encoding to handle file downloads (since Odoo Online restricts imports)
- Chatter Logging to keep invoice records easily accessible
Implementation Guide
- Login as Administrator on your Odoo online instance
- Enable Debug mode from settings
- Click on Technical Menu go to Actions -> Server Actions
- On Server Actions screen, Create a New Server Action
- Name it as Create Billingo Invoice or anything that is indicates what it does.
In Type select Execute Code
In Technical Settings, in Model Select Journal Entry
In Allowed Groups, select a group that should have access, example Sales / Administrator, you can select multiple groups or leave it blank as well depending upon who all you want to give access to.
Click the Create Contextual Action button
In ACTION DETAILS in Code tab paste the below attached code from Code section - Replace your API key and Invoice block as per your Billingo configuration
Code
billingo_api_url = "https://api.billingo.hu/v3" # Billingo API endpoint
billingo_api_key = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Replace with your actual API key from Billingo
billingo_invoice_block = "xxxxxx" # Invoice Block from Billingo
message_post_body = "Billingo Invoice PDF attached automatically." # Message on the chatter it is also use to check if the Invoice is already created and attached
# Helper function to encode in base64
def custom_base64_encode(binary_data):
"""Converts binary data to Base64 string."""
base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
encoded_str = ""
padding = 0
binary_string = "".join(f"{byte:08b}" for byte in binary_data)
while len(binary_string) % 6 != 0:
binary_string += "0"
padding += 1
for i in range(0, len(binary_string), 6):
index = int(binary_string[i:i+6], 2)
encoded_str += base64_chars[index]
encoded_str += "=" * (padding // 2)
return encoded_str
# Fetch JSON response from Billingo API
def fetch_json_response(url, headers):
"""Fetch and validate JSON response from Billingo API."""
response = requests.get(url, headers=headers)
if response.status_code != 200:
raise Exception(f"API Error ({response.status_code}): {response.text}")
try:
data = response.json()
except ValueError:
raise Exception(f"Invalid JSON response from Billingo: {response.text}")
return data.get("data", data) if isinstance(data, dict) else data
# Check if partner exists and create if not
def create_partner_if_not_exists(record):
"""Check if partner exists, create if not."""
partner_url = f"{billingo_api_url}/partners"
headers = {"X-API-KEY": billingo_api_key, "Content-Type": "application/json"}
if not record.partner_id.street or not record.partner_id.city or not record.partner_id.zip:
raise Exception("Missing required address fields: Street, City, or Zip Code.")
existing_partners = fetch_json_response(partner_url, headers)
for partner in existing_partners:
if partner.get("name") == record.partner_id.name:
return partner.get("id")
# Create new partner
data = {
"name": record.partner_id.name,
"address": {
"country_code": record.partner_id.country_id.code if record.partner_id.country_id else "HU",
"post_code": record.partner_id.zip or "",
"address": record.partner_id.street or "", # street
"city": record.partner_id.city or "", # city
},
"tax_code": record.partner_id.vat or "",
}
response = requests.post(partner_url, headers=headers, json=data)
if response.status_code == 201:
return response.json()["id"]
raise Exception(f"Failed to create partner: {response.text}")
# Check if product exists and create if not
def create_product_if_not_exists(line):
"""Check if product exists, create if not."""
product_url = f"{billingo_api_url}/products"
headers = {"X-API-KEY": billingo_api_key, "Content-Type": "application/json"}
existing_products = fetch_json_response(product_url, headers)
for product in existing_products:
if product["name"] == line.name:
return product["id"]
# Create new product
data = {
"name": line.name,
"unit_price": line.price_unit, # Net unit price
"vat": "27%", # Adjust VAT if needed or pass it from your odoo product configuration
"currency": line.currency_id.name if line.currency_id else "HUF", # Add currency field
"unit": line.product_uom_id.name if line.product_uom_id else "unit",
}
# Decide whether to use net_unit_price or gross_unit_price
if line.price_total != line.price_unit:
# If there's a difference, assume gross price
data["gross_unit_price"] = line.price_total
else:
# If not, use net price
data["net_unit_price"] = line.price_unit
response = requests.post(product_url, headers=headers, json=data)
if response.status_code == 201:
return response.json()["id"]
raise Exception(f"Failed to create product: {response.text}")
# Generate Invoice
def generate_invoice(record):
"""Generates an invoice, ensuring required entities exist first."""
billingo_url = f"{billingo_api_url}/documents"
headers = {"Content-Type": "application/json", "X-API-KEY": billingo_api_key}
partner_id = create_partner_if_not_exists(record)
valid_payment_methods = [
"aruhitel", "bankcard", "barion", "barter", "cash", "cash_on_delivery", "coupon", "ebay",
"elore_utalas", "ep_kartya", "kompenzacio", "levonas", "online_bankcard", "other", "paylike",
"payoneer", "paypal", "paypal_utolag", "payu", "pick_pack_pont", "postai_csekk",
"postautalvany", "revolut", "skrill", "szep_card", "transferwise", "upwork", "utalvany",
"valto", "wire_transfer"
]
items = []
for line in record.invoice_line_ids:
product_id = create_product_if_not_exists(line)
items.append({
"product_id": product_id,
"name": line.name,
"unit_price": line.price_unit,
"vat": f"{int(line.tax_ids[0].amount)}%" if line.tax_ids else "27%",
"quantity": line.quantity,
"unit": line.product_uom_id.name if line.product_uom_id else "unit",
"net_price": line.price_subtotal, # Corrected line reference
"gross_price": line.price_total,
})
data = {
"partner_id": partner_id,
"name": record.name,
"emails": [record.partner_id.email] if record.partner_id.email else [],
"block_id": billingo_invoice_block,
"type": "invoice",
"payment_method": record.l10n_hu_payment_mode if record.l10n_hu_payment_mode in valid_payment_methods else "cash",
"currency": record.currency_id.name,
"conversion_rate": 1,
"electronic": True,
"fulfillment_date": record.invoice_date.strftime('%Y-%m-%d') if record.invoice_date else datetime.today().strftime('%Y-%m-%d'),
"due_date": record.invoice_date_due.strftime('%Y-%m-%d') if record.invoice_date_due else "",
"language": record.partner_id.lang if record.partner_id.lang in ["en", "hu", "de"] else "hu",
"items": items,
"discount": {
"type": "percent" if any(line.discount != 0 for line in record.invoice_line_ids) else None,
"value": int(sum(line.discount for line in record.invoice_line_ids if line.discount != 0) or 0)
}
}
response = requests.post(billingo_url, headers=headers, json=data)
if response.status_code == 201:
result = response.json()
return result['id'], result['invoice_number']
raise Exception(f"Billingo API Error: {response.text}")
# Download Invoice as PDF and attach to Odoo record
def download_invoice(document_id, document_name, record):
"""Downloads and attaches the Billingo invoice PDF to Odoo."""
url = f'{billingo_api_url}/documents/{document_id}/download'
headers = {"accept": "application/json", "X-API-KEY": billingo_api_key}
response = requests.get(url, headers=headers)
if response.status_code == 200:
pdf_base64 = custom_base64_encode(response.content)
attachment = record.env['ir.attachment'].create({
'name': f'{document_name}.pdf',
'type': 'binary',
'datas': pdf_base64,
'res_model': 'account.move',
'res_id': record.id,
'mimetype': 'application/pdf'
})
record.message_post(
body="Billingo Invoice PDF attached automatically.",
message_type="comment",
subtype_xmlid="mail.mt_note",
attachment_ids=[attachment.id]
)
return attachment.id
raise Exception(f"Error downloading Invoice: {response.text}")
def is_invoice_already_created(rec):
existing_message = env['mail.message'].search([
('model', '=', 'account.move'), # Ensure it's related to an Invoice
('res_id', '=', rec.id), # Ensure it's linked to the current invoice
('body', '=', message_post_body) # Message content
], limit=1)
if existing_message:
return True
else:
return False
# Main logic to process records
for rec in records:
if is_invoice_already_created(rec):
raise UserError("Invoice already created. In case you are not able to see it, try refreshing the page.")
generated_id, generated_number = generate_invoice(rec)
time.sleep(2) # sleep the script for 2 sec allowing Billing to generate the PDF Invocie, TODO change this to check if the invoice is generated or not using Billingo API
attachment_id = download_invoice(generated_id, generated_number, rec)
# return action to download the generated invoice
action = {
'type': 'ir.actions.act_url',
'url': f'/web/content/{attachment_id}?download=true',
'target': 'self',
}
Possible Improvements
While this integration provides a streamlined approach, several enhancements can be made:
- Add a Dedicated Button on the Invoice Screen:
- Instead of accessing the action via the gear icon beside the invoice, a dedicated button can be added directly to the Invoice form, improving usability and reducing clicks.
- Customization of Invoice Data Passed to Billingo:
- When sending invoice details to Billingo, various parameters can be adjusted based on configuration settings or invoice-specific data. Some key areas for further customization include:
- VAT Rates: Ensure compliance with regional tax regulations.
- Payment Methods: Automatically map Odoo payment methods to Billingo.
- Fulfillment Date & Due Date: Set based on company policies.
- Currency Conversion: Handle multi-currency transactions accurately.
- Discount Handling: Ensure line-item discounts are correctly applied.
- Grouping of Invoice Line Items: Improve invoice structure for better readability.
- Handling multiple Invoices: possibility to handle multiple selected Invoices from the list view
- When sending invoice details to Billingo, various parameters can be adjusted based on configuration settings or invoice-specific data. Some key areas for further customization include:
There could be more possible improvements needed, above is not an exhaustive list.
Disclaimer
This article provides sample code for integrating Billingo with Odoo Online. The code is provided as-is, and you use it at your own risk. We do not guarantee functionality, security, or compatibility with your specific Odoo setup. You might need to adjust parts of the code to match your Odoo configuration and company processes. Always test the implementation in a safe environment before deploying it to production.
It is highly recommended to match your company master data in Odoo and Billingo including but not limited to Address, payment configurations, payment methods, Invoice naming rules, due date rules, Bank details etc.
Conclusion
With this integration, Odoo Online users can now enjoy a streamlined invoicing process, leveraging Billingo’s powerful invoicing features without leaving Odoo. While it’s not a fully automated process, the one-click operation ensures efficiency, accuracy, and ease of use.
Seamless Billingo Invoice Generation from Odoo Online