Zökkenőmentes Billingo számla generálás az Odoo Online-ból

Billingo számlázás integrációja az Odoo Online rendszerével

Az Odoo Online egy hatékony vállalatirányítási rendszer, de ha a magyar számlázásról van szó, egyes vállalkozások inkább külső számlázási szolgáltatásokat, például a Billingót használják. Egy egyszerű integrációval az Odoo Online felhasználók mostantól egyetlen kattintással közvetlenül az Odoo számlázásból generálhatnak Billingo számlákat.

Hogyan működik az integráció?

Ebben az esetben nem egy nulla kattintásos integrációról van szó, hanem egy kattintásos integráció, ami azt jelenti, hogy a felhasználónak egyetlen kattintásra van szüksége a folyamat elindításához, amely biztosítja a zökkenőmentes számla generálást, lekérdezést és csatolást az Odoo-n belül. A következők történnek, amikor a felhasználó elindítja a Billingo számla generálását:

  1. Hívás a Billingo API-hoz: A szkript elküldi a szükséges adatokat a Billingónak, amely ezután létrehozza a számlát.
  2. A PDF letöltése: A Billingo-generált számla automatikusan letöltődik az Odoo-ba​.
  3. Csatolás az Odoo Chatterhez: A számla csatolmányként hozzá van csatolva a megfelelő Odoo számla chatteréhez.​
  4. Letöltés helyi tárolásra: A PDF a felhasználó rendszerébe is letöltődik külső használatra.
  5. Küldés az Odoo számlával együtt: Az Odoo számla ügyfélnek történő elküldésekor a Billingo-generált számla is csatolható.

Az integráció legnagyobb előnyei

  • Egyszerűség és hatékonyság: Egyetlen kattintás elegendő a Billingo számla létrehozásához és lekérdezéséhez az Odoo-ban.
  • Automatizált csatolás: A számla az Odoo chatterjében lesz tárolva, így biztosítva, hogy a számlával kapcsolatos összes dokumentum egy helyen legyen.​
  • Javított felhasználói élmény : A felhasználók csatolhatják a Billingo-generált számlát az Odoo-számla kiküldésekor, biztosítva a következetességet és az adóügyi szabályoknak megfelelést.
  • Nincs szükség összetett konfigurációra: Az integráció zökkenőmentesen működik az Odoo Online-on belül, nem igényel külső szkripteket vagy bármilyen külső alkalmazást, továbbá nincs szüksége Odoo.sh -ra sem.  

Az integráció használata a gyakorlatban

Javasoljuk, hogy az Odoo-ban a számla előállítása után, illetve a számla elküldése előtt használja ezt a funkciót.

  1. Nyisson meg egy számlát. Az Odoo-ban a számlákhoz többféleképpen is eljuthat, használja az Ön esetében legmegfelelőbbet, vagy használja az Értékesítés → Számlák vagy a Könyvelés → Számlák vagy Számlázás → Dashboard → Számlák menüpontokat.
  2. Nyisson meg egy számlát, és kattintson a Művelet ikonra (fogaskerék ikon a számla száma után). 

  3. Kattintson a „Create Billingo Invoice” gombra (vagy az egyéni Szerver Műveletének a nevére).
  4. Várja meg az API-hívás végrehajtását - a Billingo azonnal létrehozza a számlát.
  5. Megtalálhatja a PDF-et a Chatterben - A generált Billingo számla mellékletként jelenik meg.
  6. A fájl automatikusan letöltődik - A PDF elmentésre kerül a felhasználó rendszerébe.
  7. A Billingo számla csatolása küldéskor - A számla e-mailben küldésekor a felhasználó a Billingo számlát mellékletként választhatja ki.

Technikai megvalósítás

A megvalósításhoz az integráció a következőket használja:

  • Odoo Server Actions a Billingo API hívásához.
  • Az Odoo ir.attachment modellje a számla tárolására és csatolására.
  • Base64 Encoding a fájlletöltések kezeléséhez (mivel az Odoo Online korlátozza az importot).
  • Chatter Logging a számla documentumok könnyű elérhetőségéhez.

Útmutató az implementációhoz

  1. Jelentkezzen be rendszergazdaként az Odoo online verzióba.
  2. Engedélyezze a fejlesztői módot (debug mode) a beállítások között.
  3. Kattintson a Technikai menüpontra, menjen a Akciók -> Szerver műveletek menüpontra.

  4. A Szerver műveletek képernyőn hozzon létre egy új szerver műveletet.
  5. Nevezze el úgy, hogy a név jelezze a művelet funbkcióját (pl. Create Billingo Invoice). 
    A Type (Típus) menüpontban válassza a Execute Code (Kód végrehajtása) opciót.
    A Technikai beállításoknál a Modellnél válassza a Könyvelési tétel lehetőséget (account.move). Az Allowed Groups (Engedélyezett csoportok) mezőben válasszon ki egy csoportot, amelyiknek hozzáféréssel kell rendelkeznie, például Értékesítések / Adminisztrátor,  több csoportot is kiválaszthat, vagy üresen is hagyhatja, attól függően, hogy kinek szeretne hozzáférést adni. Kattintson a Create Contextual Action gombra.
    Az ACTION DETAILS menü, kód füle alá illessze be az alább csatolt kódot .
  6. Cserélje ki az API-kulcsot és a Számla tömböt a Billingo konfigurációjának megfelelően.


Kód

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',
    }


További lehetséges fejlesztések

Bár ez az integráció biztosít egy egyszerűsített megközelítést, számos további fejlesztést el lehet még végezni:

  1. Dedikált gomb hozzáadása a számlaképernyőn:
    • Ahelyett, hogy a műveletet a számla melletti fogaskerék ikonon keresztül érné el, közvetlenül a számlaűrlaphoz adható egy külön gomb, ami javítja a használhatóságot és csökkenti a kattintások számát.
  2. A Billingo felé továbbított számlaadatok testreszabása:
    • A számlaadatok Billingónak történő továbbítása során a konfigurációs beállítások vagy a számlára vonatkozó adatok alapján különböző paraméterek módosíthatók. A további testreszabás néhány kulcsfontosságú területe a következő:
      • VAT-kulcsok: A regionális adószabályoknak való megfelelés biztosítása.
      • Fizetési módok: Az Odoo fizetési módok automatikus leképezése a Billingo-hoz.
      • Teljesítési dátum és fizetési határidő: Beállítás a vállalati irányelvek alapján.
      • Pénznemek átváltása: Több pénznemben lebonyolított tranzakciók pontos kezelése​.
      • Kedvezmények kezelése: Biztosítsa a tételes kedvezmények helyes alkalmazását.
      • A számla tételeinek csoportosítása: A számla szerkezetének javítása a jobb olvashatóság érdekében.
      • Több számla kezelése: Lehetőség több kiválasztott számla kezelésére a listanézetből.

Természetesen további egyéni fejlesztések is szükségesek lehetnek, a fenti lista nem teljes.  


Jogi nyilatkozat

Ez a cikk sample kódot tartalmaz a Billingo és az Odoo Online integrálásához. A kódot úgy adjuk meg, ahogy van, használja saját felelősségre! Nem garantáljuk a funkcionalitást, a biztonságot vagy a kompatibilitást az Ön konkrét Odoo beállításával. Előfordulhat, hogy a kód egyes részeit az Ön Odoo-konfigurációjának és vállalati folyamatainak megfelelően kell módosítania. Mindig tesztelje az implementációt biztonságos környezetben, mielőtt telepítené a termelésbe!

Erősen ajánlott, hogy az Odoo és a Billingo céges törzsadatait egyeztesse, beleértve, de nem kizárólagosan a címeket, fizetési konfigurációkat, fizetési módokat, számla elnevezési szabályokat, fizetési határidő szabályokat, banki adatokat stb.

 

Összefoglalás

Ezzel az integrációval az Odoo Online felhasználók mostantól élvezhetik az egyszerűsített számlázási folyamatot, kihasználva a Billingo hatékony számlázási funkcióit anélkül, hogy elhagynák az Odoo-t. Bár nem teljesen automatizált folyamatról van szó, az egy kattintással történő működés biztosítja a hatékonyságot, a pontosságot és a könnyű használatot.


Kapcsolat

hüvelyk Hírek
Lépjen be to leave a comment