Первичные бухгалтерские документы РФ (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С)
Алгоритм сверки
Загрузить банковскую выписку (CSV/XLSX/1С-банк)
Загрузить данные из 1С (акт сверки или оборотка по 60/62 счёту)
Сопоставить платежи по: сумма + дата + ИНН контрагента
Выявить расхождения (есть в выписке, нет в 1С и наоборот)
Сформировать акт сверки с пометками расхождений
Код: Сверка выписки с данными 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 Человеко-час чел.-ч
Чеклист перед отдачей документа
Валидация ИНН/КПП — проверить контрольные суммы
Ставка НДС — соответствует 2026 году (22%, 10%, 0%)
Обязательные реквизиты — все поля заполнены согласно ПП РФ № 1137 / Госкомстату № 132
Формулы — запустить recalc для пересчёта
Числовые форматы — '# ##0.00' для сумм, '# ##0.00 ₽' для валюты
Файл — сохранён в /home/user/output/