⚖️

Первичные бухгалтерские документы (РФ)

Схемы и паттерны Python-кода для генерации первичных бухгалтерских документов в Excel — счёт-фактура, ТОРГ-12, УПД, акт сверки — актуально на февраль 2026

Системный промпт

Первичные бухгалтерские документы РФ (v1.0)

Дополняет skill excel_operations_ru для формул и форматирования. Здесь — схемы и код для генерации первичных бухгалтерских документов.


Ставки НДС (актуально на февраль 2026)

СтавкаПрименение
22%Стандартная (с 01.01.2026)
10%Продовольствие, детские товары, медикаменты, периодика
0%Экспорт, международные перевозки, космос
Без НДСОперации, не облагаемые НДС (ст. 149 НК РФ)
VAT_RATES = {
    22: "22%",
    10: "10%",
    0: "0%",
    None: "Без НДС"
}

Валидация ИНН и КПП

import re

def validate_inn(inn: str) -> tuple[bool, str]:
    """Проверка ИНН (10 или 12 цифр)."""
    inn = inn.strip().replace(" ", "")
    if not re.match(r"^\d{10}$|^\d{12}$", inn):
        return False, "ИНН должен содержать 10 (ЮЛ) или 12 (ИП/ФЛ) цифр"

    # Контрольные суммы
    if len(inn) == 10:
        weights = [2, 4, 10, 3, 5, 9, 4, 6, 8]
        check = sum(int(inn[i]) * weights[i] for i in range(9)) % 11 % 10
        if check != int(inn[9]):
            return False, "Неверная контрольная сумма ИНН"
    elif len(inn) == 12:
        w1 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
        w2 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
        c1 = sum(int(inn[i]) * w1[i] for i in range(10)) % 11 % 10
        c2 = sum(int(inn[i]) * w2[i] for i in range(11)) % 11 % 10
        if c1 != int(inn[10]) or c2 != int(inn[11]):
            return False, "Неверная контрольная сумма ИНН"

    return True, "OK"

def validate_kpp(kpp: str) -> tuple[bool, str]:
    """Проверка КПП (9 цифр)."""
    kpp = kpp.strip().replace(" ", "")
    if not re.match(r"^\d{9}$", kpp):
        return False, "КПП должен содержать 9 цифр"
    return True, "OK"

1. Счёт-фактура

Обязательные реквизиты (ПП РФ № 1137)

Шапка документа

ПолеОписаниеПример
номерПорядковый номер СФ"СФ-001/2026"
датаДата выставления"15.02.2026"
исправление_номер№ исправления (0 = первичный)"0"
исправление_датаДата исправления"-"

Продавец

ПолеОписаниеПример
продавец_наименованиеПолное наименование ЮЛ"ООО «Поставщик»"
продавец_иннИНН (10 цифр)"7701234567"
продавец_кппКПП (9 цифр)"770101001"
продавец_адресЮридический адрес"г. Москва, ул. Ленина, д. 1"

Покупатель

ПолеОписаниеПример
покупатель_наименованиеПолное наименование ЮЛ"АО «Покупатель»"
покупатель_иннИНН"7709876543"
покупатель_кппКПП"770901001"
покупатель_адресЮридический адрес"г. Москва, ул. Мира, д. 10"

Грузоотправитель / грузополучатель

ПолеОписаниеПример
грузоотправительНаименование и адрес"ООО «Склад», г. Москва..." или "он же"
грузополучательНаименование и адрес"АО «Покупатель», г. Москва..."

Платёжные документы

ПолеОписаниеПример
платежный_документНомер и дата платёжки"№ 123 от 10.02.2026"

Табличная часть (строки)

ПолеОписаниеПример
наименованиеНаименование товара/услуги"Товар А"
код_вида_товараКод по ТН ВЭД (при экспорте)"8471300000"
единица_кодКод по ОКЕИ"796" (штуки)
единица_наимНаименование ед. изм."шт"
количествоКоличество100
ценаЦена без НДС1000.00
стоимость_без_ндсСумма без НДС100000.00
ндс_ставкаСтавка НДС"22%"
ндс_суммаСумма НДС22000.00
стоимость_с_ндсСумма с НДС122000.00
страна_кодКод страны происхождения"643" (Россия)
страна_наимКраткое наименование"РОССИЯ"
номер_тдНомер таможенной декларации"-"

Код генерации счёта-фактуры

from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter

def create_invoice(data: dict) -> str:
    """
    Генерирует счёт-фактуру в формате Excel.

    Args:
        data: {
            "номер": "СФ-001/2026",
            "дата": "15.02.2026",
            "продавец": {"наименование": "...", "инн": "...", "кпп": "...", "адрес": "..."},
            "покупатель": {"наименование": "...", "инн": "...", "кпп": "...", "адрес": "..."},
            "позиции": [{"наименование": "...", "ед": "шт", "кол": 10, "цена": 1000, "ндс": 22}, ...]
        }
    """
    wb = Workbook()
    ws = wb.active
    ws.title = "Счёт-фактура"

    BORDER = Border(*(Side(style="thin") for _ in range(4)))
    HEADER_FONT = Font(bold=True, size=10)
    TITLE_FONT = Font(bold=True, size=14)

    # Заголовок
    ws.merge_cells("A1:K1")
    ws["A1"] = f"СЧЁТ-ФАКТУРА № {data['номер']} от {data['дата']}"
    ws["A1"].font = TITLE_FONT
    ws["A1"].alignment = Alignment(horizontal="center")

    # Продавец
    ws["A3"] = "Продавец:"
    ws["B3"] = data["продавец"]["наименование"]
    ws["A4"] = "Адрес:"
    ws["B4"] = data["продавец"]["адрес"]
    ws["A5"] = "ИНН/КПП:"
    ws["B5"] = f'{data["продавец"]["инн"]} / {data["продавец"]["кпп"]}'

    # Покупатель
    ws["A7"] = "Покупатель:"
    ws["B7"] = data["покупатель"]["наименование"]
    ws["A8"] = "Адрес:"
    ws["B8"] = data["покупатель"]["адрес"]
    ws["A9"] = "ИНН/КПП:"
    ws["B9"] = f'{data["покупатель"]["инн"]} / {data["покупатель"]["кпп"]}'

    # Шапка таблицы
    headers = ["№", "Наименование", "Ед.", "Кол-во", "Цена", "Сумма без НДС",
               "Ставка НДС", "Сумма НДС", "Сумма с НДС"]
    widths = [5, 30, 8, 10, 12, 15, 12, 12, 15]

    for c, (h, w) in enumerate(zip(headers, widths), 1):
        cell = ws.cell(11, c, h)
        cell.font = HEADER_FONT
        cell.border = BORDER
        cell.alignment = Alignment(horizontal="center", wrap_text=True)
        ws.column_dimensions[get_column_letter(c)].width = w

    # Позиции
    row = 12
    for i, pos in enumerate(data["позиции"], 1):
        qty = pos["кол"]
        price = pos["цена"]
        vat_rate = pos.get("ндс", 22)
        sum_no_vat = qty * price
        vat_sum = sum_no_vat * vat_rate / 100 if vat_rate else 0
        sum_with_vat = sum_no_vat + vat_sum

        values = [i, pos["наименование"], pos.get("ед", "шт"), qty, price,
                  sum_no_vat, f"{vat_rate}%" if vat_rate else "Без НДС", vat_sum, sum_with_vat]
        for c, v in enumerate(values, 1):
            cell = ws.cell(row, c, v)
            cell.border = BORDER
            if c >= 4:
                cell.number_format = '# ##0.00'
        row += 1

    # Итого
    last_data_row = row - 1
    ws.cell(row, 5, "ИТОГО:").font = HEADER_FONT
    ws.cell(row, 6, f"=SUM(F12:F{last_data_row})")
    ws.cell(row, 8, f"=SUM(H12:H{last_data_row})")
    ws.cell(row, 9, f"=SUM(I12:I{last_data_row})")
    for c in [6, 8, 9]:
        ws.cell(row, c).number_format = '# ##0.00'
        ws.cell(row, c).font = HEADER_FONT

    # Подписи
    row += 3
    ws.cell(row, 1, "Руководитель организации")
    ws.cell(row, 5, "_____________")
    ws.cell(row, 6, "________________")
    ws.cell(row + 2, 1, "Главный бухгалтер")
    ws.cell(row + 2, 5, "_____________")
    ws.cell(row + 2, 6, "________________")

    output_path = cw.output_path("invoice.xlsx")
    wb.save(output_path)
    return output_path

2. ТОРГ-12 (Товарная накладная)

Обязательные реквизиты (Постановление Госкомстата № 132)

Шапка документа

ПолеОписаниеПример
номерНомер накладной"ТН-001"
датаДата составления"15.02.2026"
код_формы_окудКод по ОКУД"0330212"
код_организации_окпоКод по ОКПО"12345678"

Участники

ПолеОписаниеПример
грузоотправительПолное наименование, адрес, ИНН/КПП"ООО «Склад», г. Москва..., ИНН 7701234567"
структурное_подразделениеОтдел/склад"Склад № 1"
грузополучательПолное наименование, адрес"АО «Покупатель», г. Москва..."
поставщикПолное наименование, адрес, банк. реквизиты"ООО «Поставщик»..."
плательщикНаименование и реквизиты"АО «Покупатель»..."
основаниеДоговор, заказ, наряд"Договор № 123 от 01.01.2026"

Табличная часть

ПолеОписаниеПример
номер_п_пПорядковый номер1
наименованиеНаименование товара"Товар А"
код_товараВнутренний код"АРТ-001"
единица_кодКод по ОКЕИ"796"
единица_наимНаименование ед."шт"
вид_упаковкиТип упаковки"коробка"
количество_в_упаковкеКол-во в одном месте10
количество_местЧисло мест (упаковок)5
масса_бруттоМасса с упаковкой, кг25.5
количествоОбщее количество товара50
ценаЦена за единицу1000.00
сумма_без_ндсСумма без НДС50000.00
ндс_ставкаСтавка НДС"22%"
ндс_суммаСумма НДС11000.00
сумма_с_ндсСумма с НДС61000.00

Подписи

ПолеОписаниеПример
отпустил_должностьДолжность"Кладовщик"
отпустил_фиоФИО"Иванов И.И."
принял_должностьДолжность"Водитель-экспедитор"
принял_фиоФИО"Петров П.П."
груз_принял_фиоФИО грузополучателя"Сидоров С.С."

Код генерации ТОРГ-12

from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter

def create_torg12(data: dict) -> str:
    """
    Генерирует товарную накладную ТОРГ-12 в формате Excel.

    Args:
        data: {
            "номер": "ТН-001",
            "дата": "15.02.2026",
            "грузоотправитель": {"наименование": "...", "адрес": "...", "инн": "..."},
            "грузополучатель": {"наименование": "...", "адрес": "..."},
            "поставщик": {"наименование": "...", "адрес": "...", "инн": "...", "кпп": "..."},
            "плательщик": {"наименование": "...", "адрес": "..."},
            "основание": "Договор № 123 от 01.01.2026",
            "позиции": [
                {"наименование": "...", "ед": "шт", "кол": 50, "цена": 1000,
                 "масса": 25.5, "места": 5, "ндс": 22}, ...
            ]
        }
    """
    wb = Workbook()
    ws = wb.active
    ws.title = "ТОРГ-12"

    BORDER = Border(*(Side(style="thin") for _ in range(4)))
    BOLD = Font(bold=True)

    # Шапка — коды
    ws["J1"] = "Унифицированная форма № ТОРГ-12"
    ws["J2"] = "Утверждена постановлением Госкомстата"
    ws["J3"] = "России от 25.12.98 № 132"

    ws["K5"] = "Код"
    ws["L5"] = "Форма по ОКУД"
    ws["M5"] = "0330212"
    ws.cell(5, 13).border = BORDER

    # Заголовок
    ws.merge_cells("A7:M7")
    ws["A7"] = f"ТОВАРНАЯ НАКЛАДНАЯ № {data['номер']} от {data['дата']}"
    ws["A7"].font = Font(bold=True, size=14)
    ws["A7"].alignment = Alignment(horizontal="center")

    # Участники
    fields = [
        ("Грузоотправитель:", data["грузоотправитель"]["наименование"]),
        ("Грузополучатель:", data["грузополучатель"]["наименование"]),
        ("Поставщик:", data["поставщик"]["наименование"]),
        ("Плательщик:", data["плательщик"]["наименование"]),
        ("Основание:", data["основание"]),
    ]
    for i, (label, value) in enumerate(fields, 9):
        ws.cell(i, 1, label).font = BOLD
        ws.cell(i, 2, value)

    # Шапка таблицы
    headers = ["№", "Наименование", "Код", "Ед.", "Вид уп.", "Мест",
               "Масса брутто", "Кол-во", "Цена", "Сумма без НДС",
               "НДС %", "НДС сумма", "Сумма с НДС"]
    widths = [4, 25, 10, 6, 10, 6, 10, 10, 12, 14, 8, 12, 14]

    header_row = 15
    for c, (h, w) in enumerate(zip(headers, widths), 1):
        cell = ws.cell(header_row, c, h)
        cell.font = BOLD
        cell.border = BORDER
        cell.alignment = Alignment(horizontal="center", wrap_text=True)
        ws.column_dimensions[get_column_letter(c)].width = w

    # Позиции
    row = header_row + 1
    for i, pos in enumerate(data["позиции"], 1):
        qty = pos["кол"]
        price = pos["цена"]
        vat_rate = pos.get("ндс", 22)
        mass = pos.get("масса", 0)
        places = pos.get("места", 1)
        sum_no_vat = qty * price
        vat_sum = sum_no_vat * vat_rate / 100 if vat_rate else 0
        sum_with_vat = sum_no_vat + vat_sum

        values = [i, pos["наименование"], pos.get("код", "-"), pos.get("ед", "шт"),
                  pos.get("упаковка", "-"), places, mass, qty, price,
                  sum_no_vat, f"{vat_rate}%" if vat_rate else "-", vat_sum, sum_with_vat]
        for c, v in enumerate(values, 1):
            cell = ws.cell(row, c, v)
            cell.border = BORDER
            if c >= 6:
                cell.number_format = '# ##0.00'
        row += 1

    # Итого
    last_data = row - 1
    ws.cell(row, 8, "ИТОГО:").font = BOLD
    for col, letter in [(7, "G"), (8, "H"), (10, "J"), (12, "L"), (13, "M")]:
        ws.cell(row, col, f"=SUM({letter}{header_row+1}:{letter}{last_data})")
        ws.cell(row, col).font = BOLD
        ws.cell(row, col).number_format = '# ##0.00'

    # Подписи
    row += 3
    signatures = [
        ("Отпустил груз:", "отпустил"),
        ("Груз принял водитель:", "принял"),
        ("Груз получил:", "получатель"),
    ]
    for label, key in signatures:
        ws.cell(row, 1, label).font = BOLD
        ws.cell(row, 3, "_____________")
        ws.cell(row, 5, f"({data.get(key, {}).get('фио', '________________')})")
        row += 2

    output_path = cw.output_path("torg12.xlsx")
    wb.save(output_path)
    return output_path

3. УПД (Универсальный передаточный документ)

Статусы УПД

СтатусНазначениеОснование
1Счёт-фактура + первичный документПодтверждает НДС и передачу товара/услуги
2Только первичный документДля операций без НДС или освобождённых

Обязательные реквизиты (Письмо ФНС № ММВ-20-3/96@)

УПД объединяет реквизиты счёта-фактуры (верхняя часть) и ТОРГ-12/акта (нижняя часть).

Дополнительные поля УПД (к полям СФ)

ПолеОписаниеПример
статус1 или 2"1"
основание_передачиДоговор, заказ"Договор № 123 от 01.01.2026"
данные_о_транспортировкеТранспортная накладная"ТН № 456 от 15.02.2026"
дата_отгрузкиФактическая дата передачи"15.02.2026"
кто_отгрузилДолжность, ФИО"Кладовщик Иванов И.И."
дата_полученияДата получения покупателем"16.02.2026"
кто_получилДолжность, ФИО"Менеджер Петров П.П."
сведения_о_полученииИные сведения"Без замечаний"

Код генерации УПД

from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter

def create_upd(data: dict) -> str:
    """
    Генерирует УПД (универсальный передаточный документ) в формате Excel.

    Args:
        data: {
            "статус": 1,  # 1 = СФ + первичка, 2 = только первичка
            "номер": "УПД-001",
            "дата": "15.02.2026",
            "продавец": {"наименование": "...", "инн": "...", "кпп": "...", "адрес": "..."},
            "покупатель": {"наименование": "...", "инн": "...", "кпп": "...", "адрес": "..."},
            "грузоотправитель": "он же",
            "грузополучатель": "...",
            "основание": "Договор № 123 от 01.01.2026",
            "транспортировка": "ТН № 456 от 15.02.2026",
            "позиции": [{"наименование": "...", "ед": "шт", "кол": 10, "цена": 1000, "ндс": 22}, ...],
            "отгрузил": {"должность": "Кладовщик", "фио": "Иванов И.И."},
            "получил": {"должность": "Менеджер", "фио": "Петров П.П."},
            "дата_отгрузки": "15.02.2026",
            "дата_получения": "16.02.2026"
        }
    """
    wb = Workbook()
    ws = wb.active
    ws.title = "УПД"

    BORDER = Border(*(Side(style="thin") for _ in range(4)))
    BOLD = Font(bold=True)
    TITLE = Font(bold=True, size=12)
    GRAY_FILL = PatternFill("solid", start_color="F0F0F0")

    status = data.get("статус", 1)

    # Шапка — статус
    ws["A1"] = f"Статус: {status}"
    ws["A1"].font = BOLD
    ws["B1"] = "(1 – счёт-фактура и передаточный документ, 2 – передаточный документ)"

    # Заголовок
    ws.merge_cells("A3:K3")
    ws["A3"] = f"УНИВЕРСАЛЬНЫЙ ПЕРЕДАТОЧНЫЙ ДОКУМЕНТ № {data['номер']} от {data['дата']}"
    ws["A3"].font = TITLE
    ws["A3"].alignment = Alignment(horizontal="center")

    # === ЧАСТЬ 1: Реквизиты счёта-фактуры ===
    if status == 1:
        ws["A5"] = "Исправление:"
        ws["B5"] = "--- (первичный документ)"

    # Продавец / Покупатель
    row = 6
    for role, key in [("Продавец", "продавец"), ("Покупатель", "покупатель")]:
        ws.cell(row, 1, f"{role}:").font = BOLD
        ws.cell(row, 2, data[key]["наименование"])
        ws.cell(row + 1, 1, "Адрес:")
        ws.cell(row + 1, 2, data[key]["адрес"])
        ws.cell(row + 2, 1, "ИНН/КПП:")
        ws.cell(row + 2, 2, f'{data[key]["инн"]} / {data[key]["кпп"]}')
        row += 4

    # Грузоотправитель / грузополучатель
    ws.cell(row, 1, "Грузоотправитель:").font = BOLD
    ws.cell(row, 2, data.get("грузоотправитель", "он же"))
    row += 1
    ws.cell(row, 1, "Грузополучатель:").font = BOLD
    ws.cell(row, 2, data.get("грузополучатель", "-"))
    row += 2

    # Таблица
    headers = ["№", "Наименование", "Код", "Ед.", "Кол-во", "Цена",
               "Сумма без НДС", "НДС %", "НДС сумма", "Сумма с НДС", "Страна"]
    widths = [4, 25, 10, 6, 10, 12, 14, 8, 12, 14, 12]

    header_row = row
    for c, (h, w) in enumerate(zip(headers, widths), 1):
        cell = ws.cell(row, c, h)
        cell.font = BOLD
        cell.border = BORDER
        cell.fill = GRAY_FILL
        cell.alignment = Alignment(horizontal="center", wrap_text=True)
        ws.column_dimensions[get_column_letter(c)].width = w
    row += 1

    # Позиции
    for i, pos in enumerate(data["позиции"], 1):
        qty = pos["кол"]
        price = pos["цена"]
        vat_rate = pos.get("ндс", 22) if status == 1 else None
        sum_no_vat = qty * price
        vat_sum = sum_no_vat * vat_rate / 100 if vat_rate else 0
        sum_with_vat = sum_no_vat + vat_sum

        values = [i, pos["наименование"], pos.get("код", "-"), pos.get("ед", "шт"),
                  qty, price, sum_no_vat,
                  f"{vat_rate}%" if vat_rate else "Без НДС",
                  vat_sum if vat_rate else "-", sum_with_vat,
                  pos.get("страна", "РОССИЯ")]
        for c, v in enumerate(values, 1):
            cell = ws.cell(row, c, v)
            cell.border = BORDER
            if c >= 5 and c != 8 and c != 11:
                cell.number_format = '# ##0.00'
        row += 1

    # Итого
    last_data = row - 1
    ws.cell(row, 5, "ИТОГО:").font = BOLD
    for col, letter in [(5, "E"), (7, "G"), (9, "I"), (10, "J")]:
        ws.cell(row, col, f"=SUM({letter}{header_row+1}:{letter}{last_data})")
        ws.cell(row, col).font = BOLD
        ws.cell(row, col).number_format = '# ##0.00'
    row += 2

    # === ЧАСТЬ 2: Первичный документ ===
    ws.cell(row, 1, "ОСНОВАНИЕ ПЕРЕДАЧИ:").font = BOLD
    ws.cell(row, 2, data.get("основание", "-"))
    row += 1
    ws.cell(row, 1, "Данные о транспортировке:").font = BOLD
    ws.cell(row, 2, data.get("транспортировка", "-"))
    row += 2

    # Подписи
    ws.merge_cells(f"A{row}:E{row}")
    ws.cell(row, 1, "ОТГРУЗКА").font = BOLD
    ws.cell(row, 1).fill = GRAY_FILL
    ws.merge_cells(f"F{row}:K{row}")
    ws.cell(row, 6, "ПОЛУЧЕНИЕ").font = BOLD
    ws.cell(row, 6).fill = GRAY_FILL
    row += 1

    # Отгрузил
    ws.cell(row, 1, "Дата отгрузки:")
    ws.cell(row, 2, data.get("дата_отгрузки", "-"))
    ws.cell(row, 6, "Дата получения:")
    ws.cell(row, 7, data.get("дата_получения", "-"))
    row += 1

    ws.cell(row, 1, "Кто отгрузил:")
    ws.cell(row, 2, f'{data.get("отгрузил", {}).get("должность", "")} {data.get("отгрузил", {}).get("фио", "")}')
    ws.cell(row, 6, "Кто получил:")
    ws.cell(row, 7, f'{data.get("получил", {}).get("должность", "")} {data.get("получил", {}).get("фио", "")}')
    row += 2

    # Подписи
    ws.cell(row, 1, "Подпись: _____________")
    ws.cell(row, 6, "Подпись: _____________")
    row += 1
    ws.cell(row, 1, "М.П.")
    ws.cell(row, 6, "М.П.")

    output_path = cw.output_path("upd.xlsx")
    wb.save(output_path)
    return output_path

4. Акт сверки взаимных расчётов

Обязательные реквизиты

ПолеОписаниеПример
период_сНачало периода"01.01.2026"
период_поКонец периода"31.03.2026"
сторона_1Наименование первой стороны"ООО «Поставщик»"
сторона_2Наименование второй стороны"АО «Покупатель»"
сальдо_входящееСальдо на начало периода100000.00
операцииСписок операций (дата, документ, дебет, кредит)[...]
сальдо_исходящееСальдо на конец периода (расчётное)30000.00

Код генерации акта сверки

from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter

def create_reconciliation_act(data: dict) -> str:
    """
    Генерирует акт сверки взаимных расчётов в формате Excel.

    Args:
        data: {
            "период_с": "01.01.2026",
            "период_по": "31.03.2026",
            "сторона_1": {"наименование": "ООО «Поставщик»", "инн": "7701234567"},
            "сторона_2": {"наименование": "АО «Покупатель»", "инн": "7709876543"},
            "сальдо_входящее": 100000,  # положительное = должен сторона_2
            "операции": [
                {"дата": "15.01.2026", "документ": "Накладная №1", "дебет": 50000, "кредит": 0},
                {"дата": "20.01.2026", "документ": "Платёж №1", "дебет": 0, "кредит": 80000},
                ...
            ]
        }
    """
    wb = Workbook()
    ws = wb.active
    ws.title = "Акт сверки"

    BORDER = Border(*(Side(style="thin") for _ in range(4)))
    BOLD = Font(bold=True)
    TITLE = Font(bold=True, size=14)

    # Заголовок
    ws.merge_cells("A1:E1")
    ws["A1"] = "АКТ СВЕРКИ ВЗАИМНЫХ РАСЧЁТОВ"
    ws["A1"].font = TITLE
    ws["A1"].alignment = Alignment(horizontal="center")

    # Период
    ws["A3"] = f"за период с {data['период_с']} по {data['период_по']}"
    ws["A3"].alignment = Alignment(horizontal="center")
    ws.merge_cells("A3:E3")

    # Стороны
    ws["A5"] = "Между:"
    ws["B5"] = data["сторона_1"]["наименование"]
    ws["A6"] = "и:"
    ws["B6"] = data["сторона_2"]["наименование"]

    # Шапка таблицы
    headers = ["Дата", "Документ", "Дебет", "Кредит", "Сальдо"]
    widths = [12, 30, 15, 15, 15]

    header_row = 8
    for c, (h, w) in enumerate(zip(headers, widths), 1):
        cell = ws.cell(header_row, c, h)
        cell.font = BOLD
        cell.border = BORDER
        cell.alignment = Alignment(horizontal="center")
        ws.column_dimensions[get_column_letter(c)].width = w

    # Сальдо входящее
    row = header_row + 1
    ws.cell(row, 1, data['период_с'])
    ws.cell(row, 2, "Сальдо входящее").font = BOLD
    ws.cell(row, 3, data['сальдо_входящее'] if data['сальдо_входящее'] > 0 else 0)
    ws.cell(row, 4, -data['сальдо_входящее'] if data['сальдо_входящее'] < 0 else 0)
    ws.cell(row, 5, f"=C{row}-D{row}")
    for c in range(1, 6):
        ws.cell(row, c).border = BORDER
    for c in [3, 4, 5]:
        ws.cell(row, c).number_format = '# ##0.00'
    row += 1

    # Операции
    first_op_row = row
    for op in data["операции"]:
        ws.cell(row, 1, op["дата"])
        ws.cell(row, 2, op["документ"])
        ws.cell(row, 3, op.get("дебет", 0) or 0)
        ws.cell(row, 4, op.get("кредит", 0) or 0)
        ws.cell(row, 5, f"=E{row-1}+C{row}-D{row}")
        for c in range(1, 6):
            ws.cell(row, c).border = BORDER
        for c in [3, 4, 5]:
            ws.cell(row, c).number_format = '# ##0.00'
        row += 1

    last_op_row = row - 1

    # Обороты
    ws.cell(row, 2, "Обороты за период:").font = BOLD
    ws.cell(row, 3, f"=SUM(C{first_op_row}:C{last_op_row})")
    ws.cell(row, 4, f"=SUM(D{first_op_row}:D{last_op_row})")
    for c in [2, 3, 4]:
        ws.cell(row, c).border = BORDER
        ws.cell(row, c).font = BOLD
    ws.cell(row, 3).number_format = '# ##0.00'
    ws.cell(row, 4).number_format = '# ##0.00'
    row += 1

    # Сальдо исходящее
    ws.cell(row, 2, "Сальдо исходящее:").font = BOLD
    ws.cell(row, 5, f"=E{last_op_row}").font = BOLD
    ws.cell(row, 5).number_format = '# ##0.00'
    for c in [2, 5]:
        ws.cell(row, c).border = BORDER
    row += 2

    # Заключение
    ws.cell(row, 1, "По данным")
    ws.cell(row, 2, data["сторона_1"]["наименование"])
    ws.cell(row, 4, "задолженность составляет:")
    ws.cell(row, 5, f"=E{row-2}")
    ws.cell(row, 5).number_format = '# ##0.00 ₽'
    row += 3

    # Подписи
    ws.merge_cells(f"A{row}:B{row}")
    ws.cell(row, 1, f"От {data['сторона_1']['наименование']}:")
    ws.merge_cells(f"D{row}:E{row}")
    ws.cell(row, 4, f"От {data['сторона_2']['наименование']}:")
    row += 2

    ws.cell(row, 1, "Подпись: _____________")
    ws.cell(row, 4, "Подпись: _____________")
    row += 1
    ws.cell(row, 1, "М.П.")
    ws.cell(row, 4, "М.П.")
    row += 2
    ws.cell(row, 1, "ФИО: ________________")
    ws.cell(row, 4, "ФИО: ________________")

    output_path = cw.output_path("reconciliation_act.xlsx")
    wb.save(output_path)
    return output_path


5. Автоматическая сверка с контрагентами (выписка + 1С)

Алгоритм сверки

  1. Загрузить банковскую выписку (CSV/XLSX/1С-банк)
  2. Загрузить данные из 1С (акт сверки или оборотка по 60/62 счёту)
  3. Сопоставить платежи по: сумма + дата + ИНН контрагента
  4. Выявить расхождения (есть в выписке, нет в 1С и наоборот)
  5. Сформировать акт сверки с пометками расхождений

Код: Сверка выписки с данными 1С

import subprocess
subprocess.run(['pip', 'install', 'pandas', 'openpyxl'], capture_output=True)

import pandas as pd
from datetime import datetime

def reconcile_bank_vs_1c(bank_df: pd.DataFrame, accounting_df: pd.DataFrame,
                          counterparty_inn: str, tolerance_days: int = 3) -> dict:
    """
    Сверка банковской выписки с данными бухучёта (1С) по контрагенту.

    Args:
        bank_df: выписка банка с колонками: дата, сумма, инн, назначение
        accounting_df: данные 1С с колонками: дата, сумма, документ, тип (дебет/кредит)
        counterparty_inn: ИНН контрагента для сверки
        tolerance_days: допуск по дате (дней)

    Returns:
        dict с результатами сверки
    """
    # Фильтруем выписку по контрагенту
    bank = bank_df[bank_df["инн"].astype(str) == str(counterparty_inn)].copy()
    bank["дата"] = pd.to_datetime(bank["дата"], dayfirst=True)
    bank["matched"] = False

    acct = accounting_df.copy()
    acct["дата"] = pd.to_datetime(acct["дата"], dayfirst=True)
    acct["matched"] = False

    matched_pairs = []
    bank_only = []
    acct_only = []

    # Сопоставление по сумме и дате (с допуском)
    for bi, brow in bank.iterrows():
        best_match = None
        best_diff = None
        for ai, arow in acct[~acct["matched"]].iterrows():
            if abs(float(brow["сумма"]) - float(arow["сумма"])) < 0.01:
                day_diff = abs((brow["дата"] - arow["дата"]).days)
                if day_diff <= tolerance_days:
                    if best_diff is None or day_diff < best_diff:
                        best_match = ai
                        best_diff = day_diff

        if best_match is not None:
            bank.at[bi, "matched"] = True
            acct.at[best_match, "matched"] = True
            matched_pairs.append({
                "банк_дата": brow["дата"].strftime("%d.%m.%Y"),
                "банк_сумма": float(brow["сумма"]),
                "банк_назначение": brow.get("назначение", ""),
                "1с_дата": acct.at[best_match, "дата"].strftime("%d.%m.%Y"),
                "1с_сумма": float(acct.at[best_match, "сумма"]),
                "1с_документ": acct.at[best_match, "документ"],
                "разница_дней": best_diff,
            })

    # Несопоставленные
    for bi, brow in bank[~bank["matched"]].iterrows():
        bank_only.append({
            "дата": brow["дата"].strftime("%d.%m.%Y"),
            "сумма": float(brow["сумма"]),
            "назначение": brow.get("назначение", ""),
        })

    for ai, arow in acct[~acct["matched"]].iterrows():
        acct_only.append({
            "дата": arow["дата"].strftime("%d.%m.%Y"),
            "сумма": float(arow["сумма"]),
            "документ": arow["документ"],
        })

    return {
        "контрагент_инн": counterparty_inn,
        "совпадений": len(matched_pairs),
        "только_в_банке": len(bank_only),
        "только_в_1с": len(acct_only),
        "matched": matched_pairs,
        "bank_only": bank_only,
        "acct_only": acct_only,
        "итого_банк": float(bank["сумма"].sum()),
        "итого_1с": float(acct["сумма"].sum()),
        "расхождение": float(bank["сумма"].sum() - acct["сумма"].sum()),
    }

Код: Генерация акта сверки с расхождениями

from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill

def create_reconciliation_with_discrepancies(recon_result: dict, party1: dict, party2: dict,
                                              period_from: str, period_to: str) -> str:
    """
    Генерирует акт сверки с пометками расхождений.

    Args:
        recon_result: результат reconcile_bank_vs_1c()
        party1: {"наименование": "...", "инн": "..."}
        party2: {"наименование": "...", "инн": "..."}
        period_from: "01.01.2026"
        period_to: "31.03.2026"
    """
    wb = Workbook()
    ws = wb.active
    ws.title = "Акт сверки"

    BORDER = Border(*(Side(style="thin") for _ in range(4)))
    BOLD = Font(bold=True)
    TITLE = Font(bold=True, size=14)
    RED_FILL = PatternFill("solid", start_color="FFCCCC")
    GREEN_FILL = PatternFill("solid", start_color="CCFFCC")

    # Заголовок
    ws.merge_cells("A1:G1")
    ws["A1"] = "АКТ СВЕРКИ ВЗАИМНЫХ РАСЧЁТОВ"
    ws["A1"].font = TITLE
    ws["A1"].alignment = Alignment(horizontal="center")

    ws.merge_cells("A2:G2")
    ws["A2"] = f"за период с {period_from} по {period_to}"
    ws["A2"].alignment = Alignment(horizontal="center")

    ws["A4"] = "Между:"
    ws["B4"] = party1["наименование"]
    ws["A5"] = "и:"
    ws["B5"] = party2["наименование"]

    # Секция 1: Совпавшие операции
    row = 7
    ws.cell(row, 1, "СОВПАВШИЕ ОПЕРАЦИИ").font = BOLD
    ws.cell(row, 1).fill = GREEN_FILL
    row += 1

    headers = ["Дата (банк)", "Сумма", "Назначение", "Дата (1С)", "Документ 1С", "Разница дней", "Статус"]
    widths = [12, 14, 30, 12, 25, 12, 12]
    for c, (h, w) in enumerate(zip(headers, widths), 1):
        cell = ws.cell(row, c, h)
        cell.font = BOLD
        cell.border = BORDER
        ws.column_dimensions[chr(64 + c)].width = w
    row += 1

    for m in recon_result["matched"]:
        values = [m["банк_дата"], m["банк_сумма"], m["банк_назначение"],
                  m["1с_дата"], m["1с_документ"], m["разница_дней"], "OK"]
        for c, v in enumerate(values, 1):
            cell = ws.cell(row, c, v)
            cell.border = BORDER
            if c == 2:
                cell.number_format = '# ##0.00'
        row += 1

    # Секция 2: Только в банке
    row += 1
    ws.cell(row, 1, "ТОЛЬКО В БАНКОВСКОЙ ВЫПИСКЕ (нет в 1С)").font = BOLD
    ws.cell(row, 1).fill = RED_FILL
    row += 1

    for item in recon_result["bank_only"]:
        ws.cell(row, 1, item["дата"]).border = BORDER
        ws.cell(row, 2, item["сумма"]).border = BORDER
        ws.cell(row, 2).number_format = '# ##0.00'
        ws.cell(row, 3, item["назначение"]).border = BORDER
        ws.cell(row, 7, "НЕТ В 1С").font = Font(color="FF0000", bold=True)
        row += 1

    # Секция 3: Только в 1С
    row += 1
    ws.cell(row, 1, "ТОЛЬКО В 1С (нет в банковской выписке)").font = BOLD
    ws.cell(row, 1).fill = RED_FILL
    row += 1

    for item in recon_result["acct_only"]:
        ws.cell(row, 4, item["дата"]).border = BORDER
        ws.cell(row, 2, item["сумма"]).border = BORDER
        ws.cell(row, 2).number_format = '# ##0.00'
        ws.cell(row, 5, item["документ"]).border = BORDER
        ws.cell(row, 7, "НЕТ В БАНКЕ").font = Font(color="FF0000", bold=True)
        row += 1

    # Итоги
    row += 2
    ws.cell(row, 1, "ИТОГИ СВЕРКИ").font = Font(bold=True, size=12)
    row += 1
    ws.cell(row, 1, "Совпавших операций:")
    ws.cell(row, 2, recon_result["совпадений"])
    row += 1
    ws.cell(row, 1, "Только в банке:")
    ws.cell(row, 2, recon_result["только_в_банке"])
    row += 1
    ws.cell(row, 1, "Только в 1С:")
    ws.cell(row, 2, recon_result["только_в_1с"])
    row += 1
    ws.cell(row, 1, "Итого по банку:").font = BOLD
    ws.cell(row, 2, recon_result["итого_банк"]).number_format = '# ##0.00'
    row += 1
    ws.cell(row, 1, "Итого по 1С:").font = BOLD
    ws.cell(row, 2, recon_result["итого_1с"]).number_format = '# ##0.00'
    row += 1
    ws.cell(row, 1, "Расхождение:").font = Font(bold=True, color="FF0000")
    ws.cell(row, 2, recon_result["расхождение"])
    ws.cell(row, 2).number_format = '# ##0.00'
    ws.cell(row, 2).font = Font(bold=True, color="FF0000")

    # Подписи
    row += 3
    ws.cell(row, 1, f"От {party1['наименование']}:")
    ws.cell(row, 5, f"От {party2['наименование']}:")
    row += 2
    ws.cell(row, 1, "Подпись: _____________")
    ws.cell(row, 5, "Подпись: _____________")
    row += 1
    ws.cell(row, 1, "М.П.")
    ws.cell(row, 5, "М.П.")

    output_path = cw.output_path("reconciliation_with_discrepancies.xlsx")
    wb.save(output_path)
    return output_path

Коды ОКЕИ (единицы измерения)

КодНаименованиеСокращение
796Штукашт
166Килограммкг
168Тоннат
112Литрл
006Метрм
055Квадратный метрм²
113Кубический метрм³
876Условная единицаусл. ед
383Рубльруб
384Тысяча рублейтыс. руб
642Часч
657Человеко-часчел.-ч

Чеклист перед отдачей документа

  1. Валидация ИНН/КПП — проверить контрольные суммы
  2. Ставка НДС — соответствует 2026 году (22%, 10%, 0%)
  3. Обязательные реквизиты — все поля заполнены согласно ПП РФ № 1137 / Госкомстату № 132
  4. Формулы — запустить recalc для пересчёта
  5. Числовые форматы — '# ##0.00' для сумм, '# ##0.00 ₽' для валюты
  6. Файл — сохранён в /home/user/output/
Категория
⚖️ Договоры и право
Платформа
Сам Решу

Попробуйте этот навык

Зарегистрируйтесь и используйте навык «Первичные бухгалтерские документы (РФ)» бесплатно.