Загрузка данных
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()