Знакът на КАГАМИ КАГАМИ
kagami.bg/academy · lesson · machine-readable viewVERIFIED 2026-10-01 · UPDATED 2026-10-01
IDENTITY
module
GX10-04-134 · Draft annual financial statements from PostgreSQL to Excel
series
GX10 (local AI server class: NVIDIA GB10, e.g. ASUS Ascent GX10 / DGX Spark)
level
Advanced
duration
about 75 min
prerequisites
PostgreSQL basics, Python 3, a trial balance (closing balances per account) in a table, Microsoft Excel to open the result
trust_label
VERIFIED 2026-10-01 (library versions on PyPI; euro changeover on the ECB page) · UPDATED 2026-10-01 · NOT TESTED on a GB10-class machine and NOT run against a real PostgreSQL; the workbook-building part was run in a sandbox with invented data (openpyxl 3.1.5). Accountancy Act Art. 5(1) and Art. 29(1), (4), (6) verified verbatim against a consolidated text; NSS 1, size thresholds and filing rules were NOT verified
versions
openpyxl 3.1.5 (latest on PyPI, 2024-06-28) · XlsxWriter 3.2.9 (alternative, write-only) · psycopg 3.3.6 · Python 3.10 in the sandbox
language
human view: bg · english edition: /en/academy/gx10/ (same file name)
previous / next
04-133 corporate tax scenarios / 04-176 SAF-T export (finance chain: 04-127, 04-128, 04-129, 04-130, 04-132, 04-133, 04-134, 04-176; related: 04-127 invoice OCR, 04-128 expense classification, 04-132 bank reconciliation)
PURPOSE

Produce a DRAFT balance sheet and income statement as an Excel workbook from closing account balances stored in PostgreSQL, with a built-in balance check. The tool only prepares a draft: a human accountant maps accounts to the official forms, reviews, corrects and signs. Data in the example is fictional; amounts are in euro.

KEY CONCEPTS
COMMANDS / PATHS
CHECKLIST
NEXT MODULE

04-176 SAF-T export (04-176_SAF_T_Export.html) · previous: 04-133 corporate tax scenarios · related: 04-132 bank reconciliation · GX10 series index · offer: Quick experiment (kagami.bg/stalbata/)

SOURCES
TAGS
gx10postgresqlpythonopenpyxlexcelaccountingeurodraft-with-human-review
ПРОВЕРЕНО · 01.10.2026 ОБНОВЕНО · 01.10.2026

ГФО от Postgres в Excel: чернова за счетоводителя

Показваме как от оборотна ведомост в PostgreSQL да получиш чернова на баланс и отчет за приходите и разходите в Excel. Скриптът само подготвя и проверява дали балансът излиза; счетоводителят го съпоставя с официалния образец, поправя го и го подписва. Данните са измислени.

⏱ 75 мин Напреднали GX10 Счетоводство · евро PostgreSQL · Python · openpyxl · Excel
PostgreSQL🔒 локално Python · openpyxl🔒 локално Microsoft Excel (за отваряне)🔒 локално
🔄
ОБНОВЕНО · 01.10.2026 — какво
Урокът е преработен. Махнахме твърдения, които не можахме да потвърдим: че файлът е „готов за НАП и Търговски регистър“, че балансът е „по образец на НАП“, реда „Извънредни приходи“ и позоваването на конкретни форми. Махнахме и грешки в стария код: повторен ред WHEN '30%' в SQL, баланс само с активи, година 2024, записване в /tmp, празен лист ОПР. Добавихме: ясно разделение „чернова — човек подписва“, таблица trial_balance с измислени данни за две години, финансов резултат в капитала, контролен ред (активи = капитал + задължения), точни суми (NUMERIC и Decimal вместо float), суми в евро, връзка към базата през променлива на средата, версии: openpyxl 3.1.5, psycopg 3.3.6, XlsxWriter 3.2.9 (по-долу). Стъпката с локален модел за пояснителни бележки остава само като идея. Част от кода е пускана: частта, която строи Excel файла, я изпълнихме в пясъчник с измислените данни (openpyxl 3.1.5) и прочетохме резултата обратно — контролът излезе 0. Не е пускано върху истинска база и върху GB10.
⚠️
Какво не сме пускали сами
От Закона за счетоводството сверихме с пълния текст само чл. 5, ал. 1 и чл. 29, ал. 1, 4 и 6 (цитирани в стъпка 1). Не сме проверили НСС 1 (Представяне на финансови отчети), праговете за микро- и малко предприятие (в консолидирания текст, който четохме, са още в лева, макар разпоредбите да са изменяни в сила от 01.01.2026), срокове и начин на подаване — затова не ги цитираме; за тях питай счетоводителя и виж актуалния текст в Държавен вестник. Не сме пускали SQL и psycopg срещу истинска PostgreSQL, нито нещо на машина от класа GB10 (затова няма етикет „ТЕСТВАНО“). Не сме отваряли файла в Microsoft Excel — само го прочетохме обратно с openpyxl. Не знаем как се третира валутата на сравнителната колона за 2025 г. — виж стъпка 1.

01Какво ще научиш

02Преди да започнеш

ПакетВерсия (проверено 01.10.2026)За какво е
openpyxl3.1.5 (PyPI, 28.06.2024)чете и пише .xlsx — ползваме го
XlsxWriter3.2.9само пише .xlsx; алтернатива при сложно оформление
psycopg3.3.6връзка с PostgreSQL от Python
💡
Защо евро
България е в еврозоната от 01.01.2026, с фиксиран курс 1 EUR = 1,95583 BGN (по ЕЦБ). Затова примерът е в евро. ⚠️ Как се представя валутата на сравнителните данни за предходна година не проверихме — реши го със счетоводителя, не със скрипта.

03Стъпки

  1. Какво е чернова и кой подписва

    Защо това е първо? Отчетът има правна тежест, а скриптът няма как да знае особеностите на твоята фирма. Затова го ползваме така: скриптът чете числата, които вече са в ведомостта, пренарежда ги и показва дали излизат; човекът-счетоводител решава коя сметка в кой ред отива, поправя и подписва.

    ✅
    Правило
    Инструментът не подава нищо никъде и не твърди, че отчетът е по образец. Всяка страница на файла носи „ЧЕРНОВА“. Човекът одобрява, преди да се прави каквото и да било с реален ефект.
    📜
    Какво казва Законът за счетоводството (сверено дословно)

    Чл. 29, ал. 1: „Годишният финансов отчет за всички предприятия се състои най-малко от счетоводен баланс, отчет за приходите и разходите и приложение.“

    Чл. 29, ал. 4: „Годишният финансов отчет на микропредприятията може да се състои само от съкратен баланс по раздели и съкратен отчет за приходите и разходите.“

    Чл. 29, ал. 6: „Годишният финансов отчет на малките предприятия може да се състои от съкратен баланс по раздели и групи, съкратен отчет за приходите и разходите и приложение.“

    Чл. 5, ал. 1 (в сила от 01.01.2026): „Счетоводните документи в предприятията се съставят на български език с арабски цифри и в евро.“

    Затова черновата има баланс и ОПР, а приложението остава за счетоводителя. Към коя категория е фирмата (чл. 19) и кой вариант на отчета се прилага — решава той. Текстът е от консолидираната редакция в КиК Инфо, прочетена на 01.10.2026; официалният източник е Държавен вестник.

  2. Подготви средата

    bash
    python3 -m venv .venv
    source .venv/bin/activate
    pip install openpyxl==3.1.5 "psycopg[binary]"
    python3 -c "import openpyxl, psycopg; print(openpyxl.__version__, psycopg.__version__)"

    Връзката към базата не се пише в скрипта. Задай я в променлива на средата (GFO_DSN) със стойността от твоята инсталация — вместо шаблон: <връзка-към-базата>.

  3. Таблица и измислени данни

    Скриптът чете затварящите салда. Парите са NUMERIC(15,2) — точни, без закръгляне, каквото прави float. Данните са за измислената „Демо Трейд ЕООД“: за 2026 г. резултатът е 26 000, за 2025 г. — 22 000, и в двата случая активите са равни на капитала плюс задълженията.

    sql · измислени данни
    CREATE TABLE trial_balance (
      company_id     INT      NOT NULL,
      period_year    SMALLINT NOT NULL,
      period_month   SMALLINT NOT NULL,
      account_code   VARCHAR(10) NOT NULL,
      account_name   TEXT,
      closing_debit  NUMERIC(15,2) NOT NULL DEFAULT 0,
      closing_credit NUMERIC(15,2) NOT NULL DEFAULT 0,
      PRIMARY KEY (company_id, period_year, period_month, account_code)
    );
    
    INSERT INTO trial_balance
      (company_id, period_year, period_month, account_code, account_name, closing_debit, closing_credit)
    VALUES
      (1,2026,12,'20','Дълготрайни материални активи',40000,0),
      (1,2026,12,'30','Материални запаси',8000,0),
      (1,2026,12,'41','Клиенти',12000,0),
      (1,2026,12,'50','Парични средства',15000,0),
      (1,2026,12,'10','Основен капитал',0,5000),
      (1,2026,12,'12','Натрупан финансов резултат',0,20000),
      (1,2026,12,'15','Заеми',0,10000),
      (1,2026,12,'40','Доставчици',0,14000),
      (1,2026,12,'70','Приходи от продажби',0,120000),
      (1,2026,12,'60','Разходи за дейността',94000,0),
      (1,2025,12,'20','Дълготрайни материални активи',36000,0),
      (1,2025,12,'30','Материални запаси',7000,0),
      (1,2025,12,'41','Клиенти',10000,0),
      (1,2025,12,'50','Парични средства',9000,0),
      (1,2025,12,'10','Основен капитал',0,5000),
      (1,2025,12,'12','Натрупан финансов резултат',0,12000),
      (1,2025,12,'15','Заеми',0,12000),
      (1,2025,12,'40','Доставчици',0,11000),
      (1,2025,12,'70','Приходи от продажби',0,100000),
      (1,2025,12,'60','Разходи за дейността',78000,0);
    ⚠️
    Сметките са от примера
    Кодовете и имената по-горе са илюстрация, не препис на официален сметкоплан. Твоята фирма има свой сметкоплан и свои подсметки — съпоставянето в стъпка 4 е работа за счетоводителя.
  4. Съпоставяне: сметка → ред

    Защо списък, а не твърд код? Когато счетоводителят промени реда, променяш един ред в списъка, не логиката. Скриптът сумира всички сметки, чийто код започва с даден префикс.

    ПрефиксРед в черноваЧастЗнак
    20Дълготрайни материални активиАктивидебит
    30Материални запасиАктивидебит
    41Вземания от клиентиАктивидебит
    50Парични средстваАктивидебит
    10, 12Основен капитал; натрупан резултатКапиталкредит
    15, 40Заеми; задължения към доставчициЗадължениякредит
    70 / 60Приходи от продажби / разходи за дейносттаОПРкредит / дебит

    Резултатът за годината е приходите минус разходите (клас 7 минус клас 6 в този пример) и влиза в капитала — иначе балансът няма как да излезе.

  5. Скриптът

    Три малки функции: load чете салдата, pick сумира по префикс, build пише листовете и връща разликата от контрола. В Excel записваме стойности, не формули: openpyxl не изчислява формули, а прегледници и други програми показват празни клетки там, където няма записан резултат.

    python · gfo_draft.py
    """Чернова на ГФО: оборотна ведомост (Postgres) в Excel (.xlsx).
    Измислена фирма, EUR. Само чернова: преглежда и подписва счетоводител."""
    import os
    from decimal import Decimal
    from openpyxl import Workbook
    from openpyxl.styles import Font, PatternFill
    import psycopg  # psycopg 3
    
    NAVY = "0D1B3E"
    
    # Илюстративно съпоставяне: префикс на сметка и ред от отчета.
    # Подравни го със СВОЯ сметкоплан и с официалния образец (работа на счетоводителя).
    BALANCE_MAP = [
        ("А. Активи", [("20", "Дълготрайни материални активи"),
                       ("30", "Материални запаси"),
                       ("41", "Вземания от клиенти"),
                       ("50", "Парични средства")], +1),
        ("Б. Собствен капитал", [("10", "Основен капитал"),
                                 ("12", "Натрупан финансов резултат")], -1),
        ("В. Задължения", [("15", "Заеми"),
                           ("40", "Задължения към доставчици")], -1),
    ]
    PNL_MAP = [("Приходи", [("70", "Приходи от продажби")], -1),
               ("Разходи", [("60", "Разходи за дейността")], +1)]
    
    SQL = """
    SELECT account_code, closing_debit - closing_credit AS net
    FROM trial_balance
    WHERE company_id = %s AND period_year = %s AND period_month = 12
    """
    
    def load(conn, company_id, year):
        with conn.cursor() as cur:
            cur.execute(SQL, (company_id, year))
            return {code: Decimal(net) for code, net in cur.fetchall()}
    
    def pick(tb, prefix):                 # дебитно салдо по префикс
        return sum((v for c, v in tb.items() if c.startswith(prefix)), Decimal("0"))
    
    def section_total(tb, lines, sign):   # sign: +1 дебитно, -1 кредитно
        return sum((sign * pick(tb, p) for p, _ in lines), Decimal("0"))
    
    def sheet_header(ws, title, year):
        ws.append([title]); ws["A1"].font = Font(bold=True, size=12)
        ws.append([]); ws.append(["Показател", f"{year}", f"{year-1}"])
        for c in ws[3]:
            c.font, c.fill = Font(bold=True, color="FFFFFF"), PatternFill("solid", fgColor=NAVY)
    
    def money_format(ws):
        for row in ws.iter_rows(min_row=4, min_col=2, max_col=3):
            for c in row:
                if isinstance(c.value, float):
                    c.number_format = "#,##0.00"
        ws.column_dimensions["A"].width = 48
        for col in "BC":
            ws.column_dimensions[col].width = 16
    
    def build(tb_now, tb_prev, company, year, path):
        result = {k: -(pick(tb, "7") + pick(tb, "6"))        # приходи минус разходи
                  for k, tb in (("now", tb_now), ("prev", tb_prev))}
        wb = Workbook()
        ws = wb.active; ws.title = "Баланс"
        sheet_header(ws, f"{company} - Счетоводен баланс към 31.12.{year} (ЧЕРНОВА, EUR)", year)
        totals = {}
        for title, lines, sign in BALANCE_MAP:
            ws.append([title]); ws.cell(ws.max_row, 1).font = Font(bold=True)
            for p, label in lines:
                ws.append([label, float(sign * pick(tb_now, p)), float(sign * pick(tb_prev, p))])
            is_equity = title.startswith("Б")
            if is_equity:                                    # резултатът влиза в капитала
                ws.append(["Финансов резултат за годината",
                           float(result["now"]), float(result["prev"])])
            t = [section_total(tb, lines, sign) + (result[k] if is_equity else 0)
                 for k, tb in (("now", tb_now), ("prev", tb_prev))]
            totals[title[0]] = t
            ws.append([f"Общо {title[3:].lower()}", float(t[0]), float(t[1])])
            ws.cell(ws.max_row, 1).font = Font(bold=True)
        diff = [totals["А"][i] - totals["Б"][i] - totals["В"][i] for i in (0, 1)]
        ws.append([])
        ws.append(["КОНТРОЛ: активи - (капитал + задължения)", float(diff[0]), float(diff[1])])
        ws.append(["Статус"] + ["СХОДЕН" if d == 0 else "НЕСХОДЕН" for d in diff])
        money_format(ws)
    
        wp = wb.create_sheet("ОПР")
        sheet_header(wp, f"Отчет за приходите и разходите за {year} г. (ЧЕРНОВА, EUR)", year)
        for title, lines, sign in PNL_MAP:
            wp.append([title]); wp.cell(wp.max_row, 1).font = Font(bold=True)
            for p, label in lines:
                wp.append([label, float(sign * pick(tb_now, p)), float(sign * pick(tb_prev, p))])
        wp.append(["Финансов резултат", float(result["now"]), float(result["prev"])])
        wp.cell(wp.max_row, 1).font = Font(bold=True)
        money_format(wp)
    
        wn = wb.create_sheet("Бележки")
        wn.append(["Чернова. Не е официален образец. Преглежда и подписва счетоводител."])
        wn.append(["Съпоставянето на сметки в кода е илюстративно."])
        wb.save(path)
        return diff
    
    if __name__ == "__main__":
        year = 2026
        with psycopg.connect(os.environ["GFO_DSN"]) as conn:
            now, prev = load(conn, 1, year), load(conn, 1, year - 1)
        print("разлика от контрола:", build(now, prev, "Демо Трейд ЕООД", year,
                                             f"gfo_chernova_{year}.xlsx"))
  6. Пусни и провери

    bash
    python3 gfo_draft.py

    Очакваш разлика от контрола нула и за двете години (в пясъчника без база тя се печата като [Decimal('0'), Decimal('0')]), а в Excel — три листа. На листа „Баланс“ редът „Статус“ трябва да казва „СХОДЕН“ и за двете години. Ако казва „НЕСХОДЕН“, не оправяй числото в Excel — виж ведомостта: най-често липсва сметка в съпоставянето или салдо със обратен знак. Файлът се отваря с Microsoft Excel.

    В пясъчника с измислените данни изходът беше точно такъв: активи 75 000 и 62 000, капитал 51 000 и 39 000, задължения 24 000 и 23 000, разлика 0 (прочетено обратно с openpyxl 3.1.5).

    💡
    Защо контролът е в самия файл
    Така счетоводителят вижда проверката още при първото отваряне, а несходна чернова не се подава мълчаливо нататък.
  7. Пояснителни бележки (по желание)

    Локален езиков модел може да предложи чернови на пояснителни бележки — например „приходите растат с 20% спрямо предходната година“. Идеята работи само ако човек чете и редактира всяка бележка; моделът не знае политиките на фирмата. 🔒 локално ⚠️ Тази стъпка не сме пускали.

  8. Предай на счетоводителя

    • Файлът е с измислени данни, докато упражняваш; при истински данни — само по сигурен канал и без хранилище с код.
    • Покажи му списъка със съпоставянето (BALANCE_MAP, PNL_MAP) — той го поправя, не ти.
    • Той сверява със своя сметкоплан и с официалния образец, поправя, подписва и решава за подаването. Скриптът не подава нищо.

04Проверка

Тест

1. Каква е ролята на скрипта в урока?

2. За какво е контролният ред „активи − (капитал + задължения)“?

3. Защо парите са NUMERIC в базата и Decimal в Python?

4. Защо в Excel се записват стойности, а не формули?

05Какво следва

06Източници

  1. openpyxl в PyPI · документация 🔒 локално — версия 3.1.5.
  2. XlsxWriter в PyPI · документация — версия 3.2.9.
  3. psycopg в PyPI · документация — версия 3.3.6.
  4. PostgreSQL: числови типове — NUMERIC за точни суми.
  5. ЕЦБ: България в еврозоната — 1 януари 2026 г., курс 1 EUR = 1,95583 BGN.
  6. Закон за счетоводството — консолидиран текст (КиК Инфо) — сверени дословно чл. 5, ал. 1 и чл. 29, ал. 1, 4 и 6 (01.10.2026). ⚠️ НСС 1 и праговете по чл. 19 не са проверявани; официалният текст е в Държавен вестник.