arm-core/api/scripts/convert_chart_1c.py
Andrey Epifancev 408dcaf3b0 feat(seed): конвертер плана счетов 1С в csv seed
Одноразовый скрипт api/scripts/convert_chart_1c.py: xlsx выгрузки 1С -> chart_of_accounts.csv
(иерархия по структуре кода, kind из «Вид», is_off_balance из «Заб.»).
ASSUMPTION(OQ-113): выгрузка считается полным планом счетов.

Refs #23

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 12:57:20 +04:00

111 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Одноразовый конвертер плана счетов 1С (xlsx) в `seed/core/chart_of_accounts.csv` (issue #23).
Источник — `fixtures/directories/chart-of-accounts-1c.xlsx` (данные, не код). Колонки ищутся по заголовкам:
«Код счета», «Наименование счета», «Вид» (А/П/АП), «Заб.» (Да/Нет). Порядок строк — как в выгрузке.
ASSUMPTION(OQ-113): выгрузка считается полным планом счетов заказчика.
Запуск (из `arm-core/api`)::
.venv/bin/python -I -m scripts.convert_chart_1c <chart-of-accounts-1c.xlsx> <chart_of_accounts.csv>
`-I` отключает подхват кода из текущего каталога; пути — абсолютные.
"""
import csv
import sys
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from pathlib import Path
from openpyxl import load_workbook
CSV_COLUMNS = ["code", "parent_code", "name", "kind", "row_type", "is_off_balance"]
KIND_BY_1C = {"А": "A", "П": "P", "АП": "AP"} # кириллица, как в выгрузке
ROW_TYPE_BY_DEPTH = ("account", "subaccount", "analytic")
HEADERS = {"code": "Код счета", "name": "Наименование счета", "kind": "Вид", "off_balance": "Заб."}
@dataclass(frozen=True)
class ChartRow:
code: str
parent_code: str
name: str
kind: str
row_type: str
is_off_balance: bool
def convert(records: Iterable[Mapping[str, object]]) -> list[ChartRow]:
"""Строки выгрузки (словари по заголовкам) -> строки seed; иерархия — по структуре кода."""
raw = [
(
str(r[HEADERS["code"]]).strip(),
" ".join(str(r[HEADERS["name"]]).split()),
str(r[HEADERS["kind"]]).strip(),
str(r[HEADERS["off_balance"]]).strip(),
)
for r in records
]
codes = [code for code, *_ in raw]
known = set(codes)
if len(known) != len(codes):
raise ValueError("в выгрузке повторяются коды счетов")
rows: list[ChartRow] = []
for code, name, kind_1c, off_balance in raw:
if kind_1c not in KIND_BY_1C:
raise ValueError(f"{code}: неизвестный вид «{kind_1c}» (ожидались А, П, АП)")
if off_balance not in ("Да", "Нет"):
raise ValueError(f"{code}: «Заб.» должно быть Да или Нет, получено «{off_balance}»")
if not name:
raise ValueError(f"{code}: пустое наименование")
depth = code.count(".")
if depth >= len(ROW_TYPE_BY_DEPTH):
raise ValueError(f"{code}: глубина кода больше {len(ROW_TYPE_BY_DEPTH) - 1} точек")
parent = code.rsplit(".", 1)[0] if depth else ""
if parent and parent not in known:
# Сирота: родителя нет в выгрузке (например, «Я68.01» без «Я68») — родителей не выдумываем,
# но предков выше быть не должно, иначе иерархия в выгрузке неполная
ancestors = {code.rsplit(".", i)[0] for i in range(2, depth + 1)}
if ancestors & known:
raise ValueError(f"{code}: нет родителя «{parent}», но есть более дальний предок")
parent = ""
rows.append(ChartRow(code, parent, name, KIND_BY_1C[kind_1c], ROW_TYPE_BY_DEPTH[depth], off_balance == "Да"))
return rows
def read_xlsx(path: Path) -> list[dict[str, object]]:
"""Первый лист: заголовки — первая строка, колонки ищутся по названию."""
workbook = load_workbook(path, read_only=True, data_only=True)
try:
lines = list(workbook.worksheets[0].iter_rows(values_only=True))
finally:
workbook.close()
header = [str(cell).strip() if cell is not None else "" for cell in lines[0]]
missing = [title for title in HEADERS.values() if title not in header]
if missing:
raise ValueError(f"в выгрузке нет колонок: {', '.join(missing)}")
return [dict(zip(header, line, strict=False)) for line in lines[1:] if any(cell is not None for cell in line)]
def write_csv(rows: Iterable[ChartRow], path: Path) -> None:
with path.open("w", encoding="utf-8", newline="") as stream:
writer = csv.writer(stream, lineterminator="\n")
writer.writerow(CSV_COLUMNS)
for r in rows:
writer.writerow(
[r.code, r.parent_code, r.name, r.kind, r.row_type, "true" if r.is_off_balance else "false"]
)
def main(argv: list[str]) -> int:
if len(argv) != 3: # noqa: PLR2004
print(__doc__, file=sys.stderr)
return 2
write_csv(convert(read_xlsx(Path(argv[1]))), Path(argv[2]))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))