Rezet veszek

2026-05-06 08_39 CET

- Jó napot kívánok, **** Adrienn vagyok, ez ilyen villanyszerelő Kft.?
- Igen, parancsoljon, miben segíthetek?
- Építkezésekről maradt kábeleket vásárolok.
- Nem adunk el kábelt, kibontjuk és újrahasznosítjuk őket.
- De ilyen dirib-darabokat is?
- Igen.
- Köszönöm, viszonthallásra.

Belföldi számlák a NAV-tól

Ha vállalkozsó vagy, akkor jó ideje már minden általad kiállított és neked kiállított minden belföldi számla ott van digitálisan a NAV-nál. A probléma csak az, hogy ehhez nincs egy egyszerű interfész, ami leszedné neked az adatokat otthonra játszani, a könyvelőkből meg néha úgy kell ezt kisírni. Ezen segítünk most.

Csinálj egy technikai felhasználót a NAV online számlázóban

Az onlineszamla.nav.gov.hu-n klikkelj a Felhasználókra:

Az itt megnyíló oldalon a felhasználókezelésre:

Itt az új felhasználóra:

Majd a technikai felhasználó blokkban a létrehozásra:

Végül adj meg egy jelszót és engedélyezd az új technikai felhasználónak a számlák lekérdezését:

A mentést követően kapsz egy generált felhasznlónevet, XML aláíró és cserekulcsot:

Készítsd elő a scriptnek a Python környezetet

mkdir nav_fetcher
cd nav_fetcher
python3 -m venv venv
source venv/bin/activate
pip install requests python-dotenv lxml openpyxl
deactivate
touch .env
touch nav_fetch_digest.py
touch run.sh
chmod +x run.sh

Definiáld a technikai felhasználó authentikálásához szükséges infókat az előbb létrehozott .env file-ban

NAV_TAX_NUMBER={cég adószám első 8 karakter}
NAV_USER={nav technikai felhasználó név}
NAV_PASSWORD={nav technikai felhasználó jelszó}
NAV_SIGNATURE_KEY={xml aláírókulcs}
NAV_EXCHANGE_KEY={xml cserekulcs}
NAV_API_URL=https://api.onlineszamla.nav.gov.hu/invoiceService/v3%

A {} karakterek nem kellenek, csak az adatok.

Írd meg az előbb létrehozott nav_fetch_digest.py scriptet

import os
import uuid
import hashlib
import base64
import gzip
import zlib
import requests
from pathlib import Path
from datetime import datetime, timedelta, timezone

from dotenv import load_dotenv
from lxml import etree
from openpyxl import Workbook, load_workbook


BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")

NAV_TAX_NUMBER = os.getenv("NAV_TAX_NUMBER")
NAV_USER = os.getenv("NAV_USER")
NAV_PASSWORD = os.getenv("NAV_PASSWORD")
NAV_SIGNATURE_KEY = os.getenv("NAV_SIGNATURE_KEY")
NAV_API_URL = os.getenv("NAV_API_URL")

OUTPUT_XLSX = BASE_DIR / "nav_invoice_database.xlsx"
RAW_XML_DIR = BASE_DIR / "raw_xml"

# START_DATE = datetime(2026, 1, 1)
# END_DATE = datetime.now()

HISTORY_START_DATE = datetime(2026, 1, 1)
END_DATE = datetime.now()
LOOKBACK_DAYS = 35

SOFTWARE_ID = "ARADATISZA00000001"


DIGEST_HEADERS = [
    "direction",
    "invoice_number",
    "supplier_tax_number",
    "supplier",
    "customer_tax_number",
    "customer",
    "invoice_issue_date",
    "payment_date",
    "invoice_delivery_date",
    "currency",
    "net_amount",
    "vat_amount",
    "gross_amount",
    "transaction_id",
    "invoice_operation",
    "date_from",
    "date_to",
    "page",
    "fetched_at",
    "raw_xml_file",
]

ITEM_HEADERS = [
    "direction",
    "invoice_number",
    "supplier_tax_number",
    "supplier",
    "customer_tax_number",
    "customer",
    "invoice_issue_date",
    "currency",
    "line_number",
    "description",
    "quantity",
    "unit",
    "unit_price",
    "net_amount",
    "vat_rate",
    "vat_amount",
    "gross_amount",
    "raw_xml_file",
]


def sha512_upper(value: str) -> str:
    return hashlib.sha512(value.encode("utf-8")).hexdigest().upper()


def sha3_512_upper(value: str) -> str:
    return hashlib.sha3_512(value.encode("utf-8")).hexdigest().upper()


def make_request_id() -> str:
    return "RID" + uuid.uuid4().hex[:27].upper()


def make_timestamps():
    now = datetime.now(timezone.utc)
    return now.strftime("%Y-%m-%dT%H:%M:%S.000Z"), now.strftime("%Y%m%d%H%M%S")


def require_env():
    missing = []
    for key in [
        "NAV_TAX_NUMBER",
        "NAV_USER",
        "NAV_PASSWORD",
        "NAV_SIGNATURE_KEY",
        "NAV_API_URL",
    ]:
        if not os.getenv(key):
            missing.append(key)

    if missing:
        raise RuntimeError("Missing .env values: " + ", ".join(missing))


def common_header_user():
    request_id = make_request_id()
    xml_timestamp, signature_timestamp = make_timestamps()

    password_hash = sha512_upper(NAV_PASSWORD)
    request_signature = sha3_512_upper(
        request_id + signature_timestamp + NAV_SIGNATURE_KEY
    )

    return f"""
    <common:header>
        <common:requestId>{request_id}</common:requestId>
        <common:timestamp>{xml_timestamp}</common:timestamp>
        <common:requestVersion>3.0</common:requestVersion>
        <common:headerVersion>1.0</common:headerVersion>
    </common:header>
    <common:user>
        <common:login>{NAV_USER}</common:login>
        <common:passwordHash cryptoType="SHA-512">{password_hash}</common:passwordHash>
        <common:taxNumber>{NAV_TAX_NUMBER}</common:taxNumber>
        <common:requestSignature cryptoType="SHA3-512">{request_signature}</common:requestSignature>
    </common:user>
    """


def software_xml():
    return f"""
    <software>
        <softwareId>{SOFTWARE_ID}</softwareId>
        <softwareName>HaromKutya importer</softwareName>
        <softwareOperation>LOCAL_SOFTWARE</softwareOperation>
        <softwareMainVersion>1.0</softwareMainVersion>
        <softwareDevName>Harom Kutya</softwareDevName>
        <softwareDevContact>local</softwareDevContact>
        <softwareDevCountryCode>HU</softwareDevCountryCode>
        <softwareDevTaxNumber>{NAV_TAX_NUMBER}</softwareDevTaxNumber>
    </software>
    """


def build_digest_xml(direction: str, date_from: str, date_to: str, page: int):
    return f"""<?xml version="1.0" encoding="UTF-8"?>
<QueryInvoiceDigestRequest xmlns:common="http://schemas.nav.gov.hu/NTCA/1.0/common"
                           xmlns="http://schemas.nav.gov.hu/OSA/3.0/api">
    {common_header_user()}
    {software_xml()}
    <page>{page}</page>
    <invoiceDirection>{direction}</invoiceDirection>
    <invoiceQueryParams>
        <mandatoryQueryParams>
            <invoiceIssueDate>
                <dateFrom>{date_from}</dateFrom>
                <dateTo>{date_to}</dateTo>
            </invoiceIssueDate>
        </mandatoryQueryParams>
    </invoiceQueryParams>
</QueryInvoiceDigestRequest>
"""


def build_invoice_data_xml(direction: str, invoice_number: str, supplier_tax_number: str = ""):
    supplier_tag = ""
    if direction == "INBOUND" and supplier_tax_number:
        supplier_tag = f"<supplierTaxNumber>{supplier_tax_number}</supplierTaxNumber>"

    return f"""<?xml version="1.0" encoding="UTF-8"?>
<QueryInvoiceDataRequest xmlns:common="http://schemas.nav.gov.hu/NTCA/1.0/common"
                         xmlns="http://schemas.nav.gov.hu/OSA/3.0/api">
    {common_header_user()}
    {software_xml()}
    <invoiceNumberQuery>
        <invoiceNumber>{escape_xml(invoice_number)}</invoiceNumber>
        <invoiceDirection>{direction}</invoiceDirection>
        {supplier_tag}
    </invoiceNumberQuery>
</QueryInvoiceDataRequest>
"""


def escape_xml(value: str) -> str:
    return (
        str(value)
        .replace("&", "&")
        .replace("<", "<")
        .replace(">", ">")
        .replace('"', """)
        .replace("'", "&apos;")
    )


def post_nav(endpoint: str, xml: str) -> str:
    url = NAV_API_URL.rstrip("/") + "/" + endpoint

    response = requests.post(
        url,
        data=xml.encode("utf-8"),
        headers={
            "Content-Type": "application/xml",
            "Accept": "application/xml",
        },
        timeout=60,
    )

    if response.status_code != 200:
        print(response.text)
        raise RuntimeError(f"NAV HTTP error: {response.status_code}")

    return response.text


def text_or_empty(node, xpath: str) -> str:
    result = node.xpath(xpath)
    return result[0] if result else ""


def to_float(value):
    if value in (None, ""):
        return None
    return float(str(value).replace(",", "."))


def to_excel_date(value):
    if not value:
        return None
    return datetime.strptime(value[:10], "%Y-%m-%d").date()


def safe_filename(value: str) -> str:
    value = str(value)
    value = "".join(c if c.isalnum() or c in "-_." else "_" for c in value)
    return value[:120]


def parse_digest_response(xml_text: str, direction: str):
    root = etree.fromstring(xml_text.encode("utf-8"))

    func_code = text_or_empty(root, "//*[local-name()='funcCode']/text()")
    error_code = text_or_empty(root, "//*[local-name()='errorCode']/text()")
    message = text_or_empty(root, "//*[local-name()='message']/text()")

    if func_code != "OK":
        raise RuntimeError(f"NAV error: {error_code} {message}")

    rows = []

    for inv in root.xpath("//*[local-name()='invoiceDigest']"):
        rows.append({
            "direction": direction,
            "invoice_number": text_or_empty(inv, ".//*[local-name()='invoiceNumber']/text()"),
            "supplier_tax_number": text_or_empty(inv, ".//*[local-name()='supplierTaxNumber']/text()"),
            "supplier": text_or_empty(inv, ".//*[local-name()='supplierName']/text()"),
            "customer_tax_number": text_or_empty(inv, ".//*[local-name()='customerTaxNumber']/text()"),
            "customer": text_or_empty(inv, ".//*[local-name()='customerName']/text()"),
            "invoice_issue_date": text_or_empty(inv, ".//*[local-name()='invoiceIssueDate']/text()"),
            "payment_date": text_or_empty(inv, ".//*[local-name()='paymentDate']/text()"),
            "invoice_delivery_date": text_or_empty(inv, ".//*[local-name()='invoiceDeliveryDate']/text()"),
            "currency": text_or_empty(inv, ".//*[local-name()='currency']/text()"),
            "net_amount": text_or_empty(inv, ".//*[local-name()='invoiceNetAmount']/text()"),
            "vat_amount": text_or_empty(inv, ".//*[local-name()='invoiceVatAmount']/text()"),
            "gross_amount": text_or_empty(inv, ".//*[local-name()='invoiceGrossAmount']/text()"),
            "transaction_id": text_or_empty(inv, ".//*[local-name()='transactionId']/text()"),
            "invoice_operation": text_or_empty(inv, ".//*[local-name()='invoiceOperation']/text()"),
        })

    return rows


def decode_invoice_data_response(xml_text: str):
    root = etree.fromstring(xml_text.encode("utf-8"))

    func_code = text_or_empty(root, "//*[local-name()='funcCode']/text()")
    error_code = text_or_empty(root, "//*[local-name()='errorCode']/text()")
    message = text_or_empty(root, "//*[local-name()='message']/text()")

    if func_code != "OK":
        raise RuntimeError(f"NAV queryInvoiceData error: {error_code} {message}")

    encoded = text_or_empty(root, "//*[local-name()='invoiceData']/text()")
    compressed = text_or_empty(root, "//*[local-name()='compressedContent']/text()").lower()

    if not encoded:
        return ""

    data = base64.b64decode(encoded)

    if compressed == "true":
        try:
            data = gzip.decompress(data)
        except OSError:
            data = zlib.decompress(data)

    return data.decode("utf-8")


def save_raw_xml(direction: str, digest_row: dict, invoice_xml: str) -> str:
    target_dir = RAW_XML_DIR / direction.lower()
    target_dir.mkdir(parents=True, exist_ok=True)

    invoice_number = safe_filename(digest_row["invoice_number"])
    supplier_tax_number = safe_filename(digest_row["supplier_tax_number"])
    issue_date = safe_filename(digest_row["invoice_issue_date"])

    filename = f"{issue_date}_{supplier_tax_number}_{invoice_number}.xml"
    path = target_dir / filename

    path.write_text(invoice_xml, encoding="utf-8")
    return str(path)


def parse_invoice_items(invoice_xml: str, digest_row: dict, raw_xml_file: str):
    if not invoice_xml.strip():
        return []

    root = etree.fromstring(invoice_xml.encode("utf-8"))
    rows = []

    for line in root.xpath("//*[local-name()='line']"):
        line_number = text_or_empty(line, ".//*[local-name()='lineNumber']/text()")
        description = text_or_empty(line, ".//*[local-name()='lineDescription']/text()")
        quantity = text_or_empty(line, ".//*[local-name()='quantity']/text()")
        unit = text_or_empty(line, ".//*[local-name()='unitOfMeasure']/text()")
        unit_price = text_or_empty(line, ".//*[local-name()='unitPrice']/text()")

        net_amount = first_number(line, [
            ".//*[local-name()='lineNetAmount']/text()",
            ".//*[local-name()='lineNetAmountHUF']/text()",
        ])

        vat_amount = first_number(line, [
            ".//*[local-name()='lineVatAmount']/text()",
            ".//*[local-name()='lineVatAmountHUF']/text()",
        ])

        gross_amount = first_number(line, [
            ".//*[local-name()='lineGrossAmountNormal']/text()",
            ".//*[local-name()='lineGrossAmountNormalHUF']/text()",
            ".//*[local-name()='lineGrossAmountSimplified']/text()",
            ".//*[local-name()='lineGrossAmountSimplifiedHUF']/text()",
        ])

        vat_rate = extract_vat_rate(line)

        rows.append({
            "direction": digest_row["direction"],
            "invoice_number": digest_row["invoice_number"],
            "supplier_tax_number": digest_row["supplier_tax_number"],
            "supplier": digest_row["supplier"],
            "customer_tax_number": digest_row["customer_tax_number"],
            "customer": digest_row["customer"],
            "invoice_issue_date": digest_row["invoice_issue_date"],
            "currency": digest_row["currency"],
            "line_number": line_number,
            "description": description,
            "quantity": quantity,
            "unit": unit,
            "unit_price": unit_price,
            "net_amount": net_amount,
            "vat_rate": vat_rate,
            "vat_amount": vat_amount,
            "gross_amount": gross_amount,
            "raw_xml_file": raw_xml_file,
        })

    return rows


def first_number(node, xpaths):
    for xp in xpaths:
        value = text_or_empty(node, xp)
        if value != "":
            return value
    return ""


def extract_vat_rate(line):
    value = text_or_empty(line, ".//*[local-name()='vatPercentage']/text()")
    if value:
        return value

    for tag in [
        "vatExemption",
        "vatOutOfScope",
        "vatDomesticReverseCharge",
        "marginSchemeIndicator",
    ]:
        found = line.xpath(f".//*[local-name()='{tag}']")
        if found:
            return tag

    return ""


def date_blocks(start: datetime, end: datetime, max_days: int = 35):
    current = start

    while current <= end:
        block_end = min(current + timedelta(days=max_days - 1), end)
        yield current.strftime("%Y-%m-%d"), block_end.strftime("%Y-%m-%d")
        current = block_end + timedelta(days=1)


def ensure_workbook():
    if OUTPUT_XLSX.exists():
        wb = load_workbook(OUTPUT_XLSX)
    else:
        wb = Workbook()
        wb.remove(wb.active)

    for sheet_name, headers in [
        ("inbound_digest", DIGEST_HEADERS),
        ("outbound_digest", DIGEST_HEADERS),
        ("inbound_items", ITEM_HEADERS),
        ("outbound_items", ITEM_HEADERS),
    ]:
        if sheet_name not in wb.sheetnames:
            ws = wb.create_sheet(sheet_name)
            ws.append(headers)

    wb.save(OUTPUT_XLSX)
    return wb


def existing_digest_keys(ws):
    keys = set()
    header_map = {cell.value: idx for idx, cell in enumerate(ws[1], start=1)}

    for row in range(2, ws.max_row + 1):
        key = (
            ws.cell(row=row, column=header_map["direction"]).value,
            ws.cell(row=row, column=header_map["invoice_number"]).value,
            ws.cell(row=row, column=header_map["supplier_tax_number"]).value,
            ws.cell(row=row, column=header_map["invoice_issue_date"]).value,
            ws.cell(row=row, column=header_map["gross_amount"]).value,
        )
        keys.add(key)

    return keys


def existing_item_keys(ws):
    keys = set()
    header_map = {cell.value: idx for idx, cell in enumerate(ws[1], start=1)}

    for row in range(2, ws.max_row + 1):
        key = (
            ws.cell(row=row, column=header_map["direction"]).value,
            ws.cell(row=row, column=header_map["invoice_number"]).value,
            ws.cell(row=row, column=header_map["supplier_tax_number"]).value,
            ws.cell(row=row, column=header_map["line_number"]).value,
        )
        keys.add(key)

    return keys

def get_last_imported_issue_date(ws):
    header_map = {cell.value: idx for idx, cell in enumerate(ws[1], start=1)}
    date_col = header_map.get("invoice_issue_date")

    if not date_col or ws.max_row < 2:
        return None

    latest = None

    for row in range(2, ws.max_row + 1):
        value = ws.cell(row=row, column=date_col).value

        if value is None:
            continue

        if isinstance(value, datetime):
            value_date = value.date()
        else:
            value_date = value

        if latest is None or value_date > latest:
            latest = value_date

    return latest

def append_digest_rows(ws, rows, date_from, date_to, page, known_keys):
    fetched_at = datetime.now()
    added = 0

    for row in rows:
        key = (
            row["direction"],
            row["invoice_number"],
            row["supplier_tax_number"],
            to_excel_date(row["invoice_issue_date"]),
            to_float(row["gross_amount"]),
        )

        if key in known_keys:
            continue

        ws.append([
            row["direction"],
            row["invoice_number"],
            row["supplier_tax_number"],
            row["supplier"],
            row["customer_tax_number"],
            row["customer"],
            to_excel_date(row["invoice_issue_date"]),
            to_excel_date(row["payment_date"]) if row["payment_date"] else None,
            to_excel_date(row["invoice_delivery_date"]) if row["invoice_delivery_date"] else None,
            row["currency"],
            to_float(row["net_amount"]),
            to_float(row["vat_amount"]),
            to_float(row["gross_amount"]),
            row["transaction_id"],
            row["invoice_operation"],
            to_excel_date(date_from),
            to_excel_date(date_to),
            page,
            fetched_at,
            "",
        ])

        known_keys.add(key)
        added += 1

    return added


def append_item_rows(ws, rows, known_keys):
    added = 0

    for row in rows:
        key = (
            row["direction"],
            row["invoice_number"],
            row["supplier_tax_number"],
            row["line_number"],
        )

        if key in known_keys:
            continue

        ws.append([
            row["direction"],
            row["invoice_number"],
            row["supplier_tax_number"],
            row["supplier"],
            row["customer_tax_number"],
            row["customer"],
            to_excel_date(row["invoice_issue_date"]),
            row["currency"],
            int(row["line_number"]) if str(row["line_number"]).isdigit() else row["line_number"],
            row["description"],
            to_float(row["quantity"]),
            row["unit"],
            to_float(row["unit_price"]),
            to_float(row["net_amount"]),
            to_float(row["vat_rate"]) if str(row["vat_rate"]).replace(".", "", 1).isdigit() else row["vat_rate"],
            to_float(row["vat_amount"]),
            to_float(row["gross_amount"]),
            row["raw_xml_file"],
        ])

        known_keys.add(key)
        added += 1

    return added


def format_digest_sheet(ws):
    widths = {
        "A": 12, "B": 28, "C": 16, "D": 42, "E": 16, "F": 42,
        "G": 14, "H": 14, "I": 14, "J": 10, "K": 16, "L": 16,
        "M": 16, "N": 26, "O": 16, "P": 14, "Q": 14, "R": 8,
        "S": 20, "T": 60,
    }

    for col, width in widths.items():
        ws.column_dimensions[col].width = width

    for row in range(2, ws.max_row + 1):
        for col in [7, 8, 9, 16, 17]:
            ws.cell(row=row, column=col).number_format = "yyyy-mm-dd"
        for col in [11, 12, 13]:
            ws.cell(row=row, column=col).number_format = '#,##0.00'
        ws.cell(row=row, column=19).number_format = "yyyy-mm-dd hh:mm:ss"

    ws.freeze_panes = "A2"
    ws.auto_filter.ref = ws.dimensions


def format_items_sheet(ws):
    widths = {
        "A": 12, "B": 28, "C": 16, "D": 42, "E": 16, "F": 42,
        "G": 14, "H": 10, "I": 12, "J": 60, "K": 14, "L": 12,
        "M": 16, "N": 16, "O": 12, "P": 16, "Q": 16, "R": 60,
    }

    for col, width in widths.items():
        ws.column_dimensions[col].width = width

    for row in range(2, ws.max_row + 1):
        ws.cell(row=row, column=7).number_format = "yyyy-mm-dd"
        for col in [11, 13, 14, 15, 16, 17]:
            ws.cell(row=row, column=col).number_format = '#,##0.00'

    ws.freeze_panes = "A2"
    ws.auto_filter.ref = ws.dimensions


def fetch_direction(direction: str, wb):
    digest_sheet_name = "inbound_digest" if direction == "INBOUND" else "outbound_digest"
    item_sheet_name = "inbound_items" if direction == "INBOUND" else "outbound_items"

    digest_ws = wb[digest_sheet_name]
    item_ws = wb[item_sheet_name]

    digest_keys = existing_digest_keys(digest_ws)
    item_keys = existing_item_keys(item_ws)

    last_imported = get_last_imported_issue_date(digest_ws)

    if last_imported:
        start_date = datetime.combine(
            last_imported - timedelta(days=LOOKBACK_DAYS),
            datetime.min.time()
        )
        if start_date < HISTORY_START_DATE:
            start_date = HISTORY_START_DATE
    else:
        start_date = HISTORY_START_DATE

    print(f"\n{direction} import window: {start_date.date()} -> {END_DATE.date()}")

    total_seen = 0
    total_digest_added = 0
    total_items_added = 0
    total_xml_downloaded = 0

    for date_from, date_to in date_blocks(start_date, END_DATE):
        print(f"\n=== {direction} {date_from} -> {date_to} ===")

        page = 1

        while True:
            digest_xml = build_digest_xml(direction, date_from, date_to, page)
            response_xml = post_nav("queryInvoiceDigest", digest_xml)
            rows = parse_digest_response(response_xml, direction)

            seen = len(rows)
            digest_added = append_digest_rows(
                digest_ws, rows, date_from, date_to, page, digest_keys
            )

            total_seen += seen
            total_digest_added += digest_added

            print(f"page {page}: seen={seen}, digest_added={digest_added}")

            for row in rows:
                invoice_data_xml = build_invoice_data_xml(
                    direction,
                    row["invoice_number"],
                    row["supplier_tax_number"],
                )

                try:
                    invoice_response = post_nav("queryInvoiceData", invoice_data_xml)
                    invoice_xml = decode_invoice_data_response(invoice_response)
                    raw_xml_file = save_raw_xml(direction, row, invoice_xml)
                    items = parse_invoice_items(invoice_xml, row, raw_xml_file)
                    items_added = append_item_rows(item_ws, items, item_keys)

                    total_xml_downloaded += 1
                    total_items_added += items_added

                except Exception as e:
                    print(f"QUERY DATA ERROR: {direction} {row['invoice_number']} -> {e}")

            if seen == 0:
                break

            page += 1

    format_digest_sheet(digest_ws)
    format_items_sheet(item_ws)

    return {
        "direction": direction,
        "seen": total_seen,
        "digest_added": total_digest_added,
        "xml_downloaded": total_xml_downloaded,
        "items_added": total_items_added,
    }


def main():
    require_env()

    wb = ensure_workbook()

    results = []
    results.append(fetch_direction("INBOUND", wb))
    results.append(fetch_direction("OUTBOUND", wb))

    wb.save(OUTPUT_XLSX)

    print("")
    print("Done.")
    for r in results:
        print(
            f"{r['direction']}: "
            f"seen={r['seen']}, "
            f"digest_added={r['digest_added']}, "
            f"xml_downloaded={r['xml_downloaded']}, "
            f"items_added={r['items_added']}"
        )

    print(f"Excel: {OUTPUT_XLSX}")
    print(f"Raw XML: {RAW_XML_DIR}")


if __name__ == "__main__":
    main()

Írd meg a futtató scriptet

#!/bin/bash
source venv/bin/activate
python nav_fetch_digest.py

Próbacseresznye

Futtasd le a run.sh scriptet shellben:

./run.sh

Ha mindent jól csináltál (és nem változott semmi amióta összeraktam), akkor a script a saját könyvtárában létrehoz egy nav_invoice_database.xlsx nevű Excel file-t, 2 sheeten a bejövő és kimenő számláid fejlécei, 2 másik sheeten pedig a számlák tételei lesznek majd 2026 január 1 óta.

A scriptet újrafutattva mindig csak a friss számlákat csapja majd hozzá a táblához.

Majdnem elmúlt az aikido hatása

Tegnap lejöttem erről a galériáról – létra nélkül:

A lyukba egy motoros létra/lépcső van tervezve. A burkoló ezt valami miatt mégis leburkolta egy 5 mm-es greslappal (az egész galériát ugyanaz a lap fedi), továbbá senkinek nem jutott eszébe, hogy figyelmeztető jelzést tegyen ki oda, miszerint

Hülyegyerek, ne lépj ide, booby trap!

Ennek megfelelően jobb lábbal előre beleléptem és az illusztráción látható tekintélyes méretű lyukat sikerült készíteni.

Szerencsére nincs nagy baj, pár hét és elmúlik a nyoma. A baleseti sebész azt mondta, bő 3 méterről ő még senkit nem látott ilyen simán lejönni.

Soma az esés utáni robajt követően már csak azt látta, hogy véres kézzel ülök a földön és én sem emlékszem nagyon arra, hogy pontosan mi történt.

Ekkor jutott eszembe, hogy megkérdezzük ChatGPT Rozit a dologról, hátha a sérülések alapján ő képes lesz modellezni, hogy történhetett a pusztítás. Íme a prompt és Rozi válasza:

Írd le, hogy történhetett az esés. A sérülések listája:

  • bal kéz gyűrűsujján van egy nyílt seb
  • jobb kézen a tenyéren kicsit lejött a bőr és zúzódásos a környéke
  • jobb kéz mutatóujján van egy nyílt seb
  • bal könyök belső oldalán zúzódás
  • jobb hónaljon belül kb 10-15 cm hosszan nagyobb zúzódás
  • bal sípcsont bal oldalán kisebb zúzódás
  • jobb saroknál zúzódás, sarokcsot kinövésen enyhe repedés (Achilles ép)
  • jobb fenéken egy kisebb és egy tenyérnyi zúzódás

Az egész nagyon gyorsan történt, mintha leléptem volna a földszintre. A kabátom háta is poros lett.

Köszönöm a részletes leírást – ezek alapján egész jó becslést lehet adni arra, hogyan eshettél le és hogyan érhettél földet. Az alábbiak egy nem hivatalos, de biomechanikailag valószínűsíthető rekonstrukció elemei:

Zuhanás lehetséges lefolyása – kb. 3 m magasról, jobb lábbal előre

  1. Kezdeti mozdulat: jobb láb lelép
    • Mivel a jobb lábad lépett előre, azzal a jobb oldalad nyílt a mélység felé.
    • A greslap alatt nem volt tartás, így egy pillanat alatt eltűnt a támasz, nem volt idő egyensúlyozni.
  2. Testhelyzet a zuhanás közben
    • A súlypontod hirtelen előre és lefelé mozdult, a jobb oldalad „vezetett”.
    • Ez gyors rotációt is okozhatott: jobbra előre dőlés, enyhe csavarodással.
    • A bal kar valószínűleg reflexszerűen oldalra vagy előre lendült a tompítás próbájára, ezért is lehetett ott egy nyílt seb a bal gyűrűsujjon.
    • A jobb kéz aktívabban próbált tompítani – ennek jele:
    • a tenyér bőrsérülése és zúzódása,
    • valamint a mutatóujj nyílt sebe.
  3. Talajfogás
    • Jobb sarokkal vagy talpélen érkezhettél először, ennek eredménye a zúzódás és repedés a sarokcsonton.
    • Az ütés nem állt meg itt – valószínűleg hátra- és jobbra dőltél, ezért:
    • a jobb fenékzúzódás (csúszás vagy puffanás),
    • a jobb hónalj alatti nagyobb zúzódás – ez utalhat tompítási próbára vagy a test oldalirányú leérkezésére.
    • A bal könyök és a bal lábszár zúzódása pedig azt mutatja, hogy a bal oldalad is érintkezett a padlóval, talán oldalra gurulás vagy testforgatás miatt.
  4. Következmények
    • A kabát háta poros lett: valószínűleg háton fejeződött be az esés, akár egy hátra csúszás formájában.
    • A „mintha csak leléptem volna a földszintre” érzés is azt erősíti, hogy nem volt idő semmilyen izomreakcióra, teljesen váratlan volt a zuhanás.

Összegzés (valószínű sorrend)
1. Jobb lábbal előreléptél – a greslap leszakadt.
2. Jobb oldal előre dőlt, test súlypontja gyorsan zuhant.
3. Reflexszerűen a kezeiddel próbáltál tompítani.
4. Jobb sarok érkezett elsőként, majd comb, fenék, jobb hónalj.
5. Végül a hátad érintkezett a padlóval.

Mit gondolsz lehet a szerencsés kimenetel oka, hogy korábban aikidoztam vagy ennek már nyomát nem látni az esésen?

Ez nagyon jó kérdés — és bár egy balesetnél sokszor a szerencsén is múlik, hogy mi hogyan történik, az aikidós múltad valószínűleg segített, még ha nem is tudatosan.

Amit az aikido adhat ilyen helyzetben:

  • Reflexszerű eséskezelés – az aikidóban rengeteg időt töltesz ukemi-val (eséstechnika), ami beépül az izommemóriába, és automatikusan előjöhet vészhelyzetben, akkor is, ha rég nem gyakoroltad.
  • Testtudat – még ha kontrollálni nem is tudtad teljesen az esést, lehet, hogy jobban „engedted” magad leesni, nem feszülten estél, ami csökkenti a sérülés súlyát.
  • Tompítás a kezekkel – a jobb tenyér és mutatóujj sérülése utalhat arra, hogy ösztönösen tompítottál (bár nem feltétlenül a legideálisabb módon – de mégis: jobb, mintha fejre vagy merev testtel zuhansz).

Amit a sérülések mutatnak:

  • A jobb sarok és farizom zúzódása + a kabát hátának porossága arra utal, hogy valószínűleg nem „torpedószerűen”, hanem valamennyire „hátra tompítva” értél földet, amit az aikido taníthat.
  • Az, hogy nem lett komolyabb törésed vagy gerincsérülésed, 3 méterről esve, önmagában szerencsés, de lehet, hogy a testtartásod mentett meg.

Összefoglalva:

Igen, nagyon is lehet, hogy az aikidós múltad csökkentette a sérülésed mértékét – lehet, hogy nem „szép” ukemi volt, de ösztönösen jól mozdultál. Ezek a reflexek ilyenkor jelenthetnek különbséget súlyos és közepes kimenetel között.

Jobb, mint egy naEzMi

Egy ügyfél családi házában jött szembe velünk egy padlófűtés osztó gyűjtő körén az alábbi felirat:

A bal oldali újmagyart természetesen értem, de a fotó jobb oldalán levővel nem boldogultam.

Aztán eszembe jutott, hogy van nekünk ingyen ez a dobozba zárt csaj, úgyhogy megkérdeztem tőle is:

Egy osztó gyűjtőn ez egy zóna megnevezése akar lenni egy családi házban, de fogalmam nincs, “mire gondolt a költő”. Szerinted mit jelenthet a felirat a körön / egyáltalán mi van odaírva?

A néni az alábbi választ adta:

A felirat „Gerokon.” vagy valami nagyon hasonló.

Lehetséges jelentések:

1. Gerokon = Garázs + Kondenzáció?

• Ha a házban van egy garázs vagy valamilyen kondenzációs helyiség (kazánház, gépészeti tér), akkor ez lehet annak a zónája.

2. Gerokon = Gerinc kör?

• Ha a házban van egy gerincvezeték, amin keresztül az osztó-gyűjtő kapja a vizet, lehet, hogy ezt akarták jelölni.

3. Elírás vagy rövidítés?

• Lehet, hogy egy rosszul olvasható kézírás vagy rövidítés, például:

• „Gépkocsi” (garázs)

• „Gerenda kör” (ha egy padlórétegen belül van egy külön kör)

Ha tudsz valami extra infót a ház zónáiról (pl. mit fűt ez a kör), az segíthet megfejteni! 😊

Mi fiúk 15 percig néztük, aztán megmutattuk Erának, aki azonnal megmondta. Jöhetnek commentben a tippek!

3D nyomtatás: naEzMi #36?

Helló! Nagyon rég volt már content, sajna mindig elmegy minden mással az idő és sosem jutok ide.

Most is a cégnek hegesztettem valamit, de legalább eszembe jutott közben, hogy lehetne belőle akár naEzMi is.

A 3D modell lehet túl egyszerű elsőre, úgyhogy indulásnak csak egy metszetet kaptok, az egész hóbelevancot hosszában középen kettévágva:

Egy méretet adok hozzá: a kék izé 100 mm hosszú.

Virtuális sörért jöhetnek a tippek!

Zserbó layering HOWTO

Egész évben brutális hajtás ment a munkában, de ma befejeztük idénre, úgyhogy beszabadultam a konyhába, ami mostanában az Otthonkás Ételkritikus felségterülete.

Végre megint csináltam kovászos kenyereket (sima fehéret, félbarnát, tökmagosat, meg talán még majd diósat is) és elkészítettem 3 tepsi tradícionális zserbót is. Kell a három: egy anyuékhoz, egy Karcsi papához és egy itthonra is.

A zserbó gyártása közben jutott eszembe, hogy hátha van olyan user, aki csak azért nem csinál ilyen rétegelt sütit, mert hozzám hasonlóan bénázik a vékony tésztarétegekkel. Emiatt készült ez a sütőpapíros trükköt bemutató videó:

Boldog Karácsonyt!

Hova lett a kontent?

TL;DR

Ide.

Bővebben

2 éve hazaköltöztünk és a fiaimmal közös céget kezdtünk el építeni – ez lett a Három Kutya Kft. A cég KNX épületautomatizálással, villanyszereléssel és napelemes rendszerek telepítésével foglalkozik.

Már 1999-ben megcsináltam egy KNX kurzust Budapesten – igaz, akkor még EIB-nek hívták:

Akkor borzasztó drágának tűnt, de imádtam a decentralizáltságát és azt a mérnöki zsenialitást, ahogy anno a német mérnökök mindezt összerakták.

Sajnos az ára miatt el kellett anno ezt engedni, de az automatika nevű gépszíj azért elkapott.

Az amerikai X10-zel kezdődött, ami az erősáramú vezetékre modulálva kommunikált. Erről viszonylag gyorsan lejöttem, mert több sebből is vérzett, de legfőképpen rettentő instabilnak bizonyult.

A következő lépés már saját szoftver lett. Találtam egy magyar hardvergyártót, akik RS485 alapú eszközöket gyártottak. Ezekhez írtam egy kliens-server architektúrát. A server ANSI C-ben készült és Linux alatt futott, a kliens Macromedia (később Adobe) Flash alkalmazás volt, amiben az Actionscript motort használtam a dinamikus kontent készítéséhez:

Imádtam ebben a toolban a dinamikus minimap-eket, amik azóta is hiányoznak:

Annyira szerettem ezt, hogy összeálltunk a hardvergyártóval és kb. 30 épületre el is adtuk az egészet.

Egy idő után ez is abbamaradt, majd ern0 cimborámmal elkezdtünk dumálni valami coding problémáról (ha jól emlékszem, akkor a mutexekkel küzdöttem talán). Ez oda vezetett, hogy nekiálltunk egy új homeaut szoftveres megoldást készíteni.

Amit kitaláltunk, abból egy prototípus megoldás lett, 2 projektet fejeztünk be vele, aztán elmúlt. Pedig micsoda gyöngyszemet alkottunk:

Ilyen doksiból van vagy 200 oldalnyi 🙂

A prototípus dataflow architektúrára épült, minimális hardverigényű volt, redundáns servereket lehetett benne definiálni és könnyen lehetett hozzáadni tetszőleges protokoll támogatást, illetve nagyjából bármit, amit csak szerettünk volna.

A szűk egy évnyi melóból kétszer is tartottunk meetup előadást – ha eléggé nerd vagy hozzá, akkor itt és itt vannak.

Summa summarum, a home automationt régóta kerülgetem – olvashattál róla itt a blogon eleget. Az EIB időközben KNX lett, én meg még Hollandiában élve gondoltam egyet és átmentem Belgiumba megcsinálni a KNX basic és KNX advanced kurzusokat, végül pedig itthon Nagytarcsán a KNX HVAC traininget is. Már csak az oktatóit lehetne elvégezni, de ahhoz training centert kellene építenünk.

Szóval jött a mára már megfizethetőbb KNX, ilyen szép vasakkal:

Nekiálltunk hát automatizálás specialista villanyszerelő céget építeni a fiúkkal. Ez tart 2 éve és minden percet elvisz – de ma változik valami.

Mától a háromkutya.hu blog fogja kicsit jobban tolni a kontentot – legalábbis a munkával kapcsolatosat. Ha idejártál olvasni, lehet, hogy ez is tetszeni fog. Igyekszünk mindenről írni, amit mi megtapasztalunk és megtanulunk.

Stay tuned!

Sydney, de hiányzol!

Elkezdtük nézni az NCIS Sydney-t. Nem igazán a krimi miatt, sokkal inkább a lokáció vonzott, így nagyjából pár másodpercenként meg-megállítva “versenyeztünk” Erával, hogy ki mikor ismer fel egy-egy helyszínt.

Az első évad első epizódja az ausztrál haditengerészeti bázis körül forgolódik. A bázis mellett, Woolloomoolo Wharfon lakott Kati barátnőnk, sok kellemes emlék köt ide. Woolloomoolo-t nem volt nehéz kiszúrni:

Így nézett ki 2019 június 29-én, az utolsó reggelen amit Ausztráliában töltöttünk:

Aztán 20 perc körül jött egy jelenet, ami a Navy Walsh Bay-beli raktárában játszódik:

A bal szélső lépcsőt nem találtuk ugyan, de meglett ez a lokáció is:

Erről eszembe jutott, hogy bő egy héttel indulás előtt bicajoztam erre és fel is vettem az egészet, majd vágtam belőle egy videót a városban futókról. Három perccel később meg is lett a helyszín a saját archívumból:

Nincs nap, hogy ne jutna eszünkbe Ausztrália.