Загрузка данных


from pathlib import Path
from decimal import Decimal, InvalidOperation
from datetime import datetime, date
import sys
import traceback
from copy import copy

from openpyxl import load_workbook
import win32com.client
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH


# ============================================================
# НАСТРОЙКИ
# ============================================================

BASE_DIR = (
    Path(sys.executable).resolve().parent
    if getattr(sys, "frozen", False)
    else Path(__file__).resolve().parent
)

EXCEL_FOLDER = BASE_DIR / "Excel"

TEMPLATE_FILE = BASE_DIR / "_ШаблонПриказа.docx"
TTN_TEMPLATE_FILE = BASE_DIR / "_ТТН.xlsx"

OUTPUT_FOLDER = BASE_DIR / "Результат"

OUTPUT_FILE = OUTPUT_FOLDER / "Приказ.docx"
TTN_OUTPUT_FILE = OUTPUT_FOLDER / "ТТН.xlsx"

SOURCE_SHEET = "Драг. мет + Деф."


# ============================================================
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# ============================================================

def value_to_text(value):
    """Преобразует значение Excel в текст."""

    if value is None:
        return ""

    if isinstance(value, (datetime, date)):
        return value.strftime("%d.%m.%Y")

    if isinstance(value, float):
        if value.is_integer():
            return str(int(value))

        return f"{value:.2f}".replace(".", ",")

    if isinstance(value, Decimal):
        if value == value.to_integral_value():
            return str(int(value))

        return f"{value:.2f}".replace(".", ",")

    return str(value).strip()


def number_from_cell(value, address):
    """Преобразует ячейку в число; пустая ячейка считается нулём."""

    if value is None or value == "":
        return Decimal("0")

    if isinstance(value, bool):
        raise ValueError(
            f"В ячейке {address} логическое значение вместо числа."
        )

    try:
        if isinstance(value, str):
            value = (
                value
                .strip()
                .replace("\u00a0", "")
                .replace(" ", "")
                .replace(",", ".")
            )

            if value == "":
                return Decimal("0")

        return Decimal(str(value))

    except (InvalidOperation, ValueError):
        raise ValueError(
            f"В ячейке {address} ожидалось число, "
            f"получено: {value!r}"
        )


def money_to_text(value):
    """Денежный формат для Word: 595,00."""

    return f"{value:.2f}".replace(".", ",")


# ============================================================
# ФОРМИРОВАНИЕ ПРИКАЗА WORD
# ============================================================

def generate_order(excel_files):
    """Формирует Приказ.docx."""

    if not TEMPLATE_FILE.is_file():
        print(
            "\nШаблон приказа не найден. "
            "Генерация приказа пропущена."
        )
        return 0, []

    doc = Document(str(TEMPLATE_FILE))

    if not doc.tables:
        raise ValueError(
            "В шаблоне Word не найдена таблица."
        )

    table = doc.tables[0]

    if len(table.columns) < 6:
        raise ValueError(
            f"В таблице Word {len(table.columns)} колонок, "
            "а требуется минимум 6."
        )

    if len(table.rows) == 0:
        raise ValueError(
            "В таблице Word нет строки заголовка."
        )

    # Оставляем только строку заголовков.
    while len(table.rows) > 1:
        table._tbl.remove(table.rows[-1]._tr)

    processed = 0
    errors = []

    print("\n" + "=" * 58)
    print("ФОРМИРОВАНИЕ ПРИКАЗА")
    print("=" * 58)

    for index, excel_path in enumerate(excel_files, start=1):

        print(
            f"[{index}/{len(excel_files)}] "
            f"{excel_path.name}"
        )

        try:
            workbook = load_workbook(
                filename=str(excel_path),
                data_only=True,
                keep_vba=(
                    excel_path.suffix.lower() == ".xlsm"
                ),
                read_only=True
            )

            try:
                if SOURCE_SHEET not in workbook.sheetnames:
                    raise ValueError(
                        f"Не найден лист '{SOURCE_SHEET}'. "
                        f"Доступные листы: "
                        f"{', '.join(workbook.sheetnames)}"
                    )

                ws = workbook[SOURCE_SHEET]

                # Данные для приказа
                name = value_to_text(ws["K2"].value)
                inventory = value_to_text(ws["K5"].value)
                release_date = value_to_text(ws["K20"].value)

                initial_cost = number_from_cell(
                    ws["K18"].value,
                    "K18"
                )

                previous_cost = number_from_cell(
                    ws["K19"].value,
                    "K19"
                )

                residual_cost = (
                    initial_cost - previous_cost
                )

                values = [
                    str(index),
                    name,
                    inventory,
                    release_date,
                    money_to_text(initial_cost),
                    money_to_text(residual_cost),
                ]

                row = table.add_row()

                for column_index, value in enumerate(values):

                    cell = row.cells[column_index]

                    cell.text = value

                    # Все колонки, кроме Наименования,
                    # центрируем.
                    if column_index != 1:

                        for paragraph in cell.paragraphs:
                            paragraph.alignment = (
                                WD_ALIGN_PARAGRAPH.CENTER
                            )

                processed += 1

                print("    OK")

            finally:
                workbook.close()

        except Exception as exc:

            errors.append(
                (
                    excel_path.name,
                    str(exc)
                )
            )

            print(
                f"    ОШИБКА: {exc}"
            )

    doc.save(str(OUTPUT_FILE))

    print("\nПриказ сформирован:")
    print(OUTPUT_FILE)

    return processed, errors


# ============================================================
# КОПИРОВАНИЕ ОФОРМЛЕНИЯ СТРОКИ EXCEL
# ============================================================

def copy_row_format(ws, source_row, target_row):
    """
    Копирует оформление строки source_row
    в target_row.
    """

    for column in range(1, ws.max_column + 1):

        source_cell = ws.cell(
            row=source_row,
            column=column
        )

        target_cell = ws.cell(
            row=target_row,
            column=column
        )

        if source_cell.has_style:
            target_cell._style = copy(
                source_cell._style
            )

        if source_cell.number_format:
            target_cell.number_format = (
                source_cell.number_format
            )

        if source_cell.alignment:
            target_cell.alignment = copy(
                source_cell.alignment
            )

        if source_cell.protection:
            target_cell.protection = copy(
                source_cell.protection
            )

        if source_cell.font:
            target_cell.font = copy(
                source_cell.font
            )

        if source_cell.fill:
            target_cell.fill = copy(
                source_cell.fill
            )

        if source_cell.border:
            target_cell.border = copy(
                source_cell.border
            )

    # Высота строки
    if source_row in ws.row_dimensions:

        ws.row_dimensions[target_row].height = (
            ws.row_dimensions[source_row].height
        )

# ============================================================
# ФОРМИРОВАНИЕ ТТН
# ============================================================

def generate_ttn(excel_files):
    """Формирует ТТН через Microsoft Excel и win32com."""

    if not TTN_TEMPLATE_FILE.is_file():
        raise FileNotFoundError(
            f"Не найден шаблон ТТН: {TTN_TEMPLATE_FILE}"
        )

    print("\n" + "=" * 58)
    print("ФОРМИРОВАНИЕ ТТН")
    print("=" * 58)

    template_row = 27
    total_row = 28

    items = []
    errors = []

    # Читаем данные из исходных Excel через openpyxl.
    for index, excel_path in enumerate(excel_files, start=1):
        print(f"[{index}/{len(excel_files)}] {excel_path.name}")

        try:
            source_wb = load_workbook(
                filename=str(excel_path),
                data_only=True,
                keep_vba=(
                    excel_path.suffix.lower() == ".xlsm"
                ),
                read_only=True,
            )

            try:
                if SOURCE_SHEET not in source_wb.sheetnames:
                    raise ValueError(
                        f"Не найден лист '{SOURCE_SHEET}'"
                    )

                ws_source = source_wb[SOURCE_SHEET]

                value_a = value_to_text(ws_source["K2"].value)
                value_d = value_to_text(ws_source["K22"].value)

                value_k6 = number_from_cell(
                    ws_source["K6"].value, "K6"
                )
                value_k18 = number_from_cell(
                    ws_source["K18"].value, "K18"
                )

                total = value_k6 * value_k18

                items.append({
                    "filename": excel_path.name,
                    "A": value_a,
                    "D": value_d,
                    "E": float(value_k6),
                    "F": float(value_k18),
                    "G": float(total),
                    "H": 0,
                    "I": 0,
                    "J": float(total),
                })

            finally:
                source_wb.close()

        except Exception as exc:
            errors.append((excel_path.name, str(exc)))
            print(f"    ОШИБКА: {exc}")

    if not items:
        print("Нет данных для заполнения ТТН.")
        return 0, errors

    excel = None
    workbook = None
    processed = 0

    try:
        # Запускаем отдельный скрытый экземпляр Excel.
        excel = win32com.client.DispatchEx(
            "Excel.Application"
        )

        excel.Visible = False
        excel.DisplayAlerts = False
        excel.ScreenUpdating = False
        excel.EnableEvents = False

        workbook = excel.Workbooks.Open(
            str(TTN_TEMPLATE_FILE.resolve()),
            UpdateLinks=0,
            ReadOnly=False,
        )

        ws = workbook.Worksheets(1)

        # Вставляем настоящие строки Excel перед строкой «Итого».
        additional_rows = max(0, len(items) - 1)

        if additional_rows > 0:
            last_insert_row = (
                total_row + additional_rows - 1
            )

            print(
                f"Вставляем строк перед «Итого»: "
                f"{additional_rows}"
            )

            ws.Rows(
                f"{total_row}:{last_insert_row}"
            ).Insert()

        final_total_row = total_row + additional_rows

        print(f"Строка «Итого»: {final_total_row}")

        # Копируем оформление строки 27 в добавленные строки.
        for row in range(
            template_row + 1,
            final_total_row
        ):
            ws.Rows(template_row).Copy()

            ws.Rows(row).PasteSpecial(
                Paste=-4122  # xlPasteFormats
            )

            ws.Rows(row).RowHeight = (
                ws.Rows(template_row).RowHeight
            )

            # Объединяем A:C в новой строке.
            merge_range = ws.Range(f"A{row}:C{row}")

            if not merge_range.MergeCells:
                merge_range.Merge()

        excel.CutCopyMode = False

        # Записываем данные в строки ТТН.
        for index, item in enumerate(items):
            row = template_row + index

            print(
                f"[{index + 1}/{len(items)}] "
                f"{item['filename']} → строка {row}"
            )

            for column in (
                "A", "D", "E", "F",
                "G", "H", "I", "J"
            ):
                ws.Range(
                    f"{column}{row}"
                ).Value = item[column]

            processed += 1
            print("    OK")

        # Сохраняем результат отдельно от шаблона.
        workbook.SaveAs(
            str(TTN_OUTPUT_FILE.resolve()),
            FileFormat=51,  # XLSX
        )

    finally:
        # Закрываем Excel даже при ошибке.
        if workbook is not None:
            try:
                workbook.Close(SaveChanges=False)
            except Exception:
                pass

        if excel is not None:
            try:
                excel.CutCopyMode = False
                excel.Quit()
            except Exception:
                pass

    print("\nТТН сформирована:")
    print(TTN_OUTPUT_FILE)
    print(
        f"Обработано: {processed} "
        f"из {len(excel_files)}"
    )

    if errors:
        print("\nОшибки при обработке исходных файлов:")

        for filename, error in errors:
            print(f"- {filename}: {error}")

    return processed, errors

# ============================================================
# MAIN
# ============================================================

def main():

    print("=" * 58)
    print("АВТОМАТИЧЕСКОЕ ФОРМИРОВАНИЕ ДОКУМЕНТОВ")
    print("=" * 58)

    # --------------------------------------------------------
    # Проверяем папку Excel
    # --------------------------------------------------------

    if not EXCEL_FOLDER.is_dir():

        raise FileNotFoundError(
            f"Не найдена папка с Excel-файлами: "
            f"{EXCEL_FOLDER}\n"
            "Создайте папку 'Excel' рядом "
            "с программой."
        )

    # --------------------------------------------------------
    # Получаем список Excel-файлов
    # --------------------------------------------------------

    excel_files = sorted(
        [
            p
            for p in EXCEL_FOLDER.iterdir()
            if (
                p.is_file()
                and p.suffix.lower()
                in (".xlsx", ".xlsm")
                and not p.name.startswith("~$")
            )
        ],
        key=lambda p: p.name.casefold()
    )

    if not excel_files:

        raise FileNotFoundError(
            "В папке Excel нет файлов .xlsx или .xlsm."
        )

    print(
        f"\nНайдено Excel-файлов: "
        f"{len(excel_files)}"
    )

    # --------------------------------------------------------
    # Создаём папку Результат
    # --------------------------------------------------------

    OUTPUT_FOLDER.mkdir(
        parents=True,
        exist_ok=True
    )

    # --------------------------------------------------------
    # Формируем приказ
    # --------------------------------------------------------

    if TEMPLATE_FILE.is_file():
        order_processed, order_errors = (
            generate_order(excel_files)
        )
    else:
        print(
            "\nШаблон приказа не найден:"
            f"\n{TEMPLATE_FILE}"
            "\nГенерация приказа пропущена."
        )
        order_processed = 0
        order_errors = []

    # --------------------------------------------------------
    # Формируем ТТН
    # --------------------------------------------------------

    ttn_processed, ttn_errors = (
        generate_ttn(excel_files)
    )

    # --------------------------------------------------------
    # Итог
    # --------------------------------------------------------

    print("\n" + "=" * 58)
    print("ОБРАБОТКА ЗАВЕРШЕНА")
    print("=" * 58)

    print(
        f"Excel-файлов:        {len(excel_files)}"
    )

    print(
        f"Приказ обработано:   {order_processed}"
    )

    print(
        f"ТТН обработано:      {ttn_processed}"
    )

    print(
        f"\nПриказ:"
        f"\n{OUTPUT_FILE}"
    )

    print(
        f"\nТТН:"
        f"\n{TTN_OUTPUT_FILE}"
    )

    # Ошибки приказа
    if order_errors:

        print("\nОшибки при формировании Приказа:")

        for filename, error in order_errors:

            print(
                f"- {filename}: {error}"
            )

    # Ошибки ТТН
    if ttn_errors:

        print("\nОшибки при формировании ТТН:")

        for filename, error in ttn_errors:

            print(
                f"- {filename}: {error}"
            )

    print("\nПроверьте итоговые файлы.")


# ============================================================
# ЗАПУСК
# ============================================================

if __name__ == "__main__":

    try:

        main()

    except Exception as exc:

        print(
            f"\nОШИБКА: {exc}"
        )

        traceback.print_exc()