ГФО от Postgres в Excel: чернова за счетоводителя
Показваме как от оборотна ведомост в PostgreSQL да получиш чернова на баланс и отчет за приходите и разходите в Excel. Скриптът само подготвя и проверява дали балансът излиза; счетоводителят го съпоставя с официалния образец, поправя го и го подписва. Данните са измислени.
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.
psycopg срещу истинска PostgreSQL, нито нещо на машина от класа GB10 (затова няма етикет „ТЕСТВАНО“). Не сме отваряли файла в Microsoft Excel — само го прочетохме обратно с openpyxl. Не знаем как се третира валутата на сравнителната колона за 2025 г. — виж стъпка 1.
01Какво ще научиш
- Как да подредиш оборотна ведомост в таблица, от която се чертае отчет.
- Как да съпоставиш сметки с редове чрез настройваем списък, вместо с твърд код.
- Как да изградиш Excel файл с баланс и отчет за приходите и разходите с openpyxl.
- Как контролен ред хваща несходен баланс, преди някой да го е прочел.
- Защо парите се пазят точно (
NUMERIC/Decimal), а не катоfloat. - Как да предадеш чернова на счетоводителя така, че отговорността да остане при човека.
02Преди да започнеш
- Работещ PostgreSQL с оборотна ведомост: затварящи салда по сметки за декември (виж стъпка 3 за измислените данни).
- Python 3 и възможност да правиш виртуална среда (
venv). - Microsoft Excel, с който да отвориш файла. Отваряй готовите файлове само с него.
- Счетоводител, който ще прегледа и подпише. Това е условието за целия урок, не любезност.
- Само измислени данни при упражнението. Истински ведомости не се качват никъде и не влизат в хранилище с код.
| Пакет | Версия (проверено 01.10.2026) | За какво е |
|---|---|---|
| openpyxl | 3.1.5 (PyPI, 28.06.2024) | чете и пише .xlsx — ползваме го |
| XlsxWriter | 3.2.9 | само пише .xlsx; алтернатива при сложно оформление |
| psycopg | 3.3.6 | връзка с PostgreSQL от Python |
03Стъпки
-
Какво е чернова и кой подписва
Защо това е първо? Отчетът има правна тежест, а скриптът няма как да знае особеностите на твоята фирма. Затова го ползваме така: скриптът чете числата, които вече са в ведомостта, пренарежда ги и показва дали излизат; човекът-счетоводител решава коя сметка в кой ред отива, поправя и подписва.
✅ПравилоИнструментът не подава нищо никъде и не твърди, че отчетът е по образец. Всяка страница на файла носи „ЧЕРНОВА“. Човекът одобрява, преди да се прави каквото и да било с реален ефект.📜Какво казва Законът за счетоводството (сверено дословно)Чл. 29, ал. 1: „Годишният финансов отчет за всички предприятия се състои най-малко от счетоводен баланс, отчет за приходите и разходите и приложение.“
Чл. 29, ал. 4: „Годишният финансов отчет на микропредприятията може да се състои само от съкратен баланс по раздели и съкратен отчет за приходите и разходите.“
Чл. 29, ал. 6: „Годишният финансов отчет на малките предприятия може да се състои от съкратен баланс по раздели и групи, съкратен отчет за приходите и разходите и приложение.“
Чл. 5, ал. 1 (в сила от 01.01.2026): „Счетоводните документи в предприятията се съставят на български език с арабски цифри и в евро.“
Затова черновата има баланс и ОПР, а приложението остава за счетоводителя. Към коя категория е фирмата (чл. 19) и кой вариант на отчета се прилага — решава той. Текстът е от консолидираната редакция в КиК Инфо, прочетена на 01.10.2026; официалният източник е Държавен вестник.
-
Подготви средата
bashpython3 -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) със стойността от твоята инсталация — вместо шаблон:<връзка-към-базата>. -
Таблица и измислени данни
Скриптът чете затварящите салда. Парите са
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 е работа за счетоводителя. -
Съпоставяне: сметка → ред
Защо списък, а не твърд код? Когато счетоводителят промени реда, променяш един ред в списъка, не логиката. Скриптът сумира всички сметки, чийто код започва с даден префикс.
Префикс Ред в чернова Част Знак 20 Дълготрайни материални активи Активи дебит 30 Материални запаси Активи дебит 41 Вземания от клиенти Активи дебит 50 Парични средства Активи дебит 10, 12 Основен капитал; натрупан резултат Капитал кредит 15, 40 Заеми; задължения към доставчици Задължения кредит 70 / 60 Приходи от продажби / разходи за дейността ОПР кредит / дебит Резултатът за годината е приходите минус разходите (клас 7 минус клас 6 в този пример) и влиза в капитала — иначе балансът няма как да излезе.
-
Скриптът
Три малки функции:
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")) -
Пусни и провери
bashpython3 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).
💡Защо контролът е в самия файлТака счетоводителят вижда проверката още при първото отваряне, а несходна чернова не се подава мълчаливо нататък. -
Пояснителни бележки (по желание)
Локален езиков модел може да предложи чернови на пояснителни бележки — например „приходите растат с 20% спрямо предходната година“. Идеята работи само ако човек чете и редактира всяка бележка; моделът не знае политиките на фирмата. 🔒 локално ⚠️ Тази стъпка не сме пускали.
-
Предай на счетоводителя
- Файлът е с измислени данни, докато упражняваш; при истински данни — само по сигурен канал и без хранилище с код.
- Покажи му списъка със съпоставянето (
BALANCE_MAP,PNL_MAP) — той го поправя, не ти. - Той сверява със своя сметкоплан и с официалния образец, поправя, подписва и решава за подаването. Скриптът не подава нищо.
04Проверка
- Таблицата
trial_balanceима данни за две години и сумата на дебитите е равна на сумата на кредитите. gfo_draft.pyзавършва и печата разлика 0.- Excel файлът има листове „Баланс“, „ОПР“, „Бележки“ и всеки носи „ЧЕРНОВА“.
- Редът „Статус“ е „СХОДЕН“ и за двете колони.
- Връзката към базата е в променлива на средата, не в кода.
- Счетоводителят е видял съпоставянето, прегледал е и е подписал; инструментът не е подавал нищо.
Тест
1. Каква е ролята на скрипта в урока?
2. За какво е контролният ред „активи − (капитал + задължения)“?
3. Защо парите са NUMERIC в базата и Decimal в Python?
4. Защо в Excel се записват стойности, а не формули?
05Какво следва
06Източници
- openpyxl в PyPI · документация 🔒 локално — версия 3.1.5.
- XlsxWriter в PyPI · документация — версия 3.2.9.
- psycopg в PyPI · документация — версия 3.3.6.
- PostgreSQL: числови типове —
NUMERICза точни суми. - ЕЦБ: България в еврозоната — 1 януари 2026 г., курс 1 EUR = 1,95583 BGN.
- Закон за счетоводството — консолидиран текст (КиК Инфо) — сверени дословно чл. 5, ал. 1 и чл. 29, ал. 1, 4 и 6 (01.10.2026). ⚠️ НСС 1 и праговете по чл. 19 не са проверявани; официалният текст е в Държавен вестник.