Привет
раньше я использовал таблицу akt как основную, парсил под неё pdf документ, искал в нём текст и сохранял в базе. 
теперь мне дали прямой доступ к содержанию таблицы spisanie, которая содержит все необходимые мне поля, но мне необходимо добавлять к записи еще и отсканированное изображение.
Загрузку изображений надо оставить, оставить в неё поиск актов и привязку номера акта number_akt к изображению, привязывать имя рисунка к номеру number_zakl.
Страницу редактирования актов переделать так, что бы можно было посмотреть акт, и если не распознан,то ввести номер акта вручную и привязать имя рисунка к number_zakl.
Переработай код под новые требования. Сразу задай уточняющие вопросы и потом переработай.
# app/models.py

#from app import db
from .db import db  # Импортируем db из нового модуля

class Events(db.Model):
    __tablename__ = 'events'
    event_id = db.Column(db.Integer, primary_key=True)
    event_name = db.Column(db.String(127))
    event_time = db.Column(db.DateTime)

    def __repr__(self):
        return f'<Event {self.event_name}>'

class UESTable(db.Model):
    __tablename__ = 'ues_table'
    ues_id = db.Column(db.Integer, primary_key=True)
    ues_name = db.Column(db.String(31), nullable=True, unique=True)

    def __repr__(self):
        return f'<UES {self.ues_name}>'

class DescriptionTable(db.Model):
    __tablename__ = 'descriptiontbl'
    description_id = db.Column(db.Integer, primary_key=True)
    description_text = db.Column(db.String(255), unique=True)

    def __repr__(self):
        return f'<Description {self.description_id}>'


class KomplektTable(db.Model):
    __tablename__ = 'komplekttbl'
    komplekt_id = db.Column(db.Integer, primary_key=True)
    komplekt_text = db.Column(db.String(255), unique=True)

    def __repr__(self):
        return f'<Komplekt {self.komplekt_id}>'

class OsnovanieTable(db.Model):
    __tablename__ = 'osnovanietbl'
    osnovanie_id = db.Column(db.Integer, primary_key=True)
    osnovanie_txt = db.Column(db.String(255), unique=True)

    def __repr__(self):
        return f'<Osnovanie {self.osnovanie_id}>'

class Landocs(db.Model):
    __tablename__ = 'landocs'
    doc_id = db.Column(db.Integer, primary_key=True)
    doc_name = db.Column(db.String(255), nullable=False, unique=True)
    doc_date = db.Column(db.Date, nullable=False)
    doc_description = db.Column(db.Text)

    hardware = db.relationship('Hardware', backref='landoc', lazy=True)

    def __repr__(self):
        return f'<Landoc {self.doc_name}>'

class StateHW(db.Model):
    __tablename__ = 'statehw'
    statehw_id = db.Column(db.Integer, primary_key=True)
    statehw_name = db.Column(db.String(31), nullable=False, unique=True)

    def __repr__(self):
        return f'<StateHW {self.statehw_name}>'

class ChekedHW(db.Model):
    __tablename__ = 'chekedhw'
    chekedhw_id = db.Column(db.Integer, primary_key=True)
    statehw_id = db.Column(db.Integer, db.ForeignKey('statehw.statehw_id'))
    date_checked_akt = db.Column(db.Date)
    date_checked_os = db.Column(db.Date)
    doc_id = db.Column(db.Integer, db.ForeignKey('landocs.doc_id'))
    state = db.relationship('StateHW', backref='cheked_hw')
    doc = db.relationship('Landocs', backref='cheked_hw')
    hardware = db.relationship('Hardware', backref='cheked_hw', uselist=False)

    def __repr__(self):
        return f'<ChekedHW {self.chekedhw_id}>'

class HardwareDocs(db.Model):
    __tablename__ = 'hardware_docs'
    hardwaredocs_id = db.Column(db.Integer, primary_key=True)
    descrdocs_hw_text = db.Column(db.Text, nullable=False)  # Сделайте уникальность на уровне приложения
    doc_id = db.Column(db.Integer, db.ForeignKey('landocs.doc_id'), nullable=False)

    # Связь с описаниями
    descriptions = db.relationship('HardwareDescription', backref='hardware_docs', lazy=True)

    def __repr__(self):
        return f'<HardwareDocs {self.hardwaredocs_id}>'


class HardwareDescription(db.Model):
    __tablename__ = 'hardware_description'
    hardwaredescription_id = db.Column(db.Integer, primary_key=True)
    hardwaredescription_txt = db.Column(db.Text, nullable=False)
    doc_id = db.Column(db.Integer, db.ForeignKey('landocs.doc_id'), nullable=False)

    # связь с HardwareDocs
    hardware_doc_id = db.Column(db.Integer, db.ForeignKey('hardware_docs.hardwaredocs_id'))

    def __repr__(self):
        return f'<HardwareDescription {self.hardwaredescription_id}>'


class Hardware(db.Model):
    __tablename__ = 'hardware'
    hw_id = db.Column(db.Integer, primary_key=True)
    duplicate_hw_id = db.Column(db.Integer, db.ForeignKey('hardware.hw_id'))
    doc_id = db.Column(db.Integer, db.ForeignKey('landocs.doc_id'))
    no = db.Column(db.Integer)
    name_txt = db.Column(db.Text, nullable=False)
    name = db.Column(db.Text, nullable=False)
    inv = db.Column(db.String(31), nullable=False, unique=True)
    sn = db.Column(db.String(64))
    mac = db.Column(db.String(31))
    kud = db.Column(db.String(31))
    boxid = db.Column(db.String(31))
    number_akt = db.Column(db.Integer)
    date_akt = db.Column(db.Date)
    quantity = db.Column(db.Integer)
    shifr_time = db.Column(db.Integer)
    shifr_amo = db.Column(db.Integer)
    date_build = db.Column(db.Date)
    date_input = db.Column(db.Date)
    date_buh = db.Column(db.Date)
    date_nouse = db.Column(db.Date)
    first_price = db.Column(db.Float)
    first_price_date = db.Column(db.Date)
    last_price = db.Column(db.Float)
    last_price_date = db.Column(db.Date)
    srok = db.Column(db.Integer)
    ues_description = db.Column(db.String(31))
    komplekt_id = db.Column(db.Integer, db.ForeignKey('komplekttbl.komplekt_id'))
    osnovanie_id = db.Column(db.Integer, db.ForeignKey('osnovanietbl.osnovanie_id'))
    description_id = db.Column(db.Integer, db.ForeignKey('descriptiontbl.description_id'))
    ues_id = db.Column(db.Integer, db.ForeignKey('ues_table.ues_id'))
    chekedhw_id = db.Column(db.Integer, db.ForeignKey('chekedhw.chekedhw_id'))
    os_id = db.Column(db.Integer, db.ForeignKey('os.os_id'))
    akt_id = db.Column(db.Integer, db.ForeignKey('akt.akt_id'))
    spisanie_id = db.Column(db.Integer, db.ForeignKey('spisanie.spisanie_id', name='uq_spisanie_id'))
    asudoolink_id = db.Column(db.Integer, db.ForeignKey('asudoolink.asudoolink_id'))
    event_id = db.Column(db.Integer, db.ForeignKey('events.event_id'))
    duplicate_hw = db.relationship('Hardware', remote_side=[hw_id])
    komplekt = db.relationship('KomplektTable', backref='hardware')
    osnovanie = db.relationship('OsnovanieTable', backref='hardware')
    description = db.relationship('DescriptionTable', backref='hardware')
    ues = db.relationship('UESTable', backref='hardware')
    os = db.relationship('OS', backref='hardware')
    akt = db.relationship('Akt', backref='hardware')
    event = db.relationship('Events', backref='hardware')

    def __repr__(self):
        return f'<Hardware inv={self.inv}>'

class OS(db.Model):
    __tablename__ = 'os'
    os_id = db.Column(db.Integer, primary_key=True)
    inv = db.Column(db.String(31), unique=True)
    name = db.Column(db.String(255))
    passport_number = db.Column(db.String(31))
    reg_number = db.Column(db.String(31))
    zn = db.Column(db.String(127))
    nn = db.Column(db.String(31))
    date_warranty = db.Column(db.Date)
    date_production = db.Column(db.Date)
    date_expluatation = db.Column(db.Date)
    date_input = db.Column(db.Date)
    date_output = db.Column(db.Date)
    date_trash = db.Column(db.Date)
    quantity = db.Column(db.Integer)
    model = db.Column(db.String(31))
    made_by = db.Column(db.String(31))
    code_party = db.Column(db.String(31))
    period = db.Column(db.String(31))
    dop_inv = db.Column(db.String(31))
    record_number = db.Column(db.String(31))
    filial_number = db.Column(db.String(31))
    description = db.Column(db.String(31))
    groups = db.Column(db.String(31))
    type_doc = db.Column(db.String(31))
    ed_izm = db.Column(db.String(31))
    group_os = db.Column(db.String(127))
    vid_os = db.Column(db.String(127))
    podrazdelenie = db.Column(db.String(127))
    mol = db.Column(db.String(127))
    code_norma_amort = db.Column(db.String(31))
    norma_amort = db.Column(db.String(31))
    uses_os = db.Column(db.String(127))
    price = db.Column(db.Float)
    summ_amort = db.Column(db.Float)
    amort_otch_period = db.Column(db.String(31))
    price_input = db.Column(db.String(31))
    iznos_input = db.Column(db.String(31))
    fix_summ_amort = db.Column(db.String(31))
    srok_polezn_use = db.Column(db.String(31))
    valute = db.Column(db.String(31))
    metod_nach_amort = db.Column(db.String(255))
    tho_amort = db.Column(db.String(255))
    type_object = db.Column(db.String(255))
    code_group_okof = db.Column(db.String(31))
    name_group_okof = db.Column(db.String(31))
    popravka_koef = db.Column(db.String(31))
    last_price = db.Column(db.Float)
    event_id = db.Column(db.Integer, db.ForeignKey('events.event_id'))

    event = db.relationship('Events', backref='oses')

    def __repr__(self):
        return f'<OS inv={self.inv}>'

class Akt(db.Model):
    __tablename__ = 'akt'
    akt_id = db.Column(db.Integer, primary_key=True)
    number_akt = db.Column(db.Integer)
    date_akt = db.Column(db.Date)
    name_hw = db.Column(db.String(127))
    type_hw = db.Column(db.String(127))
    zn = db.Column(db.String(127))
    inv = db.Column(db.String(31))
    ues_id = db.Column(db.Integer, db.ForeignKey('ues_table.ues_id'))
    condition = db.Column(db.String(127))
    issue = db.Column(db.String(127))
    conclusion = db.Column(db.String(255))
    non_compliance = db.Column(db.String(255))
    description = db.Column(db.Text)
    employee = db.Column(db.String(255))
    #pdf = db.Column(db.LargeBinary)
    image_name = db.Column(db.String(255))  # поле для имени файла изображения
    event_id = db.Column(db.Integer, db.ForeignKey('events.event_id'))

    ues = db.relationship('UESTable', backref='akts')
    event = db.relationship('Events', backref='akts')

    def __repr__(self):
        return f'<Akt number={self.number_akt}>'

class Asudoo(db.Model):
    __tablename__ = 'asudoo'
    asudoo_id = db.Column(db.Integer, primary_key=True)
    branch_name_id = db.Column(db.Integer, db.ForeignKey('branch_name_table.branch_name_id'), nullable=False)
    rues_name_id = db.Column(db.Integer, db.ForeignKey('rues_name_table.rues_name_id'), nullable=False)
    ceh_name_id = db.Column(db.Integer, db.ForeignKey('ceh_name_table.ceh_name_id'), nullable=False)
    equipment_type_name_id = db.Column(db.Integer, db.ForeignKey('equipment_type_name_table.equipment_type_name_id'))
    equipment_name = db.Column(db.Text)
    mac = db.Column(db.String(127))
    inventory_number = db.Column(db.String(127))
    serial_number = db.Column(db.String(255))
    last_updated = db.Column(db.Date)
    cost = db.Column(db.String(31))
    chip_id = db.Column(db.String(127))
    document_date = db.Column(db.Date)
    status = db.Column(db.String(127))
    contractnum = db.Column(db.String(31))
    ttn_number = db.Column(db.String(127))
    ttn_date = db.Column(db.Date)
    number_part = db.Column(db.String(127))
    equipment_code = db.Column(db.String(127))

    branch = db.relationship('BranchNameTable', backref='asudoo')
    rues = db.relationship('RuesNameTable', backref='asudoo')
    ceh = db.relationship('CehNameTable', backref='asudoo')
    equipment_type = db.relationship('EquipmentTypeNameTable', backref='asudoo')

    def __repr__(self):
        return f'<Asudoo inventory_number={self.inventory_number}>'

class BranchNameTable(db.Model):
    __tablename__ = 'branch_name_table'
    branch_name_id = db.Column(db.Integer, primary_key=True)
    branch_name = db.Column(db.String(127), nullable=False, unique=True)

    def __repr__(self):
        return f'<Branch {self.branch_name}>'

class RuesNameTable(db.Model):
    __tablename__ = 'rues_name_table'
    rues_name_id = db.Column(db.Integer, primary_key=True)
    rues_name = db.Column(db.String(127), nullable=False, unique=True)

    def __repr__(self):
        return f'<RUES {self.rues_name}>'

class CehNameTable(db.Model):
    __tablename__ = 'ceh_name_table'
    ceh_name_id = db.Column(db.Integer, primary_key=True)
    ceh_name = db.Column(db.String(127), nullable=False, unique=True)

    def __repr__(self):
        return f'<Ceh {self.ceh_name}>'

class EquipmentTypeNameTable(db.Model):
    __tablename__ = 'equipment_type_name_table'
    equipment_type_name_id = db.Column(db.Integer, primary_key=True)
    equipment_type_name = db.Column(db.String(127), nullable=False, unique=True)

    def __repr__(self):
        return f'<EquipmentType {self.equipment_type_name}>'

class AsudooLink(db.Model):
    __tablename__ = 'asudoolink'
    asudoolink_id = db.Column(db.Integer, primary_key=True)
    asudoo_id = db.Column(db.Integer, db.ForeignKey('asudoo.asudoo_id'))
    mac = db.Column(db.String(255))
    sn = db.Column(db.String(255))
    doc_description = db.Column(db.Text)

    asudoo = db.relationship('Asudoo', backref='asudoo_links')
    hardware = db.relationship('Hardware', backref='asudoo_link')

    def __repr__(self):
        return f'<AsudooLink id={self.asudoolink_id}>'



class Spisanie(db.Model):
    __tablename__ = 'spisanie'
    spisanie_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    number_zakl = db.Column(db.Integer, nullable=True, default=0)
    date = db.Column(db.Date, nullable=False, default='2000-01-01')  # Замена '0000-00-00'
    region = db.Column(db.Text, nullable=True, default='0')
    type = db.Column(db.Text, nullable=True, default='0')
    serial_number = db.Column(db.Text, nullable=True, default='0')
    inventory_number = db.Column(db.Text, nullable=True, default='0')
    problem = db.Column(db.Text, nullable=True, default='0')
    TNPA = db.Column(db.Text, nullable=True, default='0')
    fio = db.Column(db.Text, nullable=True, default='0')
    status_zakl = db.Column(db.Text, nullable=False, default=' ')
    zalit = db.Column(db.String(10), nullable=False, default='0')
    description = db.Column(db.Text, nullable=False, default='0')
    mac = db.Column(db.Text, nullable=True, default='0')
    factory_num = db.Column(db.Text, nullable=True, default='0')

    def __repr__(self):
        return f'<Spisanie {self.id}>'
-- edit_akt.html
{% extends "base.html" %}
{% block title %}Редактирование акта{% endblock %}
{% block content %}
<h1>Редактирование акта: ID {{ akt.akt_id }}</h1>
<form method="POST" class="form-row">
    <div class="col-md-6">
        <div class="form-group">
            <label>Номер акта:</label>
            <input type="text" name="number_akt" value="{{ akt.number_akt }}" class="form-control">
        </div>
        <div class="form-group">
            <label>Дата акта:</label>
            <input type="date" name="date_akt" value="{{ akt.date_akt }}" class="form-control">
        </div>
        <div class="form-group">
            <label>Инвентарный номер:</label>
            <input type="text" name="inv" value="{{ akt.inv }}" class="form-control">
        </div>
        <div class="form-group">
            <label>Наименование:</label>
            <input type="text" name="name_hw" value="{{ akt.name_hw }}" class="form-control">
        </div>
        <div class="form-group">
            <label>Тип:</label>
            <input type="text" name="type_hw" value="{{ akt.type_hw }}" class="form-control">
        </div>
        <div class="form-group">
            <label>Серийный номер:</label>
            <input type="text" name="zn" value="{{ akt.zn }}" class="form-control">
        </div>
        <div class="form-group">
            <label>Состояние на момент осмотра:</label>
            <textarea name="condition" class="form-control">{{ akt.condition }}</textarea>
        </div>
        <div class="form-group">
            <label>Выявлено (неисправность):</label>
            <textarea name="issue" class="form-control">{{ akt.issue }}</textarea>
        </div>
        <div class="form-group">
            <label>Заключение:</label>
            <textarea name="non_compliance" class="form-control">{{ akt.non_compliance }}</textarea>
        </div>
        <div class="form-group">
            <label>Приложение:</label>
            <textarea name="conclusion" class="form-control">{{ akt.conclusion }}</textarea>
        </div>
        <button type="submit" class="btn btn-success">Сохранить</button>
        <a href="{{ url_for('akt_view_bp.view_akt') }}" class="btn btn-secondary">Вернуться</a>
    </div>
		<div class="col-md-6">
			{% if akt.image_name %}
				<img src="{{ url_for('uploaded_file', filename='akt_images/' ~ akt.image_name) }}" class="img-fluid" alt="Изображение акта">
			{% else %}
				<p>Изображение недоступно</p>
			{% endif %}
        <div class="mt-3">
            {% if prev_akt %}
            <a href="{{ url_for('akt_view_bp.edit_akt', akt_id=prev_akt.akt_id) }}" class="btn btn-primary">Предыдущий</a>
            {% endif %}
            {% if next_akt %}
            <a href="{{ url_for('akt_view_bp.edit_akt', akt_id=next_akt.akt_id) }}" class="btn btn-primary">Следующий</a>
            {% endif %}
        </div>
    </div>
</form>
{% endblock %}
-- view_akt.html
{% extends "base.html" %}
{% block title %}Просмотр всех актов{% endblock %}
{% block content %}
<h1>Просмотр актов</h1>
<form method="POST" class="form-inline mb-3">
    <select name="number_akt" class="form-control mr-2">
        <option value="">Выберите номер акта</option>
        {% for num in unique_numbers %}
        <option value="{{ num }}" {% if selected_filters.get('number_akt') == num %}selected{% endif %}>
            {{ num }}
        </option>
        {% endfor %}
    </select>
    <select name="date_akt" class="form-control mr-2">
        <option value="">Выберите дату акта</option>
        {% for date in unique_dates %}
        <option value="{{ date }}" {% if selected_filters.get('date_akt') == date %}selected{% endif %}>
            {{ date }}
        </option>
        {% endfor %}
    </select>
    <button type="submit" class="btn btn-primary">Применить</button>
</form>

<table class="table table-bordered">
    <thead>
        <tr>
            <th>ID</th>
            <th>Номер акта</th>
            <th>Дата акта</th>
            <th>Наименование</th>
        </tr>
    </thead>
    <tbody>
        {% for akt in akt_list %}
        <tr onclick="window.location='{{ url_for('akt_view_bp.edit_akt', akt_id=akt.akt_id) }}'">
            <td>{{ akt.akt_id }}</td>
            <td>{{ akt.number_akt }}</td>
            <td>{{ akt.date_akt }}</td>
            <td>{{ akt.name_hw }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}
# app/loaders/akt_loader.py

import os
import pytesseract
from pdf2image import convert_from_path
from PIL import Image
from app.models import db, Akt, Hardware, UESTable
from datetime import datetime
import re
from config import Config
from app.utils import parse_date, parse_int, parse_float, replace_similar_letters
from app.utils_db import get_or_create, process_cheked_hw
import pymorphy2
import logging


pytesseract.pytesseract.tesseract_cmd = r'D:\Program\Tesseract-OCR\tesseract.exe'
#morph = pymorphy2.MorphAnalyzer()

# Создаем логгер
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)

def parse_pdf_for_akt(filepath: str):
    """
    Функция для обработки PDF с актами, используя SQLAlchemy.
    """
    logger.debug(f"[DEBUG] parse_pdf_for_akt: Обрабатываем файл {filepath}")
    if not os.path.exists(filepath):
        logger.error(f"[ERROR] Файл PDF не найден: {filepath}")
        return

    # Конвертация PDF в изображения
    try:
        pages = convert_from_path(filepath, dpi=200)
    except Exception as e:
        logger.error(f"[ERROR] Ошибка при конвертации PDF в изображения: {e}")
        return
    
    # Папка для сохранения изображений актов
    images_folder = os.path.join(Config.UPLOAD_FOLDER, 'akt_images')
    if not os.path.exists(images_folder):
        os.makedirs(images_folder)

    page_count = 0
    akt_count = 0
    for page_index, page_img in enumerate(pages):
        page_count += 1
        # Обработка изображений и OCR
        # Предполагаем, что каждая страница содержит два акта: левый и правый
        # Поворот изображения на 90 градусов влево (против часовой стрелки)
        #page_img = page_img.rotate(90, expand=True)
        # для определения ориентации
        try:
            osd = pytesseract.image_to_osd(page_img)
            rotation = int(re.search('Orientation in degrees: (\d+)', osd).group(1))
            if rotation != 0:
                page_img = page_img.rotate(rotation, expand=True)
        except pytesseract.TesseractError as e:
            page_img = page_img.rotate(90, expand=True)
            logger.warning(f"[WARNING] Ошибка при определении поворота страницы: {e}. Поворачиваем на 90 влево.")
            
        # Размер изображения
        w, h = page_img.size
        # Разделяем изображение на левую и правую половины
        mid_x = w // 2
        left_img = page_img.crop((0, 0, mid_x, h))
        right_img = page_img.crop((mid_x, 0, w, h))

        # сохраняем при тестировании
        #page_img.save(f"page_{page_index+1}_rotated.png")
        #left_img.save(f"page_{page_index+1}_left.png")
        #right_img.save(f"page_{page_index+1}_right.png")
        # Обработка левой и правой частей
        for side, img in [('left', left_img), ('right', right_img)]:
            try:
                text = pytesseract.image_to_string(img, lang="rus+eng")
                logger.debug(f"[DEBUG] Обрабатываем страницу.")
            except pytesseract.TesseractError as e:
                logger.warning(f"[WARNING] Ошибка при обработке страницы: {e}. Пропускаем страницу.")
                continue  
            logger.debug(text)
            number_akt = find_number_akt_in_text(text)
            date_akt = find_date_in_text(text)
            inv = find_inv_in_text(text)
            image_name = save_akt_image(img, number_akt, images_folder)
            logger.debug(f"[DEBUG] Имя файла:{image_name}")
            logger.debug(f"[DEBUG] Текст:{text}")
            # Извлекаем дополнительные данные из текста
            name_hw = find_name_hw_in_text(text)
            type_hw = find_type_hw_in_text(text)
            zn = find_zn_in_text(text)
            ues_id = find_ues_id_in_text(text)
            condition, issue, non_compliance, conclusion = find_condition_and_issue_in_text(text)
            #condition, issue = find_condition_and_issue_in_text(text)
            #conclusion = find_conclusion_in_text(text)
            description = find_description_in_text(text)
            logger.debug(f"[DEBUG] Найдены данные: {number_akt}, {date_akt}, {inv}, {name_hw}, {type_hw}, {zn}, {ues_id}, {condition}, {issue}, {non_compliance}, {conclusion}, {description}")
            akt = insert_or_update_akt(
                number_akt=number_akt,
                date_akt=date_akt,
                inv=inv,
                name_hw=name_hw,
                type_hw=type_hw,
                zn=zn,
                ues_id=ues_id,
                condition=condition,
                non_compliance=non_compliance,
                issue=issue,
                conclusion=conclusion,
                description=description,
                image_name=image_name
            )
            akt_count += 1
            # Если inv найден, обновляем hardware.akt_id
            if inv:
                link_hardware_with_akt(inv, akt.akt_id)
    logger.debug(f"[DEBUG] parse_pdf_for_akt: обработка завершена. Найдено страниц: {page_count}, актов: {akt_count}.")

# ------------------------------------------------------------------------------
# Вспомогательные функции
# ------------------------------------------------------------------------------
def save_akt_image(img: Image.Image, number_akt: str, images_folder: str, size=(800, 800)) -> str:
    """
    Сохраняет изображение акта с именем, содержащим номер акта и стороны.
    Уменьшает изображение и преобразует в градации серого.
    Возвращает имя файла изображения.
    
    :param img: Исходное изображение
    :param number_akt: Номер акта
    :param images_folder: Папка для сохранения изображения
    :param size: Размер для уменьшения изображения (ширина, высота)
    """
    if not os.path.exists(images_folder):
        os.makedirs(images_folder)

    if number_akt:
        image_filename = f"akt_{number_akt}.png"
    else:
        # Используем текущее время, если номер акта неизвестен
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        image_filename = f"akt_{timestamp}.png"

    if not (isinstance(size, tuple) and len(size) == 2 and all(isinstance(i, (int, float)) for i in size)):
        raise ValueError("Параметр size должен быть кортежем из двух чисел (ширина, высота).")

    # Уменьшаем изображение
    img = img.convert("L")  # Преобразуем изображение в градации серого
    img.thumbnail(size, Image.LANCZOS)  # Уменьшаем изображение в соответствии с заданным размером

    image_path = os.path.join(images_folder, image_filename)
    img.save(image_path, format='PNG')  # Сохраняем в формате PNG
    logger.debug(f"[DEBUG] Изображение сохранено: {image_path}")
    return image_filename  # Сохраняем только имя файла, путь может быть стандартизирован

def find_number_akt_in_text(text: str) -> str:
    """
    Извлекает номер акта из OCR-текста.
    """
    pattern = re.compile(r"\s*№\s*(\d+)\s*от\s*", re.IGNORECASE)
    #pattern = re.compile(r"\s*(?:№|О|N|Ne|Н|НЕ)?\s*(\d+)[^\d]*", re.IGNORECASE)
    match = pattern.search(text)
    
    if match:
        return parse_int(match.group(1).replace('№', '').replace('от', ''))
    return ""

def find_date_in_text(text: str) -> str:
    """
    Извлекает дату из OCR-текста.
    """
    #pattern = re.compile(r"\b\d{1,2}[.\-/]\d{1,2}[.\-/]\d{2,4}\b")
    pattern = re.compile(r"\b(\d{1,2})[.\-/](\d{1,2})[.\-/](\d{2,4})\b")
    match = pattern.search(text)
    
    if match:
        #logger.debug('match дату:','.'.join(match.groups(1)))
        return '.'.join(match.groups(1))
    return ""

def find_inv_in_text(text: str) -> str:
    """
    Извлекает инвентарный номер (inv) из OCR-текста.
    """
    pattern = re.compile(r"Инвентарный\s+номер\s*(\d+\w?/?-?\d+\w?/?-?\d+\w?)", re.IGNORECASE)
    match = pattern.search(text)
    if match:
        
        return '.'.join(match.groups(1)).replace('Инвентарный', '').replace('номер', '').strip()
    return ""
def find_name_hw_in_text(text: str) -> str:
    """
    Извлекает name_hw из текста между "Наименование имущества" и "Тип".
    """
    return find_value_between_keywords(
        text,
        start_keyword="Наименование имущества",
        end_keyword="Тип",
        replacements={"ОМТ": "ONT"},
        remove_non_alnum=True
    )

def find_type_hw_in_text(text: str) -> str:
    """
    Извлекает type_hw из текста между "Тип" и "Заводской".
    """
    return find_value_between_keywords(
        text,
        start_keyword="Тип",
        end_keyword="Заводской",
        replacements={"СЕ": "CE"},
        remove_non_alnum=True
    )

def find_zn_in_text(text: str) -> str:
    """
    Извлекает zn из текста между "Заводской (серийный) номер" и "Год".
    """
    value = find_value_between_keywords(
        text,
        start_keyword="Заводской (серийный) номер",
        end_keyword="Год",
        replacements={"СЕ": "CE"},
        remove_non_alnum=True
    )
    # Заменяем похожие русские буквы на английские
    value = replace_similar_letters(value)
    
    return value

def find_ues_id_in_text(text: str) -> int:
    """
    Извлекает ues_id из текста между "Принадлежность имущества" и "Состояние".
    Учитывает падежи в русском языке для корректного соответствия.
    """
    ues_name = find_value_between_keywords(
        text,
        start_keyword="Принадлежность имущества",
        end_keyword="Состояние",
        remove_non_alnum=True
    )
    ues_name = ues_name.strip()
    if not ues_name:
        return None

    # Приводим извлечённый ues_name к начальной форме
    ues_name_normalized = normalize_text(ues_name)
    logger.debug(f"[DEBUG] ues_name_normalized: {ues_name_normalized}")
    # Ищем соответствие в базе данных среди ues_name, приведённых к начальной форме
    ues_list = UESTable.query.all()
    for ues in ues_list:
        ues_db_normalized = normalize_text(ues.ues_name)
        logger.debug(f"[DEBUG] ues_db_normalized: {ues_db_normalized}")
        if ues_name_normalized == ues_db_normalized:
            return ues.ues_id

    # Если не нашли соответствие, можно вернуть None или создать новую запись
    # Здесь возвращаем None
    return None

def normalize_text(text: str) -> str:
    """
    Приводит текст к нижнему регистру и нормальной форме (лемматизация).
    Удаляет лишние символы.

    :param text: Исходный текст.
    :return: Нормализованный текст.
    """
    # Удаляем нежелательные символы
    text = re.sub(r'[^\w\s]', '', text)
    # Разбиваем на слова
    words = text.split()
    # Лемматизируем каждое слово
    #normalized_words = [morph.parse(word)[0].normal_form for word in words]
    # Собираем обратно в строку
    #normalized_text = ' '.join(normalized_words)
    #return normalized_text.lower()

def find_condition_and_issue_in_text(text: str) -> tuple:
    """
    Извлекает 4 значения из текста: состояние, неисправность, несоответствие и заключение.
    """
    # Инициализация результатов
    condition = ""
    issue = ""
    non_compliance = ""
    conclusion = ""

    # Шаблон для извлечения состояния
    condition_pattern = re.compile(r"Состояние на момент осмотра(.*?)(Выявлено|Заключение|$)", re.DOTALL | re.IGNORECASE)
    condition_match = condition_pattern.search(text)
    if condition_match:
        condition = condition_match.group(1).strip()
    else:
        logger.debug("[DEBUG] Не удалось найти cостояние в тексте")
        
    # Шаблон для извлечения неисправности
    issue_pattern = re.compile(r"Выявлено \(неисправность\)(.*?)(Не соответствует|Заключение|$)", re.DOTALL | re.IGNORECASE)
    issue_match = issue_pattern.search(text)
    if issue_match:
        issue = issue_match.group(1).strip()
    else:
        logger.debug("[DEBUG] Не удалось найти неисправность в тексте")
        
    # Шаблон для извлечения несоответствия
    non_compliance_pattern = re.compile(r"(Не соответствует.*?Заключение\s*\S+)", re.DOTALL | re.IGNORECASE)
    non_compliance_match = non_compliance_pattern.search(text)
    if non_compliance_match:
        non_compliance = non_compliance_match.group(1).strip()
    else:
        logger.debug("[DEBUG] Не удалось найти несоответствие в тексте")

    # Шаблон для извлечения заключения
    conclusion_pattern = re.compile(r"Не подлежит(.*?)(Приложение|\nПриложение|$)", re.DOTALL | re.IGNORECASE)
    conclusion_match = conclusion_pattern.search(text)
    if conclusion_match:
        conclusion = "Не подлежит " + conclusion_match.group(1).strip()
    else:
        logger.debug("[DEBUG] Не удалось найти заключение в тексте")

    # Возвращаем результаты
    return re.sub(r'\s+', ' ', condition),\
           re.sub(r'\s+', ' ', issue), \
           re.sub(r'\s+', ' ', non_compliance.replace('Заключение', '')), \
           re.sub(r'\s+', ' ', conclusion)


def find_description_in_text(text: str) -> str:
    """
    Сохраняет весь распознанный текст.
    """
    return text

# Дополнительные вспомогательные функции

def find_value_between_keywords(text: str, start_keyword: str, end_keyword: str, replacements=None, remove_non_alnum=False):
    """
    Извлекает текст между двумя ключевыми словами.

    :param text: Исходный текст.
    :param start_keyword: Начальное ключевое слово.
    :param end_keyword: Конечное ключевое слово.
    :param replacements: Словарь замен {старое: новое}.
    :param remove_non_alnum: Удалять ли не буквенно-цифровые символы в конце.
    :return: Извлеченный и обработанный текст.
    """
    pattern = re.compile(
        re.escape(start_keyword) + r"(.*?)" + re.escape(end_keyword),
        re.DOTALL | re.IGNORECASE
    )
    match = pattern.search(text)
    if match:
        value = match.group(1).strip()
        if replacements:
            value = apply_replacements(value, replacements)
        if remove_non_alnum:
            value = value.rstrip('.,!?:;\'"«»“”’`~@#$%^&*()_+-=[]{}|\\/<>')
        return value.strip()
    return ""

def apply_replacements(text: str, replacements: dict) -> str:
    """
    Применяет замены в тексте по словарю.

    :param text: Исходный текст.
    :param replacements: Словарь замен {старое: новое}.
    :return: Текст после замены.
    """
    for old, new in replacements.items():
        text = text.replace(old, new)
    return text

def clean_line(line: str) -> str:
    """
    Удаляет в конце строки все не буквенные и не цифровые символы.

    :param line: Исходная строка.
    :return: Очищенная строка.
    """
    # Оставляем только буквы и цифры в конце строки
    return re.sub(r'[^a-zA-Zа-яА-Я0-9]+$', '', line).strip()


def insert_or_update_akt(number_akt: str, date_akt: str, inv: str, name_hw: str = None,
                         type_hw: str = None, zn: str = None, ues_id: int = None,
                         condition: str = None, issue: str = None, non_compliance: str = None, conclusion: str = None,
                         description: str = None, image_name: str = None):
    """
    Создает или обновляет запись в таблице Akt с использованием ORM.
    Возвращает объект Akt.
    """
    date_akt_parsed = parse_date(date_akt)
    akt = Akt(
        number_akt=number_akt,
        date_akt=date_akt_parsed,
        inv=inv,
        name_hw=name_hw,
        type_hw=type_hw,
        zn=zn,
        ues_id=ues_id,
        condition=condition,
        issue=issue,
        conclusion=conclusion,
        description=description,
        image_name=image_name
    )
    db.session.add(akt)
    try:
        db.session.commit()
    except Exception as e:
        db.session.rollback()
        logger.error(f"[ERROR] Ошибка при вставке Akt: {e}")
    return akt

def link_hardware_with_akt(inv: str, akt_id: int):
    """
    Привязать запись Hardware к акту через поле hardware.akt_id.
    """
    hardware = Hardware.query.filter_by(inv=inv).first()
    if hardware:
        hardware.akt_id = akt_id
        try:
            db.session.commit()
        except Exception as e:
            db.session.rollback()
            logger.error(f"[ERROR] Ошибка при обновлении Hardware (inv={inv}): {e}")
    else:
        logger.warning(f"[WARN] Не найдено Hardware с inv={inv} для привязки к Akt.")
# app/blueprints/akt_bp.py

import os
from flask import Blueprint, request, render_template, redirect, url_for, flash
from config import Config
from app.loaders.akt_loader import parse_pdf_for_akt


akt_bp = Blueprint("akt_bp", __name__, template_folder="../../templates")

def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in Config.ALLOWED_EXTENSIONS

@akt_bp.route("/upload_akt", methods=["GET", "POST"])
def upload_akt():
    if request.method == "POST":
        file_pdf = request.files.get("file_pdf")
        if file_pdf and allowed_file(file_pdf.filename):
            filename_pdf = file_pdf.filename
            path_pdf = os.path.join(Config.UPLOAD_FOLDER, filename_pdf)
            file_pdf.save(path_pdf)
            parse_pdf_for_akt(path_pdf)
            flash("Файл PDF (акты) обработан")
        return redirect(url_for("akt_bp.upload_akt"))
    return render_template("upload_akt.html")
	from flask import Blueprint, request, render_template, redirect, url_for, flash
	
# app/blueprints/akt_view_bp.py	
from app.models import db, Akt
from sqlalchemy import or_
import datetime

akt_view_bp = Blueprint('akt_view_bp', __name__, template_folder="../../templates")

@akt_view_bp.route('/akt', methods=['GET', 'POST'])
def view_akt():
    """
    Страница просмотра всех актов с фильтром.
    """
    filters = {}
    if request.method == 'POST':
        number_akt = request.form.get('number_akt')
        date_akt = request.form.get('date_akt')
        if number_akt:
            filters['number_akt'] = number_akt
        if date_akt:
            filters['date_akt'] = date_akt

    # Фильтрация данных
    query = Akt.query
    if filters.get('number_akt'):
        query = query.filter(Akt.number_akt == filters['number_akt'])
    if filters.get('date_akt'):
        query = query.filter(Akt.date_akt == filters['date_akt'])
    akt_list = query.all()

    # Данные для выпадающих списков
    unique_numbers = [akt.number_akt for akt in Akt.query.distinct(Akt.number_akt).all()]
    unique_dates = [akt.date_akt for akt in Akt.query.distinct(Akt.date_akt).all()]

    return render_template(
        'view_akt.html',
        akt_list=akt_list,
        unique_numbers=unique_numbers,
        unique_dates=unique_dates,
        selected_filters=filters
    )

@akt_view_bp.route('/akt/<int:akt_id>', methods=['GET', 'POST'])
def edit_akt(akt_id):
    """
    Страница редактирования акта.
    """
    akt = Akt.query.get_or_404(akt_id)
    if request.method == 'POST':
        try:
            akt.number_akt = request.form.get('number_akt')
            #akt.date_akt = request.form.get('date_akt')
            # Преобразование даты в объект datetime.date
            date_str = request.form.get('date_akt')
            if date_str:
                akt.date_akt = datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
            
            akt.inv = request.form.get('inv')
            akt.name_hw = request.form.get('name_hw')
            akt.type_hw = request.form.get('type_hw')
            akt.zn = request.form.get('zn')
            akt.condition = request.form.get('condition')
            akt.issue = request.form.get('issue')
            akt.conclusion = request.form.get('conclusion')
            akt.non_compliance = request.form.get('non_compliance')
            #akt.description = request.form.get('description')
            db.session.commit()
            flash('Изменения успешно сохранены!', 'success')
            return redirect(url_for('akt_view_bp.view_akt'))
        except Exception as e:
            db.session.rollback()
            flash(f'Ошибка сохранения данных: {e}', 'danger')

    # Для кнопок перехода к следующему и предыдущему
    next_akt = Akt.query.filter(Akt.akt_id > akt_id).order_by(Akt.akt_id).first()
    prev_akt = Akt.query.filter(Akt.akt_id < akt_id).order_by(Akt.akt_id.desc()).first()
    
    #print(akt.image_name)
    return render_template(
        'edit_akt.html',
        akt=akt,
        next_akt=next_akt,
        prev_akt=prev_akt
    )