# Банковские выписки и формат 1С-банк

Паттерны Python-кода для парсинга банковских выписок. Файлы доступны в /tmp/.

---

## Формат 1CClientBankExchange

Стандартный текстовый формат обмена между 1С:Предприятие и системами «Клиент-Банк». Кодировка: Windows-1251.

### Структура файла

```
1CClientBankExchange
ВерсияФормата=1.03
Кодировка=Windows
Отправитель=Бухгалтерия предприятия
Получатель=Банк
ДатаСоздания=20.02.2026
ВремяСоздания=10:30:00
ДатаНачала=01.02.2026
ДатаКонца=20.02.2026
РасsчСчёт=40702810123456789012
СекцияРас662Счёт=
НачsальныйОстаток=1000000.00
ВсегоПоступило=500000.00
ВсегоСписано=300000.00
КонечныйОстаток=1200000.00
СекцияРасчСчёт=Конец
СекцияДокумент=Платёжное поручение
Номер=123
Дата=15.02.2026
Сумма=50000.00
ПлательщикСчёт=40702810123456789012
ПлательщикИНН=7701234567
ПлательщикКПП=770101001
Плательщик1=ООО "Наша компания"
ПолучательСчёт=40702810987654321098
ПолучательИНН=7709876543
ПолучательКПП=770901001
Получатель1=АО "Контрагент"
ПолучательБИК=044525225
ПолучательБанк1=ПАО СБЕРБАНК
НазначениеПлатежа=Оплата по счёту №45 от 10.02.2026, в т.ч. НДС (22%) 9166.67
КонецДокумента
```

### Ключевые поля секции документа

| Поле | Описание | Пример |
|------|----------|--------|
| `Номер` | Номер платёжного поручения | `123` |
| `Дата` | Дата документа | `15.02.2026` |
| `Сумма` | Сумма платежа | `50000.00` |
| `ПлательщикСчёт` | Расчётный счёт плательщика | `40702810123456789012` |
| `ПлательщикИНН` | ИНН плательщика | `7701234567` |
| `ПлательщикКПП` | КПП плательщика | `770101001` |
| `Плательщик1` | Наименование плательщика | `ООО "Наша компания"` |
| `ПолучательСчёт` | Расчётный счёт получателя | `40702810987654321098` |
| `ПолучательИНН` | ИНН получателя | `7709876543` |
| `ПолучательБИК` | БИК банка получателя | `044525225` |
| `НазначениеПлатежа` | Назначение платежа | Текст |
| `ВидОплаты` | Вид операции (01, 02, 06...) | `01` |

---

# БЛОК 1: Парсинг формата 1CClientBankExchange

```python
import re
import json
import pandas as pd

# Чтение файла в кодировке windows-1251
with open("/tmp/bank_statement.txt", "r", encoding="windows-1251") as f:
    content = f.read()

# Извлечение заголовка
header = {}
header_match = re.search(r"ДатаНачала=(.+)", content)
if header_match:
    header["дата_начала"] = header_match.group(1).strip()
header_match = re.search(r"ДатаКонца=(.+)", content)
if header_match:
    header["дата_конца"] = header_match.group(1).strip()

for field in ["НачальныйОстаток", "ВсегоПоступило", "ВсегоСписано", "КонечныйОстаток"]:
    m = re.search(rf"{field}=(.+)", content)
    if m:
        header[field] = float(m.group(1).strip().replace(",", "."))

account_match = re.search(r"РасsчСчёт=(\d+)", content)
if not account_match:
    account_match = re.search(r"РасчСчет=(\d+)", content)
if account_match:
    header["расч_счёт"] = account_match.group(1)

# Извлечение документов (платёжных поручений)
doc_blocks = re.findall(r"СекцияДокумент=(.+?)\nКонецДокумента", content, re.DOTALL)

documents = []
for block in doc_blocks:
    doc = {}
    for line in block.strip().split("\n"):
        if "=" in line:
            key, _, value = line.partition("=")
            doc[key.strip()] = value.strip()
    documents.append(doc)

# Преобразуем в таблицу
rows = []
for doc in documents:
    rows.append({
        "Номер": doc.get("Номер", ""),
        "Дата": doc.get("Дата", ""),
        "Сумма": float(doc.get("Сумма", "0").replace(",", ".")),
        "Плательщик": doc.get("Плательщик1", ""),
        "ПлательщикИНН": doc.get("ПлательщикИНН", ""),
        "ПлательщикСчёт": doc.get("ПлательщикСчёт", ""),
        "Получатель": doc.get("Получатель1", ""),
        "ПолучательИНН": doc.get("ПолучательИНН", ""),
        "ПолучательСчёт": doc.get("ПолучательСчёт", ""),
        "ПолучательБИК": doc.get("ПолучательБИК", ""),
        "Назначение": doc.get("НазначениеПлатежа", ""),
    })

df = pd.DataFrame(rows)
df.to_excel(cw.output_path("bank_statement.xlsx"), index=False)

print(f"Заголовок: {json.dumps(header, ensure_ascii=False, indent=2)}")
print(f"Документов: {len(documents)}")
print(f"Общая сумма: {df['Сумма'].sum():,.2f}")
```

---

# БЛОК 2: Парсинг CSV/XLSX выписок банков

## Сбербанк Бизнес Онлайн (CSV)

```python
import pandas as pd

# Сбербанк обычно: разделитель ";", кодировка windows-1251
df = pd.read_csv(
    "/tmp/sber_statement.csv",
    sep=";",
    encoding="windows-1251",
    decimal=",",
)

# Стандартные колонки Сбера
column_map = {
    "Номер документа": "номер",
    "Дата операции": "дата",
    "Дебет": "дебет",
    "Кредит": "кредит",
    "Наименование контрагента": "контрагент",
    "ИНН контрагента": "инн",
    "Назначение платежа": "назначение",
    "Номер счёта контрагента": "счёт",
}

# Переименовываем найденные колонки
rename = {}
for orig, target in column_map.items():
    for col in df.columns:
        if orig.lower() in col.lower():
            rename[col] = target
            break

df = df.rename(columns=rename)

# Приводим суммы к числам
for col in ["дебет", "кредит"]:
    if col in df.columns:
        df[col] = pd.to_numeric(df[col].astype(str).str.replace(r"\s", "", regex=True).str.replace(",", "."), errors="coerce").fillna(0)

df.to_excel(cw.output_path("sber_parsed.xlsx"), index=False)
print(f"Строк: {len(df)}")
print(f"Дебет итого: {df.get('дебет', pd.Series([0])).sum():,.2f}")
print(f"Кредит итого: {df.get('кредит', pd.Series([0])).sum():,.2f}")
```

## Тинькофф Бизнес (XLSX)

```python
import pandas as pd

df = pd.read_excel("/tmp/tinkoff_statement.xlsx")

# Стандартные колонки Тинькофф
column_map = {
    "Номер": "номер",
    "Дата операции": "дата",
    "Сумма": "сумма",
    "Тип": "тип",  # Пополнение / Списание
    "Контрагент": "контрагент",
    "ИНН контрагента": "инн",
    "Назначение платежа": "назначение",
    "Счёт контрагента": "счёт",
}

rename = {}
for orig, target in column_map.items():
    for col in df.columns:
        if orig.lower() in col.lower():
            rename[col] = target
            break

df = df.rename(columns=rename)

# Разделяем на дебет/кредит
if "тип" in df.columns and "сумма" in df.columns:
    df["дебет"] = df.apply(lambda r: abs(r["сумма"]) if "списан" in str(r["тип"]).lower() else 0, axis=1)
    df["кредит"] = df.apply(lambda r: abs(r["сумма"]) if "поступл" in str(r["тип"]).lower() or "пополн" in str(r["тип"]).lower() else 0, axis=1)

df.to_excel(cw.output_path("tinkoff_parsed.xlsx"), index=False)
print(f"Строк: {len(df)}")
```

---

# БЛОК 3: Анализ выписки

## Сверка оборотов

```python
import pandas as pd
import json

# df — уже загруженная выписка с колонками: дата, дебет, кредит, контрагент, инн, назначение
# header — словарь с НачальныйОстаток, ВсегоПоступило, ВсегоСписано, КонечныйОстаток

reconciliation = {
    "начальный_остаток": header.get("НачальныйОстаток", 0),
    "обороты_дебет_факт": df["дебет"].sum(),
    "обороты_кредит_факт": df["кредит"].sum(),
    "обороты_дебет_заявленные": header.get("ВсегоСписано", 0),
    "обороты_кредит_заявленные": header.get("ВсегоПоступило", 0),
}

reconciliation["расхождение_дебет"] = abs(
    reconciliation["обороты_дебет_факт"] - reconciliation["обороты_дебет_заявленные"]
)
reconciliation["расхождение_кредит"] = abs(
    reconciliation["обороты_кредит_факт"] - reconciliation["обороты_кредит_заявленные"]
)

expected_end = (
    reconciliation["начальный_остаток"]
    + reconciliation["обороты_кредит_факт"]
    - reconciliation["обороты_дебет_факт"]
)
reconciliation["конечный_остаток_расчётный"] = expected_end
reconciliation["конечный_остаток_заявленный"] = header.get("КонечныйОстаток", 0)
reconciliation["сверка_пройдена"] = abs(expected_end - header.get("КонечныйОстаток", 0)) < 0.01

with open(cw.output_path("reconciliation.json"), "w", encoding="utf-8") as f:
    json.dump(reconciliation, f, ensure_ascii=False, indent=2)

print("Сверка оборотов:")
for k, v in reconciliation.items():
    print(f"  {k}: {v}")
```

## Группировка по контрагентам

```python
import pandas as pd

# Группируем по ИНН контрагента
by_inn = df.groupby("инн").agg(
    контрагент=("контрагент", "first"),
    операций=("номер", "count"),
    дебет_итого=("дебет", "sum"),
    кредит_итого=("кредит", "sum"),
).reset_index()

by_inn["сальдо"] = by_inn["кредит_итого"] - by_inn["дебет_итого"]
by_inn = by_inn.sort_values("дебет_итого", ascending=False)

by_inn.to_excel(cw.output_path("by_counterparty.xlsx"), index=False)
print(f"Уникальных контрагентов: {len(by_inn)}")
print("\nТоп-5 по списаниям:")
print(by_inn.head().to_string(index=False))
```

## Извлечение НДС из назначения платежа

```python
import re
import pandas as pd

def extract_vat(text: str) -> float | None:
    """Извлекает сумму НДС из назначения платежа."""
    if not text:
        return None

    # Паттерны НДС в назначении
    patterns = [
        r"НДС\s*(?:\(\d+%?\))?\s*[-—]\s*([\d\s]+[,.]?\d*)",
        r"в\s*т\.?\s*ч\.?\s*НДС\s*(?:\(\d+%?\))?\s*([\d\s]+[,.]?\d*)",
        r"НДС\s*([\d\s]+[,.]?\d*)\s*руб",
        r"сумма\s*НДС\s*([\d\s]+[,.]?\d*)",
    ]

    for pattern in patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            amount_str = match.group(1).replace(" ", "").replace(",", ".")
            try:
                return float(amount_str)
            except ValueError:
                continue

    # Проверяем "Без НДС" / "НДС не облагается"
    if re.search(r"без\s*НДС|НДС\s*не\s*облаг", text, re.IGNORECASE):
        return 0.0

    return None

df["ндс"] = df["назначение"].apply(extract_vat)
vat_total = df["ндс"].dropna().sum()
print(f"Общая сумма НДС в выписке: {vat_total:,.2f}")
```

---

# БЛОК 4: Конвертация форматов

## 1CClientBankExchange → Excel

Используй БЛОК 1: парсинг формата 1CClientBankExchange.

## Excel → 1CClientBankExchange

```python
import pandas as pd
from datetime import datetime

def to_1c_bank_format(df: pd.DataFrame, account: str, output_path: str) -> str:
    """Конвертирует DataFrame в формат 1CClientBankExchange."""
    now = datetime.now()

    lines = [
        "1CClientBankExchange",
        "ВерсияФормата=1.03",
        "Кодировка=Windows",
        "Отправитель=",
        "Получатель=",
        f"ДатаСоздания={now.strftime('%d.%m.%Y')}",
        f"ВремяСоздания={now.strftime('%H:%M:%S')}",
        f"ДатаНачала={df['дата'].min()}",
        f"ДатаКонца={df['дата'].max()}",
        f"РасsчСчёт={account}",
    ]

    for _, row in df.iterrows():
        is_outgoing = float(row.get("дебет", 0)) > 0
        lines.append("СекцияДокумент=Платёжное поручение")
        lines.append(f"Номер={row.get('номер', '')}")
        lines.append(f"Дата={row.get('дата', '')}")
        lines.append(f"Сумма={row.get('дебет', 0) if is_outgoing else row.get('кредит', 0):.2f}")

        if is_outgoing:
            lines.append(f"ПлательщикСчёт={account}")
            lines.append(f"ПолучательСчёт={row.get('счёт', '')}")
            lines.append(f"ПолучательИНН={row.get('инн', '')}")
            lines.append(f"Получатель1={row.get('контрагент', '')}")
        else:
            lines.append(f"ПлательщикСчёт={row.get('счёт', '')}")
            lines.append(f"ПлательщикИНН={row.get('инн', '')}")
            lines.append(f"Плательщик1={row.get('контрагент', '')}")
            lines.append(f"ПолучательСчёт={account}")

        lines.append(f"НазначениеПлатежа={row.get('назначение', '')}")
        lines.append("КонецДокумента")

    content = "\r\n".join(lines)

    with open(output_path, "w", encoding="windows-1251") as f:
        f.write(content)

    return output_path
```

---

# БЛОК 5: Валидация банковских реквизитов

```python
def validate_bik(bik: str) -> tuple[bool, str]:
    """Проверка БИК (9 цифр)."""
    bik = bik.strip()
    if not bik.isdigit() or len(bik) != 9:
        return False, "БИК должен содержать 9 цифр"
    if not bik.startswith("04"):
        return False, "БИК должен начинаться с 04"
    return True, "OK"


def validate_account(account: str, bik: str) -> tuple[bool, str]:
    """Проверка расчётного счёта (20 цифр) по БИК."""
    account = account.strip()
    bik = bik.strip()
    if not account.isdigit() or len(account) != 20:
        return False, "Счёт должен содержать 20 цифр"
    if not bik.isdigit() or len(bik) != 9:
        return False, "Для проверки нужен корректный БИК"

    # Контрольный ключ
    control_str = bik[-3:] + account
    weights = [7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1]
    total = sum(int(control_str[i]) * weights[i] for i in range(23))

    if total % 10 != 0:
        return False, "Неверный контрольный ключ счёта"
    return True, "OK"
```

---

# БЛОК 6: Типичные команды пользователя

| Команда | Действие |
|---------|----------|
| "Разобрать выписку из 1С-банка" | БЛОК 1: Парсинг 1CClientBankExchange |
| "Загрузить выписку Сбербанка" | БЛОК 2: Парсинг CSV Сбербанка |
| "Загрузить выписку Тинькофф" | БЛОК 2: Парсинг XLSX Тинькофф |
| "Сверить обороты" | БЛОК 3: Сверка оборотов |
| "Группировка по контрагентам" | БЛОК 3: Группировка по ИНН |
| "Извлечь НДС из платежей" | БЛОК 3: Извлечение НДС |
| "Конвертировать в формат 1С" | БЛОК 4: Excel → 1CClientBankExchange |
| "Проверить реквизиты" | БЛОК 5: Валидация БИК и счёта |

---

---

# БЛОК 7: Автокатегоризация платежей по статьям ДДС

## Паттерн: Категоризация по назначению платежа

```python
import re
import pandas as pd

# Словарь паттернов для категоризации
DDS_CATEGORIES = {
    "Аренда": [
        r"аренд[аы]", r"субаренд", r"помещени[еяй]", r"офис",
        r"склад", r"нежил", r"коммерч.*площ",
    ],
    "Зарплата": [
        r"заработн", r"зарплат", r"аванс.*сотрудник", r"оплата труда",
        r"з/?п", r"перечисление\s+зп", r"реестр\s+на\s+зачисление",
    ],
    "Налоги и сборы": [
        r"налог", r"ндс", r"ндфл", r"усн", r"есхн",
        r"ифнс", r"фнс", r"енвд", r"страхов.*взнос",
        r"пфр", r"фсс", r"сфр", r"пени.*налог", r"штраф.*налог",
        r"госпошлин", r"торговый\s+сбор",
    ],
    "Закупка товаров": [
        r"за\s+товар", r"поставк[аеу]", r"по\s+счёту\s+№", r"по\s+счету\s+№",
        r"закупк[аеу]", r"по\s+накладной", r"комплектующ", r"материал",
        r"сырь[еёя]", r"оборудовани[еяй]",
    ],
    "Услуги": [
        r"за\s+услуг[иу]", r"консультац", r"юридич", r"бухгалтер",
        r"аудит", r"обслуживан", r"сервис", r"абонент.*плат",
        r"подписк[аеу]", r"лицензи[яю]", r"по\s+договору.*услуг",
    ],
    "Реклама и маркетинг": [
        r"реклам[аеу]", r"маркетинг", r"продвижен", r"seo",
        r"контекстн", r"таргет", r"яндекс.*директ", r"vk\s*ads",
        r"smm", r"полиграф", r"баннер",
    ],
    "Связь и интернет": [
        r"связ[иь]", r"телеком", r"интернет", r"телефон",
        r"мобильн.*связ", r"хостинг", r"домен", r"сервер",
        r"облачн", r"cloud",
    ],
    "Транспорт и логистика": [
        r"транспорт", r"доставк[аеу]", r"логистик", r"перевозк",
        r"грузопер", r"курьер", r"экспедиц", r"такси",
        r"гсм", r"бензин", r"топлив",
    ],
    "Коммунальные платежи": [
        r"коммунал", r"электроэнерг", r"водоснабж", r"теплоснабж",
        r"газоснабж", r"жкх", r"мусор", r"тбо", r"тко",
    ],
    "Банковские комиссии": [
        r"комисси[яю].*банк", r"банковск.*комисси", r"обслуживание\s+счёт",
        r"рко", r"расчётно.*кассов", r"эквайринг", r"инкасс",
    ],
    "Займы и кредиты": [
        r"кредит", r"займ", r"заём", r"процент.*по\s+договор",
        r"погашени[еяй].*основн", r"тело\s+кредит", r"лизинг",
        r"овердрафт",
    ],
    "Возвраты": [
        r"возврат", r"рекламаци", r"компенсаци[яю].*покуп",
    ],
    "Дивиденды и прибыль": [
        r"дивиденд", r"распределени[еяй].*прибыл",
        r"выплат[аеу].*учредител", r"доход.*участник",
    ],
}


def categorize_payment(purpose: str) -> str:
    """Определяет статью ДДС по назначению платежа."""
    if not purpose:
        return "Прочее"

    purpose_lower = purpose.lower()

    for category, patterns in DDS_CATEGORIES.items():
        for pattern in patterns:
            if re.search(pattern, purpose_lower):
                return category

    return "Прочее"


def categorize_dataframe(df: pd.DataFrame, purpose_col: str = "назначение") -> pd.DataFrame:
    """Добавляет колонку 'статья_ддс' к DataFrame с банковской выпиской."""
    df = df.copy()
    df["статья_ддс"] = df[purpose_col].apply(categorize_payment)
    return df
```

## Паттерн: Сводка по статьям ДДС

```python
import pandas as pd

def dds_summary(df: pd.DataFrame) -> pd.DataFrame:
    """
    Сводка расходов/поступлений по статьям ДДС.
    Предполагает наличие колонок: статья_ддс, дебет, кредит.
    """
    summary = df.groupby("статья_ддс").agg(
        операций=("статья_ддс", "count"),
        списания=("дебет", "sum"),
        поступления=("кредит", "sum"),
    ).reset_index()

    summary["сальдо"] = summary["поступления"] - summary["списания"]
    summary = summary.sort_values("списания", ascending=False)

    # Итого
    totals = pd.DataFrame([{
        "статья_ддс": "ИТОГО",
        "операций": summary["операций"].sum(),
        "списания": summary["списания"].sum(),
        "поступления": summary["поступления"].sum(),
        "сальдо": summary["сальдо"].sum(),
    }])
    summary = pd.concat([summary, totals], ignore_index=True)

    return summary


# Использование:
# df = categorize_dataframe(bank_df)
# report = dds_summary(df)
# report.to_excel(cw.output_path("dds_report.xlsx"), index=False)
# print(report.to_string(index=False))
```

## Паттерн: Полный пайплайн — выписка с категоризацией в Excel

```python
import pandas as pd
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter

# Цвета для статей ДДС
DDS_COLORS = {
    "Аренда": "FFF2CC",
    "Зарплата": "D9E2F3",
    "Налоги и сборы": "FCE4D6",
    "Закупка товаров": "E2EFDA",
    "Услуги": "D6DCE4",
    "Реклама и маркетинг": "F8CBAD",
    "Связь и интернет": "BDD7EE",
    "Транспорт и логистика": "DBDBDB",
    "Коммунальные платежи": "FFE699",
    "Банковские комиссии": "F4B084",
    "Займы и кредиты": "C9C9C9",
    "Возвраты": "A9D18E",
    "Дивиденды и прибыль": "B4C6E7",
    "Прочее": "FFFFFF",
}

def export_categorized_statement(df: pd.DataFrame, output_path: str) -> str:
    """Экспорт выписки с категоризацией в Excel с цветовой разметкой."""
    wb = Workbook()

    # Лист 1: Детализация
    ws1 = wb.active
    ws1.title = "Выписка"
    BORDER = Border(*(Side(style="thin") for _ in range(4)))
    BOLD = Font(bold=True)

    headers = ["Дата", "Номер", "Контрагент", "ИНН", "Дебет", "Кредит", "Назначение", "Статья ДДС"]
    widths = [12, 10, 25, 14, 14, 14, 40, 20]
    cols = ["дата", "номер", "контрагент", "инн", "дебет", "кредит", "назначение", "статья_ддс"]

    for c, (h, w) in enumerate(zip(headers, widths), 1):
        cell = ws1.cell(1, c, h)
        cell.font = BOLD
        cell.border = BORDER
        ws1.column_dimensions[get_column_letter(c)].width = w

    for r, (_, row) in enumerate(df.iterrows(), 2):
        for c, col in enumerate(cols, 1):
            cell = ws1.cell(r, c, row.get(col, ""))
            cell.border = BORDER
            if c in (5, 6):
                cell.number_format = '# ##0.00'
        # Цвет по категории
        category = str(row.get("статья_ддс", "Прочее"))
        color = DDS_COLORS.get(category, "FFFFFF")
        fill = PatternFill("solid", start_color=color)
        ws1.cell(r, 8).fill = fill

    # Лист 2: Сводка по ДДС
    ws2 = wb.create_sheet("Сводка ДДС")
    summary = df.groupby("статья_ддс").agg(
        операций=("статья_ддс", "count"),
        списания=("дебет", "sum"),
        поступления=("кредит", "sum"),
    ).reset_index()
    summary["сальдо"] = summary["поступления"] - summary["списания"]
    summary = summary.sort_values("списания", ascending=False)

    sum_headers = ["Статья ДДС", "Операций", "Списания", "Поступления", "Сальдо"]
    for c, h in enumerate(sum_headers, 1):
        cell = ws2.cell(1, c, h)
        cell.font = BOLD
        cell.border = BORDER
    ws2.column_dimensions["A"].width = 25

    for r, (_, row) in enumerate(summary.iterrows(), 2):
        ws2.cell(r, 1, row["статья_ддс"]).border = BORDER
        color = DDS_COLORS.get(str(row["статья_ддс"]), "FFFFFF")
        ws2.cell(r, 1).fill = PatternFill("solid", start_color=color)
        ws2.cell(r, 2, int(row["операций"])).border = BORDER
        for c, col in [(3, "списания"), (4, "поступления"), (5, "сальдо")]:
            cell = ws2.cell(r, c, float(row[col]))
            cell.border = BORDER
            cell.number_format = '# ##0.00'

    wb.save(output_path)
    return output_path

# Использование:
# df = categorize_dataframe(bank_df)
# export_categorized_statement(df, cw.output_path("categorized_statement.xlsx"))
```


*Skill-файл содержит паттерны кода для парсинга банковских выписок. Выполняй код через sandbox_bash.*
