Привет. Сейчас я дам тебе данные по своему проекту, а потом буду задавать по нему вопросы. Просто сохрани его у себя в памяти, а будешь делать обработку после того, как я задам вопрос.
Проект:
web_app_hw_out/
├── app/
│   ├── blueprints/
│   │   ├── akt_bp.py
│   │   ├── asudoo_bp.py
│   │   ├── hardware_bp.py
│   │   ├── os_bp.py
│   │   ├── reconcile_bp.py
│   │   ├── report_bp.py
│   │   ├── __init__.py
│   ├── hardware_db.sqlite
│   ├── hardware_db1.sqlite
│   ├── loaders/
│   │   ├── akt_loader.py
│   │   ├── asudoo_loader.py
│   │   ├── hardware_loader.py
│   │   ├── os_loader.py
│   ├── models.py
│   ├── schemas.py
│   ├── static/
│   │   ├── css/
│   │   │   └── bootstrap.min.css
│   │   └── js/
│   │       ├── bootstrap.bundle.min.js
│   │       └── jquery.slim.min.js
│   ├── templates/
│   │   ├── audit_report.html
│   │   ├── base.html
│   │   ├── confirm_hardware.html
│   │   ├── index.html
│   │   ├── inventory_report.html
│   │   ├── inventory_report_form.html
│   │   ├── reconcile_doc.html
│   │   ├── reconcile_hw.html
│   │   ├── reconcile_index.html
│   │   ├── update_asudoo.html
│   │   ├── upload_akt.html
│   │   ├── upload_hardware.html
│   │   └── upload_os.html
│   ├── uploads/
│   ├── __init__.py
├── config.py
├── create_db.py
├── main.py
├── requirements.txt
# file:web_app_hw_out\app\__init__.py
# app/__init__.py
from flask import Flask, render_template
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow  # Добавляем импорт Marshmallow
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
ma = Marshmallow()  # Создаем экземпляр Marshmallow
def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)
    db.init_app(app)
    migrate.init_app(app, db)
    ma.init_app(app)  # Инициализируем Marshmallow с приложением
    # Регистрируем Blueprint'ы
    from app.blueprints import register_blueprints
    register_blueprints(app)
    # Главная страница
    @app.route("/")
    def index():
        return render_template("index.html")
    return app
# file:web_app_hw_out\app\blueprints\__init__.py
# app/blueprints/__init__.py
from .hardware_bp import hardware_bp
from .os_bp import os_bp
from .akt_bp import akt_bp
from .asudoo_bp import asudoo_bp
from .reconcile_bp import reconcile_bp
from .report_bp import report_bp
def register_blueprints(app):
    app.register_blueprint(hardware_bp)
    app.register_blueprint(os_bp)
    app.register_blueprint(akt_bp)
    app.register_blueprint(asudoo_bp)
    app.register_blueprint(reconcile_bp)
    app.register_blueprint(report_bp)
# file:web_app_hw_out\app\blueprints\akt_bp.py
# 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")
# file:web_app_hw_out\app\blueprints\asudoo_bp.py
# app/blueprints/asudoo_bp.py
from flask import Blueprint, request, render_template, redirect, url_for, flash
from config import Config
from app.loaders.asudoo_loader import parse_csv_for_asudoo
asudoo_bp = Blueprint("asudoo_bp", __name__, template_folder="../../templates")
@asudoo_bp.route("/update_asudoo", methods=["GET", "POST"])
def update_asudoo():
    if request.method == "POST":
        csv_path = request.form.get("csv_path") or Config.ASUDOO_CSV_DEFAULT_PATH
        parse_csv_for_asudoo(csv_path)
        flash("Таблица ASUDOO обновлена")
        return redirect(url_for("asudoo_bp.update_asudoo"))
    else:
        # Передаем путь по умолчанию в шаблон
        return render_template("update_asudoo.html", default_csv_path=Config.ASUDOO_CSV_DEFAULT_PATH)
# file:web_app_hw_out\app\blueprints\hardware_bp.py
# app/blueprints/hardware_bp.py
from flask import (
    Blueprint,
    request,
    render_template,
    redirect,
    url_for,
    flash,
    session
)
from app.models import (
    db,
    Landocs,
    Hardware,
    DescriptionTable,
    KomplektTable,
    OsnovanieTable
)
from app.loaders.hardware_loader import (
    parse_xlsx_for_hardware_data,
    parse_docx_for_hardware_data
)
from config import Config
import os
import datetime
import json
hardware_bp = Blueprint("hardware_bp", __name__, template_folder="../../templates")
def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in Config.ALLOWED_EXTENSIONS
@hardware_bp.route("/upload_hardware", methods=["GET", "POST"])
def upload_hardware():
    if request.method == "POST":
        # Check if this is the confirmation step
        if 'confirm' in request.form:
            # Handle confirmation and save to DB
            parsed_data_json = session.get('parsed_hardware')
            if not parsed_data_json:
                flash("Нет данных для подтверждения.")
                return redirect(url_for("hardware_bp.upload_hardware"))
            parsed_data = json.loads(parsed_data_json)
            accept_entries = request.form.getlist('accept')
            for idx, entry in enumerate(parsed_data):
                if str(idx) in accept_entries:
                    # Check if Hardware with the same inv already exists
                    existing_hw = Hardware.query.filter_by(inv=entry.get("inv")).first()
                    if existing_hw:
                        flash(f"Запись с inv={entry.get('inv')} уже существует. Пропущено сохранение.")
                        continue
                    # Save this entry to the database
                    try:
                        # Handle related fields
                        # Get or create KomplektTable
                        komplekt_text = entry.get("komplekt_text")
                        komplekt = None
                        if komplekt_text:
                            komplekt = KomplektTable.query.filter_by(komplekt_text=komplekt_text).first()
                            if not komplekt:
                                komplekt = KomplektTable(komplekt_text=komplekt_text)
                                db.session.add(komplekt)
                                db.session.flush()  # To get komplekt_id
                        # Get or create OsnovanieTable
                        osnovanie_text = entry.get("osnovanie_text")
                        osnovanie = None
                        if osnovanie_text:
                            osnovanie = OsnovanieTable.query.filter_by(osnovanie_txt=osnovanie_text).first()
                            if not osnovanie:
                                osnovanie = OsnovanieTable(osnovanie_txt=osnovanie_text)
                                db.session.add(osnovanie)
                                db.session.flush()
                        # Get or create DescriptionTable
                        description_text = entry.get("description_text")
                        description = None
                        if description_text:
                            description = DescriptionTable.query.filter_by(description_text=description_text).first()
                            if not description:
                                description = DescriptionTable(description_text=description_text)
                                db.session.add(description)
                                db.session.flush()
                        # Create Hardware entry
                        hardware = Hardware(
                            doc_id=entry.get("doc_id"),
                            no=entry.get("no"),
                            name=entry.get("name"),
                            inv=entry.get("inv"),
                            quantity=entry.get("quantity"),
                            shifr_time=entry.get("shifr_time"),
                            shifr_amo=entry.get("shifr_amo"),
                            date_build=entry.get("date_build"),
                            date_input=entry.get("date_input"),
                            date_buh=entry.get("date_buh"),
                            date_nouse=entry.get("date_nouse"),
                            first_price=entry.get("first_price"),
                            last_price=entry.get("last_price"),
                            srok=entry.get("srok"),
                            komplekt_id=komplekt.komplekt_id if komplekt else None,
                            osnovanie_id=osnovanie.osnovanie_id if osnovanie else None,
                            description_id=description.description_id if description else None,
                            ues_description=entry.get("ues_text"),
                            sn=entry.get("sn"),
                            mac=entry.get("mac"),
                            kud=entry.get("kud")
                        )
                        db.session.add(hardware)
                    except Exception as e:
                        db.session.rollback()
                        flash(f"Ошибка при сохранении записи inv={entry.get('inv')}: {e}")
            try:
                db.session.commit()
                flash("Выбранные данные успешно сохранены.")
                session.pop('parsed_hardware', None)
                return redirect(url_for("hardware_bp.upload_hardware"))
            except Exception as e:
                db.session.rollback()
                flash(f"Ошибка при сохранении данных: {e}")
                return redirect(url_for("hardware_bp.upload_hardware"))
        # Handle initial file upload and parsing
        file_xlsx = request.files.get("file_xlsx")
        file_docx = request.files.get("file_docx")
        doc_name = request.form.get("doc_name")
        doc_date = request.form.get("doc_date")
        doc_description = request.form.get("doc_description")
        # Validate document information
        if not doc_name or not doc_date:
            flash("Название и дата документа обязательны.")
            return redirect(url_for("hardware_bp.upload_hardware"))
        try:
            doc_date_parsed = datetime.datetime.strptime(doc_date, '%Y-%m-%d').date()
        except ValueError:
            flash("Неверный формат даты документа. Используйте YYYY-MM-DD.")
            return redirect(url_for("hardware_bp.upload_hardware"))
        # Проверка/создание LANDOCS
        landoc = Landocs.query.filter_by(doc_name=doc_name, doc_date=doc_date_parsed).first()
        if landoc:
            doc_id = landoc.doc_id
            flash(f"Документ уже существует, используем doc_id={doc_id}")
        else:
            landoc = Landocs(doc_name=doc_name, doc_date=doc_date_parsed, doc_description=doc_description)
            db.session.add(landoc)
            db.session.commit()
            doc_id = landoc.doc_id
            flash(f"Создан новый документ doc_id={doc_id}")
        # Parse XLSX and DOCX files
        parsed_hardware = []
        if file_xlsx and allowed_file(file_xlsx.filename):
            filename_xlsx = file_xlsx.filename
            path_xlsx = os.path.join(Config.UPLOAD_FOLDER, filename_xlsx)
            file_xlsx.save(path_xlsx)
            parsed_xlsx = parse_xlsx_for_hardware_data(path_xlsx, doc_id)
            parsed_hardware.extend(parsed_xlsx)
            flash("Файл XLSX обработан")
        if file_docx and allowed_file(file_docx.filename):
            filename_docx = file_docx.filename
            path_docx = os.path.join(Config.UPLOAD_FOLDER, filename_docx)
            file_docx.save(path_docx)
            parsed_docx = parse_docx_for_hardware_data(path_docx, doc_id)
            # Merge parsed_docx into parsed_hardware based on 'inv'
            for docx_entry in parsed_docx:
                matched = False
                for hw_entry in parsed_hardware:
                    if hw_entry['inv'] == docx_entry['inv']:
                        hw_entry['description_text'] = (hw_entry.get('description_text') or "") + "\n" + docx_entry.get('description_text', '')
                        matched = True
                        break
                if not matched:
                    # If no matching inv in XLSX, add as new entry
                    parsed_hardware.append({
                        "doc_id": doc_id,
                        "no": None,
                        "name": None,
                        "inv": docx_entry['inv'],
                        "quantity": None,
                        "shifr_time": None,
                        "shifr_amo": None,
                        "date_build": None,
                        "date_input": None,
                        "date_buh": None,
                        "date_nouse": None,
                        "first_price": None,
                        "last_price": None,
                        "srok": None,
                        "komplekt_text": None,
                        "osnovanie_text": None,
                        "description_text": docx_entry.get('description_text', ''),
                        "ues_text": None,
                        "sn": None,
                        "mac": None,
                        "kud": None
                    })
            flash("Файл DOCX обработан")
        if not parsed_hardware:
            flash("Не найдено данных для обработки.")
            return redirect(url_for("hardware_bp.upload_hardware"))
        # Store parsed data in session (as JSON)
        session['parsed_hardware'] = json.dumps(parsed_hardware, default=str)
        return render_template("confirm_hardware.html", hardware_list=parsed_hardware)
    else:
        # GET request: Render the upload form with existing document names
        doc_names = [doc.doc_name for doc in Landocs.query.distinct(Landocs.doc_name).all()]
        return render_template("upload_hardware.html", doc_names=doc_names)
# file:web_app_hw_out\app\blueprints\os_bp.py
# app/blueprints/os_bp.py
import os
from flask import Blueprint, request, render_template, redirect, url_for, flash
from config import Config
from app.loaders.os_loader import parse_csv_for_os
os_bp = Blueprint("os_bp", __name__, template_folder="../../templates")
def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in Config.ALLOWED_EXTENSIONS
@os_bp.route("/upload_os", methods=["GET", "POST"])
def upload_os():
    if request.method == "POST":
        file_csv = request.files.get("file_csv")
        if file_csv and allowed_file(file_csv.filename):
            filename_csv = file_csv.filename
            path_csv = os.path.join(Config.UPLOAD_FOLDER, filename_csv)
            file_csv.save(path_csv)
            parse_csv_for_os(path_csv)
            flash("Файл CSV для OS обработан")
        return redirect(url_for("os_bp.upload_os"))
    return render_template("upload_os.html")
# file:web_app_hw_out\app\blueprints\reconcile_bp.py
# app/blueprints/reconcile_bp.py
from flask import Blueprint, render_template, request, redirect, url_for, flash
from datetime import datetime
from app import db
from app.models import Hardware, OS, Akt, ChekedHW, Landocs
reconcile_bp = Blueprint("reconcile_bp", __name__, template_folder="../../templates")
@reconcile_bp.route("/reconcile", methods=["GET"])
def reconcile_index():
    """
    Главная страница для сверки: выбор документа
    """
    # Получаем все документы из LANDOCS
    landocs = Landocs.query.all()
    return render_template("reconcile_index.html", landocs=landocs)
@reconcile_bp.route("/reconcile/doc/<int:doc_id>", methods=["GET"])
def reconcile_doc(doc_id):
    # Получаем документ LANDOCS
    doc = Landocs.query.get(doc_id)
    if not doc:
        flash("Документ не найден")
        return redirect(url_for("reconcile_bp.reconcile_index"))
    # Получаем список оборудования, связанного с данным документом, включая связанные записи в ChekedHW
    hw_list = db.session.query(Hardware).filter_by(doc_id=doc_id).outerjoin(ChekedHW).add_columns(
        ChekedHW.date_checked_akt,
        ChekedHW.date_checked_os,
        Hardware.chekedhw_id
    ).order_by(Hardware.inv).all()
    return render_template("reconcile_doc.html", doc=doc, hw_list=hw_list)
@reconcile_bp.route("/reconcile/hw/<inv>", methods=["GET"])
def reconcile_hw(inv):
    # Получаем Hardware вместе с ChekedHW
    hw = db.session.query(Hardware).filter_by(inv=inv).outerjoin(ChekedHW).add_columns(
        Hardware,
        ChekedHW.date_checked_akt,
        ChekedHW.date_checked_os
    ).first()
    if not hw:
        flash(f"Hardware не найдено, inv={inv}")
        return redirect(url_for("reconcile_bp.reconcile_index"))
    
    hardware = hw[0]
    date_checked_akt = hw.date_checked_akt
    date_checked_os = hw.date_checked_os
    # Получаем OS
    os_ = OS.query.filter_by(inv=inv).first()
    # Получаем AKT
    akt = Akt.query.get(hardware.akt_id) if hardware.akt_id else None
    # Формируем HTML-блоки
    hw_html = build_hardware_block(hardware, date_checked_akt, date_checked_os)
    os_html = build_os_block(hardware, os_)
    akt_html = build_akt_block(hardware, akt)
    return render_template(
        "reconcile_hw.html",
        inv=inv,
        hw_html=hw_html,
        os_html=os_html,
        akt_html=akt_html
    )
def build_hardware_block(hw, date_checked_akt, date_checked_os):
    html = f"""
    <h2>Данные из Hardware</h2>
    <ul>
      <li>name: {hw.name}</li>
      <li>quantity: {hw.quantity}</li>
      <li>shifr_amo: {hw.shifr_amo}</li>
      <li>date_build: {hw.date_build}</li>
      <li>date_input: {hw.date_input}</li>
      <li>srok: {hw.srok}</li>
      <li>date_checked_os: {date_checked_os or '—'}</li>
      <li>date_checked_akt: {date_checked_akt or '—'}</li>
    </ul>
    """
    return html
def build_os_block(hw, os_):
    if not os_:
        return '<p style="color:red;">Нет данных в OS для данного inv</p>'
    
    def highlight_diff(hw_val, os_val):
        if str(hw_val or "").strip() != str(os_val or "").strip():
            return f'<span style="color:red;">{os_val}</span>'
        else:
            return f'{os_val}'
    
    html = f"""
    <h2>Данные из OS (inv={os_.inv})</h2>
    <ul>
      <li>model: {highlight_diff(hw.name, os_.model)}</li>
      <li>quantity: {highlight_diff(hw.quantity, os_.quantity)}</li>
      <li>code_norma_amort -> shifr_amo: {highlight_diff(hw.shifr_amo, os_.code_norma_amort)}</li>
      <li>date_production -> date_build: {highlight_diff(hw.date_build, os_.date_production)}</li>
      <li>date_input: {highlight_diff(hw.date_input, os_.date_input)}</li>
      <li>srok_polezn_use -> srok: {highlight_diff(hw.srok, os_.srok_polezn_use)}</li>
    </ul>
    <!-- Кнопки -->
    <form method="POST" action="{url_for('reconcile_bp.reconcile_hw_apply_os', inv=hw.inv)}">
        <button type="submit">Применить к Hardware данные из OS</button>
    </form>
    <form method="POST" action="{url_for('reconcile_bp.reconcile_hw_check_os', inv=hw.inv)}">
        <button type="submit">Сверено с ОС</button>
    </form>
    """
    return html
def build_akt_block(hw, akt):
    if not akt:
        return '<p style="color:red;">Нет записи в AKT для данного inv</p>'
    
    def highlight_diff(hw_val, akt_val):
        if str(hw_val or "").strip() != str(akt_val or "").strip():
            return f'<span style="color:red;">{akt_val}</span>'
        else:
            return f'{akt_val}'
    
    html = f"""
    <h2>Данные из AKT (akt_id={akt.akt_id})</h2>
    <ul>
      <li>name_hw: {highlight_diff(hw.name, akt.name_hw)}</li>
      <li>inv: {highlight_diff(hw.inv, akt.inv)}</li>
      <li>number_akt: {akt.number_akt}</li>
      <li>date_akt: {akt.date_akt}</li>
    </ul>
    <form method="POST" action="{url_for('reconcile_bp.reconcile_hw_check_akt', inv=hw.inv)}">
      <button type="submit">Сверено с актом</button>
    </form>
    """
    return html
@reconcile_bp.route("/reconcile/hw/<inv>/apply_os", methods=["POST"])
def reconcile_hw_apply_os(inv):
    hw = Hardware.query.filter_by(inv=inv).first()
    if not hw:
        flash("Hardware не найдено.")
        return redirect(url_for("reconcile_bp.reconcile_index"))
    os_ = OS.query.filter_by(inv=inv).first()
    if not os_:
        flash("OS не найдена, нечего применять.")
        return redirect(url_for("reconcile_bp.reconcile_hw", inv=inv))
    # Обновляем поля Hardware на основе OS
    hw.name = os_.model or hw.name
    hw.quantity = os_.quantity or hw.quantity
    hw.shifr_amo = os_.code_norma_amort or hw.shifr_amo
    hw.date_build = os_.date_production or hw.date_build
    hw.date_input = os_.date_input or hw.date_input
    hw.srok = os_.srok_polezn_use or hw.srok
    try:
        db.session.commit()
        flash("Данные из OS применены к Hardware.")
    except Exception as e:
        db.session.rollback()
        flash(f"Ошибка при сохранении данных: {e}")
    return redirect(url_for("reconcile_bp.reconcile_hw", inv=inv))
@reconcile_bp.route("/reconcile/hw/<inv>/check_os", methods=["POST"])
def reconcile_hw_check_os(inv):
    hw = Hardware.query.filter_by(inv=inv).first()
    if not hw:
        flash("Hardware не найдено.")
        return redirect(url_for("reconcile_bp.reconcile_index"))
    # Получаем или создаем ChekedHW
    if hw.chekedhw_id:
        chekedhw = ChekedHW.query.get(hw.chekedhw_id)
        if not chekedhw:
            chekedhw = ChekedHW()
            db.session.add(chekedhw)
            hw.chekedhw = chekedhw
    else:
        chekedhw = ChekedHW()
        db.session.add(chekedhw)
        hw.chekedhw = chekedhw
    # Обновляем дату сверки с OS
    chekedhw.date_checked_os = datetime.utcnow().date()
    try:
        db.session.commit()
        flash("Сверено с ОС (дата проставлена).")
    except Exception as e:
        db.session.rollback()
        flash(f"Ошибка при сохранении данных: {e}")
    return redirect(url_for("reconcile_bp.reconcile_hw", inv=inv))
@reconcile_bp.route("/reconcile/hw/<inv>/check_akt", methods=["POST"])
def reconcile_hw_check_akt(inv):
    hw = Hardware.query.filter_by(inv=inv).first()
    if not hw:
        flash("Hardware не найдено.")
        return redirect(url_for("reconcile_bp.reconcile_index"))
    if hw.chekedhw_id:
        chekedhw = ChekedHW.query.get(hw.chekedhw_id)
        if not chekedhw:
            chekedhw = ChekedHW()
            db.session.add(chekedhw)
            hw.chekedhw = chekedhw
    else:
        chekedhw = ChekedHW()
        db.session.add(chekedhw)
        hw.chekedhw = chekedhw
    chekedhw.date_checked_akt = datetime.utcnow().date()
    try:
        db.session.commit()
        flash("Сверено с актом (дата проставлена).")
    except Exception as e:
        db.session.rollback()
        flash(f"Ошибка при сохранении данных: {e}")
    return redirect(url_for("reconcile_bp.reconcile_hw", inv=inv))
# file:web_app_hw_out\app\blueprints\report_bp.py
# app/blueprints/report_bp.py
from flask import Blueprint, jsonify, render_template, request, send_file
from app.models import Hardware, OS
from app.schemas import HardwareSchema
from app import db
import io
import pandas as pd
report_bp = Blueprint("report_bp", __name__)
hardware_schema = HardwareSchema()
hardwares_schema = HardwareSchema(many=True)
@report_bp.route("/inventory_report", methods=["GET", "POST"])
def inventory_report():
    if request.method == "POST":
        # Получаем параметры фильтра из формы
        equipment_type = request.form.get('equipment_type')
        date_from = request.form.get('date_from')
        date_to = request.form.get('date_to')
        export_format = request.form.get('export_format', 'html')
        # Формируем запрос
        query = Hardware.query
        if equipment_type:
            query = query.filter(Hardware.type_hw == equipment_type)
        if date_from:
            query = query.filter(Hardware.date_input >= date_from)
        if date_to:
            query = query.filter(Hardware.date_input <= date_to)
        # Выполняем запрос
        hardware_list = query.all()
        if export_format == 'html':
            # Отображаем HTML-шаблон
            return render_template('inventory_report.html', hardware_list=hardware_list)
        elif export_format == 'excel':
            # Экспорт в Excel
            return export_hardware_to_excel(hardware_list)
        elif export_format == 'csv':
            # Экспорт в CSV
            return export_hardware_to_csv(hardware_list)
        else:
            # По умолчанию возвращаем JSON
            data = hardwares_schema.dump(hardware_list)
            return jsonify(data)
        
        # Пагинация
        page = request.args.get('page', 1, type=int)
        per_page = 50
        hardware_list = query.paginate(page=page, per_page=per_page)
        return render_template('inventory_report.html', hardware_list=hardware_list)
    
    else:
        # GET-запрос: отображаем форму для ввода параметров
        return render_template('inventory_report_form.html')
@report_bp.route("/report_spisanie", methods=["GET"])
def report_spisanie():
    rows_zero = Hardware.query.filter(Hardware.last_price == 0).all()
    rows_nonzero = Hardware.query.filter(Hardware.last_price != 0).all()
    
    zero_list = [hardware_to_dict(hw) for hw in rows_zero]
    nonzero_list = [hardware_to_dict(hw) for hw in rows_nonzero]
    
    return jsonify({
        "spisanie_zero_price": zero_list,
        "spisanie_nonzero_price": nonzero_list
    })
def export_hardware_to_excel(hardware_list):
    # Преобразуем данные в DataFrame
    df = pd.DataFrame([hardware_to_dict(hw) for hw in hardware_list])
    # Используем буфер памяти для хранения данных Excel
    output = io.BytesIO()
    with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
        df.to_excel(writer, index=False, sheet_name='Inventory')
        writer.save()
        output.seek(0)
    return send_file(output, attachment_filename='inventory_report.xlsx', as_attachment=True)
def export_hardware_to_excel(hardware_list):
    # Преобразуем данные в DataFrame
    df = pd.DataFrame([hardware_to_dict(hw) for hw in hardware_list])
    # Используем буфер памяти для хранения данных Excel
    output = io.BytesIO()
    with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
        df.to_excel(writer, index=False, sheet_name='Inventory')
    output.seek(0)
    return send_file(
        output,
        mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        as_attachment=True,
        download_name='inventory_report.xlsx'  # Use 'attachment_filename' if using Flask < 2.0
    )
    #return send_file(io.BytesIO(output.getvalue().encode()), attachment_filename='inventory_report.csv', as_attachment=True)
def hardware_to_dict(hw):
    # Преобразуем объект Hardware в словарь
    return {
        'inv': hw.inv,
        'name': hw.name,
        'quantity': hw.quantity,
        'date_input': hw.date_input.isoformat() if hw.date_input else '',
        "hw_id": hw.hw_id,
        "duplicate_hw_id": hw.duplicate_hw_id,
        "doc_id": hw.doc_id,
        "no": hw.no,
        "name": hw.name,
        "inv": hw.inv,
        "sn": hw.sn,
        "mac": hw.mac,
        "kud": hw.kud,
        "quantity": hw.quantity,
        "shifr_time": hw.shifr_time,
        "shifr_amo": hw.shifr_amo,
        "date_build": hw.date_build.isoformat() if hw.date_build else None,
        "date_input": hw.date_input.isoformat() if hw.date_input else None,
        "date_buh": hw.date_buh.isoformat() if hw.date_buh else None,
        "date_nouse": hw.date_nouse.isoformat() if hw.date_nouse else None,
        "first_price": hw.first_price,
        "first_price_date": hw.first_price_date.isoformat() if hw.first_price_date else None,
        "last_price": hw.last_price,
        "last_price_date": hw.last_price_date.isoformat() if hw.last_price_date else None,
        "srok": hw.srok,
        "komplekt_id": hw.komplekt_id,
        "osnovanie_id": hw.osnovanie_id,
        "description_id": hw.description_id,
        "ues_id": hw.ues_id,
        "chekedhw_id": hw.chekedhw_id,
        "os_id": hw.os_id,
        "akt_id": hw.akt_id,
        "asudoolink_id": hw.asudoolink_id,
        "event_id": hw.event_id,
        # Добавьте другие необходимые поля
    }
    
    
@report_bp.route("/audit_report", methods=["GET"])
def audit_report():
    # Объединяем Hardware и OS по инвентарному номеру
    results = db.session.query(
        Hardware,
        OS
    ).outerjoin(OS, Hardware.inv == OS.inv).all()
    discrepancies = []
    for hw, os_ in results:
        if os_ is None:
            # Если нет соответствия в OS
            discrepancies.append({
                'inv': hw.inv,
                'differences': {'notice': 'Нет данных в OS'}
            })
            continue
        diffs = {}
        if hw.name != os_.model:
            diffs['Наименование'] = {'Hardware': hw.name, 'OS': os_.model}
        if hw.quantity != os_.quantity:
            diffs['Количество'] = {'Hardware': hw.quantity, 'OS': os_.quantity}
        if hw.date_input != os_.date_input:
            diffs['Дата ввода'] = {'Hardware': hw.date_input, 'OS': os_.date_input}
        # Проверьте другие поля по необходимости
        if diffs:
            discrepancy = {
                'inv': hw.inv,
                'differences': diffs
            }
            discrepancies.append(discrepancy)
    return render_template('audit_report.html', discrepancies=discrepancies)
# file:web_app_hw_out\app\loaders\akt_loader.py
# 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
from datetime import datetime
import re
pytesseract.pytesseract.tesseract_cmd = r'c:\Users\grum77\AppData\Local\Programs\Tesseract-OCR\tesseract.exe'
def parse_pdf_for_akt(filepath: str):
    """
    Функция для обработки PDF с актами, используя SQLAlchemy.
    """
    print(f"[DEBUG] parse_pdf_for_akt: Обрабатываем файл {filepath}")
    if not os.path.exists(filepath):
        print(f"[ERROR] Файл PDF не найден: {filepath}")
        return
    # Конвертация PDF в изображения
    try:
        pages = convert_from_path(filepath, dpi=200)
    except Exception as e:
        print(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)
        # для определения ориентации
        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)
        # Размер изображения
        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)]:
            text = pytesseract.image_to_string(img, lang="rus+eng")
            number_akt = find_number_akt_in_text(text)
            date_akt = find_date_in_text(text)
            inv = find_inv_in_text(text)
            if number_akt or inv:
                image_name = save_akt_image(img, number_akt, side, images_folder)
                
                akt = insert_or_update_akt(
                    number_akt=number_akt,
                    date_akt=date_akt,
                    inv=inv,
                    raw_text=text
                )
                akt_count += 1
                # Если inv найден, обновляем hardware.akt_id
                if inv:
                    link_hardware_with_akt(inv, akt.akt_id)
            else:
                print(f"[WARN] Не удалось извлечь данные из {side} части страницы {page_index + 1}")
    print(f"[DEBUG] parse_pdf_for_akt: обработка завершена. Найдено страниц: {page_count}, актов: {akt_count}.")
# ------------------------------------------------------------------------------
# Вспомогательные функции
# ------------------------------------------------------------------------------
def save_akt_image(img: Image.Image, number_akt: str, side: str, images_folder: str) -> str:
    """
    Сохраняет изображение акта с именем, содержащим номер акта и сторону.
    Возвращает имя файла изображения.
    """
    if number_akt:
        image_filename = f"akt_{number_akt}_{side}.png"
    else:
        # Используем текущее время, если номер акта неизвестен
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        image_filename = f"akt_{timestamp}_{side}.png"
    image_path = os.path.join(images_folder, image_filename)
    img.save(image_path)
    print(f"[DEBUG] Изображение сохранено: {image_path}")
    return image_filename  # Сохраняем только имя файла, путь может быть стандартизирован
def find_number_akt_in_text(text: str) -> str:
    """
    Извлекает номер акта из OCR-текста.
    """
    pattern = re.compile(r"акт\s*№\s*(\d+)", re.IGNORECASE)
    match = pattern.search(text)
    if match:
        return match.group(1)
    return ""
def find_date_in_text(text: str) -> str:
    """
    Извлекает дату из OCR-текста.
    """
    import re
    pattern = re.compile(r"\b(\d{1,2})[.\-/](\d{1,2})[.\-/](\d{2,4})\b")
    match = pattern.search(text)
    if match:
        day, month, year = match.groups()
        if len(year) == 2:
            year = "20" + year
        day = day.zfill(2)
        month = month.zfill(2)
        return f"{year}-{month}-{day}"
    return ""
def find_inv_in_text(text: str) -> str:
    """
    Извлекает инвентарный номер (inv) из OCR-текста.
    """
    import re
    pattern = re.compile(r"(?:inv|инв)\s*[:=]?\s*(\d+)", re.IGNORECASE)
    match = pattern.search(text)
    if match:
        return match.group(1)
    else:
        # Ищем просто числа длиной 4-6 цифр
        pattern = re.compile(r"\b\d{4,6}\b")
        match = pattern.search(text)
        if match:
            return match.group(0)
    return ""
def insert_or_update_akt(number_akt: str, date_akt: str, inv: str, raw_text: str):
    """
    Создает или обновляет запись в таблице Akt с использованием ORM.
    Возвращает объект Akt.
    """
    date_akt_parsed = parse_date(date_akt)
    akt = Akt.query.filter_by(number_akt=number_akt, date_akt=date_akt_parsed).first()
    if akt:
        akt.inv = inv
        akt.conclusion = raw_text[:200]
        try:
            db.session.commit()
        except Exception as e:
            db.session.rollback()
            print(f"[ERROR] Ошибка при обновлении Akt: {e}")
    else:
        akt = Akt(
            number_akt=number_akt,
            date_akt=date_akt_parsed,
            inv=inv,
            conclusion=raw_text[:200]
        )
        db.session.add(akt)
        try:
            db.session.commit()
        except Exception as e:
            db.session.rollback()
            print(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()
            print(f"[ERROR] Ошибка при обновлении Hardware (inv={inv}): {e}")
    else:
        print(f"[WARN] Не найдено Hardware с inv={inv} для привязки к Akt.")
def parse_date(date_str: str):
    """
    Парсит строку с датой в объект datetime.date.
    """
    if not date_str:
        return None
    formats = ['%Y-%m-%d', '%d.%m.%Y', '%d-%m-%Y']
    for fmt in formats:
        try:
            return datetime.strptime(date_str, fmt).date()
        except ValueError:
            continue
    print(f"[WARN] Не удалось распознать дату: {date_str}")
    return None
# file:web_app_hw_out\app\loaders\asudoo_loader.py
# app/loaders/asudoo_loader.py
import pandas as pd
from app.models import db, Asudoo, BranchNameTable, RuesNameTable, CehNameTable, EquipmentTypeNameTable
from datetime import datetime
import os
def parse_csv_for_asudoo(filepath: str):
    """
    Функция для загрузки/обновления таблицы Asudoo из CSV, используя SQLAlchemy.
    """
    print(f"[DEBUG] parse_csv_for_asudoo: {filepath}")
    # Проверяем, существует ли файл по указанному пути
    if not os.path.exists(filepath):
        print(f"[ERROR] Файл не найден: {filepath}")
        return
    # Предположим, разделитель CSV - ";", кодировка - utf-8
    try:
        df = pd.read_csv(filepath, sep=";", encoding="utf-8", dtype=str)
    except Exception as e:
        print(f"[ERROR] Ошибка при чтении CSV: {e}")
        return
    # Логика вставки/обновления.
    for index, row in df.iterrows():
        inv_num = (row.get("inventory_number") or "").strip()
        if not inv_num:
            continue
        # Получаем/создаем связанные объекты справочников
        branch_name = row.get("branch_name")
        branch = get_or_create(BranchNameTable, branch_name=branch_name)
        rues_name = row.get("rues_name")
        rues = get_or_create(RuesNameTable, rues_name=rues_name)
        ceh_name = row.get("ceh_name")
        ceh = get_or_create(CehNameTable, ceh_name=ceh_name)
        equipment_type_name = row.get("equipment_type_name")
        equipment_type = get_or_create(EquipmentTypeNameTable, equipment_type_name=equipment_type_name)
        # Проверка, есть ли уже такая запись
        asudoo_record = Asudoo.query.filter_by(inventory_number=inv_num).first()
        if asudoo_record:
            # UPDATE
            asudoo_record.equipment_name = row.get("equipment_name")
            asudoo_record.mac = row.get("mac")
            asudoo_record.serial_number = row.get("serial_number")
            asudoo_record.cost = parse_float(row.get("cost"))
            asudoo_record.branch = branch
            asudoo_record.rues = rues
            asudoo_record.ceh = ceh
            asudoo_record.equipment_type = equipment_type
            asudoo_record.last_updated = datetime.utcnow()
            # Обновляем другие поля при необходимости
            try:
                db.session.commit()
            except Exception as e:
                db.session.rollback()
                print(f"[ERROR] Ошибка при обновлении Asudoo (inventory_number={inv_num}): {e}")
        else:
            # INSERT
            new_asudoo = Asudoo(
                inventory_number=inv_num,
                equipment_name=row.get("equipment_name"),
                mac=row.get("mac"),
                serial_number=row.get("serial_number"),
                cost=parse_float(row.get("cost")),
                last_updated=datetime.utcnow(),
                branch=branch,
                rues=rues,
                ceh=ceh,
                equipment_type=equipment_type
                # Добавьте другие поля при необходимости
            )
            db.session.add(new_asudoo)
            try:
                db.session.commit()
            except Exception as e:
                db.session.rollback()
                print(f"[ERROR] Ошибка при вставке Asudoo (inventory_number={inv_num}): {e}")
    print("[DEBUG] parse_csv_for_asudoo: обработка завершена")
def get_or_create(model, **kwargs):
    """
    Получает объект из БД или создает новый, если он не существует.
    """
    instance = model.query.filter_by(**kwargs).first()
    if instance:
        return instance
    else:
        instance = model(**kwargs)
        db.session.add(instance)
        try:
            db.session.commit()
        except Exception as e:
            db.session.rollback()
            print(f"[ERROR] Ошибка при вставке в {model.__tablename__}: {e}")
        return instance
def parse_float(value):
    """
    Конвертирует строку в число с плавающей запятой.
    """
    if not value or pd.isna(value):
        return None
    try:
        return float(value.replace(",", "."))
    except ValueError:
        return None
# file:web_app_hw_out\app\loaders\hardware_loader.py
# app/loaders/hardware_loader.py
import re
import pandas as pd
from typing import Tuple
from app.models import db, Hardware, DescriptionTable, KomplektTable, OsnovanieTable
from datetime import datetime
from docx import Document
def parse_name_for_sn_mac_kud(name_field: str) -> Tuple[str, str, str]:
    """
    Функция для извлечения sn, mac, kud из поля name.
    """
    # Реализация извлечения значений с помощью регулярных выражений
    sn = ""
    mac = ""
    kud = ""
    match_sn = re.search(r"SN[:=]\s*(\S+)", name_field, re.IGNORECASE)
    if match_sn:
        sn = match_sn.group(1)
    match_mac = re.search(r"MAC[:=]\s*([\w:-]+)", name_field, re.IGNORECASE)
    if match_mac:
        mac = match_mac.group(1)
    match_kud = re.search(r"KUD[:=]\s*(\S+)", name_field, re.IGNORECASE)
    if match_kud:
        kud = match_kud.group(1)
    return sn, mac, kud
def determine_header_order_with_dates_dynamic_priority(header_rows, reference_dict):
    """
    Определяет порядок столбцов в таблице на основе заголовков и опорных слов,
    автоматически формируя приоритет полей на основе порядка их появления в заголовках.
    Также извлекает дополнительные данные (например, даты) из заголовков.
    :param header_rows: Список списков, представляющих строки заголовков.
                        Например: [['Номер', 'Имя', 'Первая цена на 01.01.2023 руб', 'Последняя цена на 31.12.2023 руб'],
                                  ['1', '', '', ''],
                                  ['', '', '', '']]
    :param reference_dict: Словарь, где ключ - имя поля, значение - список опорных слов.
                           Например:
                           {
                               'id': ['номер', 'id'],
                               'name': ['имя', 'name'],
                               'first_price': ['первая цена', 'first price'],
                               'last_price': ['последняя цена', 'last price']
                           }
    :return: Список словарей, каждый из которых соответствует столбцу и содержит:
             - 'field': имя поля
             - 'extras': словарь с дополнительными данными (например, даты)
             Например:
             [
                 {'field': 'id', 'extras': {}},
                 {'field': 'name', 'extras': {}},
                 {'field': 'first_price', 'extras': {'first_price_date': '01.01.2023'}},
                 {'field': 'last_price', 'extras': {'last_price_date': '31.12.2023'}}
             ]
    """
    # Транспонируем заголовки для работы с колонками
    transposed_headers = list(zip(*header_rows))
    #print(transposed_headers)
    def merge_header_cells(cells):
        """Объединяет несколько строк заголовка в один заголовок для столбца."""
        return ' '.join([cell.strip() for cell in cells if cell.strip()])
    
    # Объединяем заголовки по вертикали
    merged_headers = [merge_header_cells(col) for col in transposed_headers]
    
    def find_fields(header):
        """
        Ищет все поля, соответствующие заголовку, на основе опорных слов.
        
        :param header: Объединенный заголовок столбца.
        :return: Список соответствующих полей.
        """
        header_lower = header.lower()
        matched_fields = set()
        for field, keywords in reference_dict.items():
            for keyword in keywords:
                # Поиск целых слов без учета регистра
                if re.search(r'\b' + re.escape(keyword.lower()) + r'\b', header_lower):
                    matched_fields.add(field)
        return list(matched_fields)
    
    def extract_date(header, field_name):
        """
        Извлекает дату из заголовка между 'на' и 'руб'.
        :param header: Объединенный заголовок столбца.
        :param field_name: Имя поля, для которого извлекается дата.
        :return: Извлечённая дата в формате ДД.ММ.ГГГГ или None.
        """
        # Паттерн: на <дата> руб
        pattern = r'на\s+(\d{2}\.\d{2}\.\d{4})\s+руб'
        match = re.search(pattern, header, re.IGNORECASE)
        if match:
            return match.group(1)
        return None
    
    # Список полей в порядке их первого появления в заголовках
    field_priority = []
    
    # Обрабатываем заголовки и формируем ordered_fields
    ordered_fields = []
    for header in merged_headers:
        fields = find_fields(header)
        if not fields:
            ordered_fields.append({'field': None, 'extras': {}})
            continue
        elif len(fields) == 1:
            selected_field = fields[0]
        else:
            # Если multiple fields match, выбираем тот, который появился первым в field_priority
            # Если поле еще не в приоритетном списке, добавляем его
            # Вначале добавляем все поля в порядке их появления
            for field in fields:
                if field not in field_priority:
                    field_priority.append(field)
            # Затем выбираем тот, который имеет наивысший приоритет
            for priority_field in field_priority:
                if priority_field in fields:
                    selected_field = priority_field
                    break
        # Если поле еще не в приоритетном списке, добавляем его
        if selected_field not in field_priority:
            field_priority.append(selected_field)
        
        extras = {}
        # Проверяем, нужно ли извлечь дату для выбранного поля
        if selected_field == 'first_price':
            date = extract_date(header, 'first_price')
            if date:
                extras['first_price_date'] = date
        elif selected_field == 'last_price':
            date = extract_date(header, 'last_price')
            if date:
                extras['last_price_date'] = date
        
        ordered_fields.append({'field': selected_field, 'extras': extras})
    
    # Дополнительно, если нужно явным образом сформировать field_priority
    # Можно вывести или вернуть его, например:
    #print("Field Priority:", field_priority)
    #print("Field ordered_fields:", ordered_fields)
    
    return ordered_fields
def extract_field_names(ordered_fields):
    """
    Извлекает только названия полей из списка ordered_fields.
    :param ordered_fields: Список словарей с ключами 'field' и 'extras'.
    :return: Список названий полей в порядке их появления.
    """
    return [col['field'] for col in ordered_fields if col['field'] is not None]
def extract_number_from_string(text, return_type=int, default=1, find_all=False):
    """
    Универсальная функция для извлечения чисел из строки.
    
    :param text: Входная строка.
    :param return_type: Тип возвращаемого числа (int или float).
    :param default: Значение по умолчанию, если число не найдено.
    :param find_all: Если True, возвращать все найденные числа.
                     Если False, возвращать только первое.
    :return: Число, список чисел или default.
    """
    
    pattern = r'-?\d+\.?\d*'
    #pattern = r'\d+'
    matches = re.findall(pattern, text)
    
    if not matches:
        return default
    
    if find_all:
        numbers = []
        for num_str in matches:
            try:
                number = return_type(num_str)
                numbers.append(number)
            except ValueError:
                continue
        return numbers if numbers else default
    else:
        num_str = matches[0]
        try:
            return return_type(num_str)
        except ValueError:
            return default
def convert_to_string(value, separator=", "):
    """
    Преобразует переданное значение в строку.
    Если значение является списком, объединяет его элементы с помощью разделителя.
    Иначе, просто преобразует значение в строку и удаляет лишние пробелы.
    
    :param value: Значение для преобразования.
    :param separator: Разделитель для объединения элементов списка.
    :return: Строковое представление значения.
    """
    if isinstance(value, list):
        return separator.join(map(str, value)).strip()
    elif isinstance(value, str):
        return value.strip()
    else:
        return str(value).strip() if value else ''
    
def parse_xlsx_for_hardware_data(filepath: str, doc_id: int) -> list[dict]:    
    """
    Парсинг XLSX файла и сохранение данных в таблицу Hardware с использованием ORM.
    """
    print(f"[DEBUG] parse_xlsx_for_hardware: filepath={filepath}, doc_id={doc_id}")
    parsed_data = []
    # Считываем файл XLSX
    try:
        df_raw = pd.read_excel(filepath, header=None, dtype=str)
    except Exception as e:
        print(f"[ERROR] Не удалось открыть xlsx: {e}")
        return parsed_data
    # Ищем строку с заголовками (1..7)
    start_index = None
    zagolovok = []
    for i in range(len(df_raw)):
        
        #row_values = df_raw.iloc[i].dropna().astype(str).tolist()
        row_values = df_raw.iloc[i].astype(str).tolist()
        needed = {"1", "2", "3", "4", "5", "6", "7"}
        if needed.issubset(set(row_values)):
            start_index = i
            break
        else:
            zagolovok.append(row_values) 
    if start_index is None:
        print("[ERROR] Не найдена строка с 1..7, не можем определить начало данных")
        return parsed_data
    # Данные начинаются после строки с заголовками
    data_start = start_index + 1
    df_data = df_raw.iloc[data_start:].copy().reset_index(drop=True)
    # Определяем названия столбцов
    priznak_cols = {
        "no":['п/п'], 
        "name":['Наименование'], 
        "inv":['Инвентарный'], 
        #"quantity":[''], 
        "shifr_time":['временному'], 
        "shifr_amo":['амортизации'], 
        "date_build":['постройки'], 
        "date_input":['ввода'], 
        "date_buh":['принятия'], 
        "date_nouse":['используется'], 
        "first_price":['Первоначальная'], 
        #"first_price_date":['Остаточная','стоимость'], 
        "last_price":['Остаточная'], 
        #"last_price_date":['',''], 
        "srok":['полезного'], 
        "komplekt":['Комплектность'], 
        "osnovanie":['Основание'], 
        "description":['Примечание'],
        "ues_description":['Стуктурное']
    }
    header_rows = determine_header_order_with_dates_dynamic_priority(zagolovok, priznak_cols)
    expected_cols = extract_field_names(header_rows)
    
    #expected_cols = [
    #    "no", "name", "inv", "quantity", "shifr_time", "shifr_amo",
    #    "date_build", "date_input", "date_buh", "date_nouse",
    #    "first_price", "first_price_date", "last_price",
    #    "last_price_date", "srok", "komplekt", "osnovanie", "description", "ues_description"
    #]
    #print(zagolovok)
    #print(header_rows)
    #print(df_data.columns)
    #print(df_data)
    #print(len(df_data.columns))
    #print(expected_cols) #expected_cols
    #print(len(expected_cols))
    df_data.columns = expected_cols[: len(df_data.columns)+1]
    # Удаляем записи, где 'no' и 'name' имеют значение NaN
    df_data = df_data.dropna(subset=['no', 'name'], how='all')
    
    print(df_data)
    # Извлечение всех first_price_date
    first_price_dates = [col['extras']['first_price_date'] for col in header_rows if 'first_price_date' in col['extras']]
    # Извлечение всех last_price_date
    last_price_dates = [col['extras']['last_price_date'] for col in header_rows if 'last_price_date' in col['extras']]
    # Парсим и сохраняем данные
    for index, row in df_data.iterrows():
        # Извлекаем поля
        no_ = parse_int(row.get("no"))
        name_ = str(row.get("name", "")).strip()
        inv_ = str(row.get("inv", "")).strip()
        #print(type(row.get("komplekt")))
        #print(row.get("komplekt"))
        quantity_ = parse_int(extract_number_from_string(convert_to_string(row.get("komplekt"))))
        shifr_time_ = parse_int(row.get("shifr_time"))
        shifr_amo_ = parse_int(row.get("shifr_amo"))
        date_build_ = parse_date(row.get("date_build"))
        date_input_ = parse_date(row.get("date_input"))
        date_buh_ = parse_date(row.get("date_buh"))
        date_nouse_ = parse_date(row.get("date_nouse"))
        first_price_ = parse_float(row.get("first_price"))
        first_price_date_ = first_price_dates #parse_date(row.get("first_price_date"))
        last_price_ = parse_float(row.get("last_price"))
        last_price_date_ = last_price_dates #parse_date(row.get("last_price_date"))
        srok_ = parse_int(row.get("srok"))
        #komplekt_text = str(row.get("komplekt", "")).strip()
        #osnovanie_text = str(row.get("osnovanie", "")).strip()
        #description_text = str(row.get("description", "")).strip()
        #ues_description = str(row.get("ues_description", "")).strip()
        
        komplekt_text = convert_to_string(row.get("komplekt"))
        osnovanie_text = convert_to_string(row.get("osnovanie")) #str(row.get("osnovanie", "")).strip()
        description_text = convert_to_string(row.get("description")) #str(row.get("description", "")).strip()
        ues_description = convert_to_string(row.get("ues_description")) #str(row.get("ues_description", "")).strip()
        # Извлекаем sn, mac, kud из name
        sn_, mac_, kud_ = parse_name_for_sn_mac_kud(name_)
        # Получаем или создаем связанные записи в таблицах KomplektTable, OsnovanieTable, DescriptionTable
        komplekt = get_or_create(KomplektTable, komplekt_text=komplekt_text) if komplekt_text else None
        osnovanie = get_or_create(OsnovanieTable, osnovanie_txt=osnovanie_text) if osnovanie_text else None
        description = get_or_create(DescriptionTable, description_text=description_text) if description_text else None
        
        # Добавляем в список
    parsed_data.append({
            "doc_id": doc_id,
            "no": no_,
            "name": name_,
            "inv": inv_,
            "quantity": quantity_,
            "shifr_time": shifr_time_,
            "shifr_amo": shifr_amo_,
            "date_build": date_build_,
            "date_input": date_input_,
            "date_buh": date_buh_,
            "date_nouse": date_nouse_,
            "first_price": first_price_,
            "last_price": last_price_,
            "srok": srok_,
            "komplekt_text": komplekt,
            "osnovanie_text": osnovanie,
            "description_text": description,
            "ues_description": ues_description,
            "sn": sn_,
            "mac": mac_,
            "kud": kud_
        })
    
    print(f"[DEBUG] parse_xlsx_for_hardware_data: найдено записей: {len(parsed_data)}")
    return parsed_data
def tmp_func():
    if True:
        # Проверяем, существует ли запись с таким inv
        existing_hw = Hardware.query.filter_by(inv=inv_).first()
        if existing_hw:
            print(f"[WARN] Запись с inv={inv_} уже существует. Пропускаем.")
            #continue
        elif (no_ is None) and (inv_ is None):
            pass
        else:
            # Создаем объект Hardware
            hardware = Hardware(
                doc_id=doc_id,
                no=no_,
                name=name_,
                inv=inv_,
                sn=sn_,
                mac=mac_,
                kud=kud_,
                quantity=quantity_,
                shifr_time=shifr_time_,
                shifr_amo=shifr_amo_,
                date_build=date_build_,
                date_input=date_input_,
                date_buh=date_buh_,
                date_nouse=date_nouse_,
                first_price=first_price_,
                first_price_date=first_price_date_,
                last_price=last_price_,
                last_price_date=last_price_date_,
                srok=srok_,
                komplekt=komplekt,
                osnovanie=osnovanie,
                description=description,
                ues_description=ues_text
            )
            db.session.add(hardware)
    # Сохраняем все изменения
    try:
        db.session.commit()
        print("[DEBUG] parse_xlsx_for_hardware: обработка завершена.")
    except Exception as e:
        db.session.rollback()
        print(f"[ERROR] Ошибка при сохранении данных: {e}")
def parse_docx_for_hardware2(filepath: str, doc_id: int):
    """
    Читаем DOCX файл и сохраняем данные в базу.
    """
    print(f"[DEBUG] parse_docx_for_hardware: filepath={filepath}, doc_id={doc_id}")
    
    try:
        doc = Document(filepath)
    except Exception as e:
        print(f"[ERROR] Не удалось открыть DOCX: {e}")
        return
    for paragraph in doc.paragraphs:
        text_p = paragraph.text.strip()
        if not text_p:
            continue
        # Ищем inv в тексте
        inv_numbers = re.findall(r"\b\d{4,6}\b", text_p)
        for inv_ in inv_numbers:
            hardware = Hardware.query.filter_by(inv=inv_).first()
            if hardware:
                # Обновляем описание
                if hardware.description:
                    hardware.description.description_text += "\n" + text_p
                else:
                    description = DescriptionTable(description_text=text_p)
                    hardware.description = description
                try:
                    db.session.commit()
                except Exception as e:
                    db.session.rollback()
                    print(f"[ERROR] Ошибка при обновлении Hardware (inv={inv_}): {e}")
def parse_docx_for_hardware_data(filepath: str, doc_id: int):
    """
    Парсинг DOCX файла и возврат данных без сохранения в БД.
    """
    print(f"[DEBUG] parse_docx_for_hardware_data: filepath={filepath}, doc_id={doc_id}")
    parsed_data = []
    try:
        doc = Document(filepath)
    except Exception as e:
        print(f"[ERROR] Не удалось открыть DOCX: {e}")
        return parsed_data
    
    for paragraph in doc.paragraphs:
        text_p = paragraph.text.strip()
        if not text_p:
            continue
        # Ищем inv в тексте
        inv_numbers = re.findall(r"\b\d{4,6}\b", text_p)
        for inv_ in inv_numbers:
            parsed_data.append({
                "doc_id": doc_id,
                "inv": inv_,
                "description_text": text_p
            })
    
    print(f"[DEBUG] parse_docx_for_hardware_data: найдено описаний: {len(parsed_data)}")
    return parsed_data
def parse_int(value):
    """
    Преобразует значение в целое число.
    """
    try:
        return int(value)
    except (TypeError, ValueError):
        return None
def parse_float(value):
    """
    Преобразует значение в число с плавающей точкой.
    """
    try:
        return float(str(value).replace(",", "."))
    except (TypeError, ValueError):
        return None
def parse_date(value):
    """
    Преобразует строку в дату.
    """
    if not value or pd.isna(value):
        return None
    if isinstance(value, datetime):
        return value.date()
    date_formats = ["%d.%m.%Y", "%Y-%m-%d", "%Y/%m/%d"]
    for fmt in date_formats:
        try:
            return datetime.strptime(str(value), fmt).date()
        except (ValueError, TypeError):
            continue
    return None
def get_or_create(model, **kwargs):
    """
    Получает или создает запись в таблице модели.
    """
    instance = model.query.filter_by(**kwargs).first()
    if instance:
        return instance
    else:
        instance = model(**kwargs)
        db.session.add(instance)
        try:
            db.session.commit()
            return instance
        except Exception as e:
            db.session.rollback()
            print(f"[ERROR] Ошибка при сохранении {model.__name__}: {e}")
            return None
# file:web_app_hw_out\app\loaders\os_loader.py
# app/loaders/os_loader.py
import re
import pandas as pd
from io import StringIO
from app.models import db, OS
from datetime import datetime
def parse_csv_for_os(filepath: str):
    """
    Функция для загрузки/обновления таблицы OS из CSV с использованием SQLAlchemy.
    """
    print(f"[DEBUG] parse_csv_for_os: Начинаем обработку файла {filepath}")
    # -------------------------------------------------
    # 1. Считываем весь файл как «сырые» строки
    # -------------------------------------------------
    try:
        with open(filepath, "r", encoding="utf-8") as f:
            lines = f.readlines()
    except UnicodeDecodeError:
        print("[ERROR] Не удалось прочитать файл в кодировке UTF-8. Попробуйте другую.")
        return
    except FileNotFoundError:
        print(f"[ERROR] Файл не найден: {filepath}")
        return
    # -------------------------------------------------
    # 2. Удаляем первые 7 строк
    # -------------------------------------------------
    if len(lines) <= 7:
        print("[WARN] Файл содержит <= 7 строк, после удаления нечего обрабатывать.")
        return
    lines = lines[7:]
    # -------------------------------------------------
    # 3. Удаляем строки по regexp
    # -------------------------------------------------
    cleaned_lines = []
    pattern = re.compile(r"^\s*\d+\s*$")  # если строка содержит только число
    for line in lines:
        if pattern.match(line.strip()):
            continue
        cleaned_lines.append(line)
    cleaned_text = "".join(cleaned_lines)
    # -------------------------------------------------
    # 4. Чтение CSV через pandas
    # -------------------------------------------------
    csv_data = StringIO(cleaned_text)
    try:
        df = pd.read_csv(csv_data, sep=";", dtype=str)
    except Exception as e:
        print(f"[ERROR] Ошибка при чтении CSV (pandas): {e}")
        return
    # -------------------------------------------------
    # 5. Сопоставление столбцов DataFrame с полями модели OS
    # -------------------------------------------------
    required_columns = [column.name for column in OS.__table__.columns]
    # Проверим наличие нужных столбцов в df
    missing_cols = [c for c in required_columns if c not in df.columns]
    if missing_cols:
        print(f"[WARN] В CSV отсутствуют столбцы: {missing_cols}. Обработка может быть неполной.")
    # -------------------------------------------------
    # 6. Обработка строк и сохранение в базе данных
    # -------------------------------------------------
    updated_count = 0
    inserted_count = 0
    for _, row in df.iterrows():
        inv = (row.get("inv") or "").strip()
        if not inv:
            continue
        # Поиск существующей записи
        existing_os = OS.query.filter_by(inv=inv).first()
        # Создание словаря с параметрами
        params = {}
        for col in required_columns:
            val = row.get(col, None)
            # Конвертация данных при необходимости
            if col in ("date_warranty", "date_production", "date_expluatation",
                       "date_input", "date_output", "date_trash"):
                val = parse_date(val)
            elif col in ("price", "last_price"):
                val = parse_float(val)
            elif col == "quantity":
                val = parse_int(val)
            else:
                val = val.strip() if val else None
            params[col] = val
        # Обновление или создание записи
        if existing_os:
            for key, value in params.items():
                setattr(existing_os, key, value)
            try:
                db.session.commit()
                updated_count += 1
            except Exception as e:
                db.session.rollback()
                print(f"[WARN] Ошибка при обновлении OS (inv={inv}): {e}")
        else:
            new_os = OS(**params)
            db.session.add(new_os)
            try:
                db.session.commit()
                inserted_count += 1
            except Exception as e:
                db.session.rollback()
                print(f"[WARN] Ошибка при вставке OS (inv={inv}): {e}")
    print(f"[DEBUG] parse_csv_for_os: Обработка завершена. Обновлено: {updated_count}, добавлено: {inserted_count}")
def parse_date(date_str):
    """
    Парсит строку с датой в объект datetime.date.
    """
    if not date_str or pd.isna(date_str):
        return None
    for fmt in ("%d.%m.%Y", "%Y-%m-%d", "%d-%m-%Y"):
        try:
            return datetime.strptime(date_str.strip(), fmt).date()
        except ValueError:
            continue
    return None
def parse_float(value):
    """
    Конвертирует строку в число с плавающей запятой.
    """
    if not value or pd.isna(value):
        return None
    try:
        return float(value.replace(",", "."))
    except ValueError:
        return None
def parse_int(value):
    """
    Конвертирует строку в целое число.
    """
    if not value or pd.isna(value):
        return None
    try:
        return int(value)
    except ValueError:
        return None
# file:web_app_hw_out\app\models.py
# app/models.py
from app import 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=False)
    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))
    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))
    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))
    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)
    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)
    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 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 = 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))
    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'))
    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)
    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.String(31))
    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, nullable=False)
    name_hw = db.Column(db.String(255))
    type_hw = db.Column(db.String(127))
    zn = db.Column(db.String(255))
    date_build = db.Column(db.Date)
    inv = db.Column(db.String(31))
    ues_id = db.Column(db.Integer, db.ForeignKey('ues_table.ues_id'))
    condition = db.Column(db.Text)
    issue = db.Column(db.Text)
    conclusion = db.Column(db.Text)
    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(255))
    inventory_number = db.Column(db.String(255))
    serial_number = db.Column(db.String(255))
    last_updated = db.Column(db.Date)
    cost = db.Column(db.String(255))
    chip_id = db.Column(db.String(255))
    document_date = db.Column(db.Date)
    status = db.Column(db.String(255))
    contractnum = db.Column(db.String(255))
    ttn_number = db.Column(db.String(255))
    ttn_date = db.Column(db.Date)
    number_part = db.Column(db.String(255))
    equipment_code = db.Column(db.String(255))
    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(255), nullable=False)
    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(255), nullable=False)
    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(255), nullable=False)
    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(255), nullable=False)
    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}>'
# file:web_app_hw_out\app\schemas.py
# app/schemas.py
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from app.models import Hardware, OS, Akt
from app import ma  # Импортируем экземпляр Marshmallow из app/__init__.py
class HardwareSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = Hardware
        load_instance = True
        include_fk = True  # Включаем внешние ключи, если нужно
class OSSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = OS
        load_instance = True
        include_fk = True
class AktSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = Akt
        load_instance = True
        include_fk = True
# file:web_app_hw_out\config.py
# config.py
import os
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
class Config:
    SECRET_KEY = "xcvH&%YHGBk389hklasd9783sdcjnzlq]zxmn"  # Лучше использовать os.environ.get('SECRET_KEY') или секрет из безопасного хранилища
    DEBUG = True
    HOST = '127.0.0.1'
    PORT = 5000
    # Конфигурация SQLAlchemy
    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'app', 'hardware_db.sqlite')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    UPLOAD_FOLDER = os.path.join(BASE_DIR, 'app', 'uploads')
    ALLOWED_EXTENSIONS = {"pdf", "xlsx", "csv", "docx"}
    ASUDOO_CSV_DEFAULT_PATH = os.path.join(BASE_DIR, 'app', 'ASUDOO.csv')
SECRET_KEY = "xcvH&%YHGBk389hklasd9783sdcjnzlq]zxmn"
DEBUG = True
#HOST = "0.0.0.0"
HOST = '127.0.0.1'
PORT = 5000
# file:web_app_hw_out\create_db.py
# -*- coding: utf-8 -*-
"""
Скрипт для создания базы данных SQLite с необходимыми таблицами.
"""
import sqlite3
def create_database(db_name: str = "hardware_db.sqlite"):
    """Создаёт файл БД (если его нет) и все необходимые таблицы."""
    # Все команды создания таблиц объединены в один SQL-скрипт
    sql_script = """
    BEGIN TRANSACTION;
    -- 1. Таблица EVENTS (с автогенерацией event_id)
    CREATE TABLE IF NOT EXISTS EVENTS (
        event_id INTEGER PRIMARY KEY AUTOINCREMENT,
        event_name TEXT,
        event_time TEXT
    );
    -- 2. Таблица оборудования (hardware)
    CREATE TABLE IF NOT EXISTS hardware (
        hw_id INTEGER PRIMARY KEY AUTOINCREMENT,
        duplicate_hw_id INTEGER,
        doc_id INTEGER,
        no INTEGER,
        name TEXT NOT NULL,
        inv TEXT NOT NULL,
        sn TEXT,
        mac TEXT,
        kud TEXT,
        quantity INTEGER,
        shifr_time INTEGER,
        shifr_amo INTEGER,
        date_build TEXT,
        date_input TEXT,
        date_buh TEXT,
        date_nouse TEXT,
        first_price REAL,
        first_price_date TEXT,
        last_price REAL,
        last_price_date TEXT,
        srok INTEGER,
        komplekt_id INTEGER,
        osnovanie_id INTEGER,
        description_id INTEGER,
        ues_description TEXT,
        ues_id INTEGER,
        chekedhw_id INTEGER,
        os_id INTEGER,
        akt_id INTEGER,
        asudoolink_id INTEGER,
        event_id INTEGER,
        FOREIGN KEY (event_id) REFERENCES EVENTS(event_id),
        FOREIGN KEY (ues_id) REFERENCES UES_TABLE(ues_id),
        FOREIGN KEY (doc_id) REFERENCES LANDOCS(doc_id),
        FOREIGN KEY (chekedhw_id) REFERENCES CHEKEDHW(chekedhw_id),
        FOREIGN KEY (os_id) REFERENCES OS(os_id),
        FOREIGN KEY (akt_id) REFERENCES AKT(akt_id),
        FOREIGN KEY (description_id) REFERENCES DESCRIPTIONTBL(description_id),
        FOREIGN KEY (komplekt_id) REFERENCES KOMPLEKTTBL(komplekt_id),
        FOREIGN KEY (asudoolink_id) REFERENCES ASUDOOLINK(asudoolink_id),
        FOREIGN KEY (osnovanie_id) REFERENCES OSNOVANIETBL(osnovanie_id),
        FOREIGN KEY (duplicate_hw_id) REFERENCES hardware(hw_id),
        UNIQUE (inv)
    );
    -- 3. Таблица ОС (OS)
    CREATE TABLE IF NOT EXISTS OS (
        os_id INTEGER PRIMARY KEY AUTOINCREMENT,
        inv TEXT UNIQUE,
        passport_number TEXT,
        reg_number TEXT,
        zn TEXT,
        nn TEXT,
        date_warranty TEXT,
        date_production TEXT,
        date_expluatation TEXT,
        date_input TEXT,
        date_output TEXT,
        date_trash TEXT,
        quantity INTEGER,
        model TEXT,
        made_by TEXT,
        code_party TEXT,
        period TEXT,
        dop_inv TEXT,
        record_number TEXT,
        filial_number TEXT,
        description TEXT,
        groups TEXT,
        type_doc TEXT,
        ed_izm TEXT,
        group_os TEXT,
        vid_os TEXT,
        podrazdelenie TEXT,
        mol TEXT,
        code_norma_amort TEXT,
        norma_amort TEXT,
        uses_os TEXT,
        price REAL,
        summ_amort TEXT,
        amort_otch_period TEXT,
        price_input TEXT,
        iznos_input TEXT,
        fix_summ_amort TEXT,
        srok_polezn_use TEXT,
        valute TEXT,
        metod_nach_amort TEXT,
        tho_amort TEXT,
        type_object TEXT,
        code_group_okof TEXT,
        name_group_okof TEXT,
        popravka_koef TEXT,
        last_price REAL,
        event_id INTEGER,
        FOREIGN KEY (event_id) REFERENCES EVENTS(event_id)
    );
    -- 4. Таблица актов (AKT)
    CREATE TABLE IF NOT EXISTS AKT (
        akt_id INTEGER PRIMARY KEY AUTOINCREMENT,
        number_akt INTEGER,
        date_akt TEXT NOT NULL,
        name_hw TEXT,
        type_hw TEXT,
        zn TEXT,
        date_build TEXT,
        inv TEXT,
        ues_id INTEGER,
        condition TEXT,
        issue TEXT,
        conclusion TEXT,
        description TEXT,
        employee TEXT,
        pdf BLOB,
        event_id INTEGER,
        FOREIGN KEY (event_id) REFERENCES EVENTS(event_id),
        FOREIGN KEY (ues_id) REFERENCES UES_TABLE(ues_id)
    );
    -- 5. Таблица UES_TABLE (неизменяемая)
    CREATE TABLE IF NOT EXISTS UES_TABLE (
        ues_id INTEGER PRIMARY KEY AUTOINCREMENT,
        ues_name TEXT NOT NULL
    );
    -- 6. Таблица DESCRIPTIONTBL (неизменяемая)
    CREATE TABLE IF NOT EXISTS DESCRIPTIONTBL (
        description_id INTEGER PRIMARY KEY AUTOINCREMENT,
        description_text TEXT
    );
    -- 7. Таблица KOMPLEKTTBL (неизменяемая)
    CREATE TABLE IF NOT EXISTS KOMPLEKTTBL (
        komplekt_id INTEGER PRIMARY KEY AUTOINCREMENT,
        komplekt_text TEXT
    );
    -- 8. Таблица OSNOVANIETBL (неизменяемая)
    CREATE TABLE IF NOT EXISTS OSNOVANIETBL (
        osnovanie_id INTEGER PRIMARY KEY AUTOINCREMENT,
        osnovanie_txt TEXT
    );
    -- 9. Таблица LANDOCS
    CREATE TABLE IF NOT EXISTS LANDOCS (
        doc_id INTEGER PRIMARY KEY AUTOINCREMENT,
        doc_name TEXT NOT NULL,
        doc_date TEXT NOT NULL,
        doc_description TEXT
    );
    -- 10. Таблица ASUDOOLINK
    CREATE TABLE IF NOT EXISTS ASUDOOLINK (
        asudoolink_id INTEGER PRIMARY KEY AUTOINCREMENT,
        asudoo_id INTEGER,
        mac TEXT,
        sn TEXT,
        doc_description TEXT,
        FOREIGN KEY (asudoo_id) REFERENCES ASUDOO(asudoo_id)
    );
    -- 11. Таблица CHEKEDHW
    CREATE TABLE IF NOT EXISTS CHEKEDHW (
        chekedhw_id INTEGER PRIMARY KEY AUTOINCREMENT,
        statehw_id INTEGER,
        date_checked_akt TEXT,
        date_checked_os TEXT,
        doc_id INTEGER,
        FOREIGN KEY (doc_id) REFERENCES LANDOCS(doc_id),
        FOREIGN KEY (statehw_id) REFERENCES STATEHW(statehw_id)
    );
    -- 12. Таблица STATEHW (неизменяемая)
    CREATE TABLE IF NOT EXISTS STATEHW (
        statehw_id INTEGER PRIMARY KEY AUTOINCREMENT,
        statehw_name TEXT NOT NULL
    );
    -- 13. Таблица ASUDOO
    CREATE TABLE IF NOT EXISTS ASUDOO (
        asudoo_id INTEGER PRIMARY KEY AUTOINCREMENT,
        branch_name_id INTEGER NOT NULL,
        rues_name_id INTEGER NOT NULL,
        ceh_name_id INTEGER NOT NULL,
        equipment_type_name_id INTEGER,
        equipment_name TEXT,
        mac TEXT,
        inventory_number TEXT,
        serial_number TEXT,
        last_updated TEXT,
        cost TEXT,
        chip_id TEXT,
        document_date TEXT,
        status TEXT,
        contractnum TEXT,
        ttn_number TEXT,
        ttn_date TEXT,
        number_part TEXT,
        equipment_code TEXT,
        FOREIGN KEY (branch_name_id) REFERENCES BRANCH_NAME_TABLE(branch_name_id),
        FOREIGN KEY (rues_name_id) REFERENCES RUES_NAME_TABLE(rues_name_id),
        FOREIGN KEY (ceh_name_id) REFERENCES CEH_NAME_TABLE(ceh_name_id),
        FOREIGN KEY (equipment_type_name_id) REFERENCES EQUIPMENT_TYPE_NAME_TABLE(equipment_type_name_id)
    );
    -- 14. Таблицы-справочники для ASUDOO
    CREATE TABLE IF NOT EXISTS BRANCH_NAME_TABLE (
        branch_name_id INTEGER PRIMARY KEY AUTOINCREMENT,
        branch_name TEXT NOT NULL
    );
    CREATE TABLE IF NOT EXISTS RUES_NAME_TABLE (
        rues_name_id INTEGER PRIMARY KEY AUTOINCREMENT,
        rues_name TEXT NOT NULL
    );
    CREATE TABLE IF NOT EXISTS CEH_NAME_TABLE (
        ceh_name_id INTEGER PRIMARY KEY AUTOINCREMENT,
        ceh_name TEXT NOT NULL
    );
    CREATE TABLE IF NOT EXISTS EQUIPMENT_TYPE_NAME_TABLE (
        equipment_type_name_id INTEGER PRIMARY KEY AUTOINCREMENT,
        equipment_type_name TEXT NOT NULL
    );
    COMMIT;
    """
    # Подключаемся к БД (файл создаётся при отсутствии)
    with sqlite3.connect(db_name) as conn:
        # Выполняем скрипт одним блоком
        conn.executescript(sql_script)
    print(f"База данных успешно создана: {db_name}")
if __name__ == "__main__":
    # Можно добавить чтение имени базы из аргументов командной строки
    create_database(r"D:\Project\2025\web_app_hw_out\app\_hardware_db.sqlite")
# file:web_app_hw_out\main.py
# main.py
from app import create_app
from config import DEBUG, HOST, PORT
if __name__ == "__main__":
    app = create_app()
    app.run(debug=DEBUG, host=HOST, port=PORT)
# file:web_app_hw_out\app\templates\audit_report.html
{% extends "base.html" %}
{% block title %}Отчет аудита{% endblock %}
{% block content %}
<h1>Отчет аудита</h1>
{% if discrepancies %}
<table class="table table-bordered">
    <thead>
        <tr>
            <th>Инв. номер</th>
            <th>Поле</th>
            <th>Hardware</th>
            <th>OS</th>
        </tr>
    </thead>
    <tbody>
        {% for item in discrepancies %}
            {% if 'notice' in item.differences %}
            <tr>
                <td>{{ item.inv }}</td>
                <td colspan="3">{{ item.differences.notice }}</td>
            </tr>
            {% else %}
                {% for field, values in item.differences.items() %}
                <tr>
                    <td>{{ item.inv }}</td>
                    <td>{{ field }}</td>
                    <td>{{ values.Hardware }}</td>
                    <td>{{ values.OS }}</td>
                </tr>
                {% endfor %}
            {% endif %}
        {% endfor %}
    </tbody>
</table>
{% else %}
<p>Расхождений не обнаружено.</p>
{% endif %}
{% endblock %}
# file:web_app_hw_out\app\templates\base.html
<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>{% block title %}Моё приложение{% endblock %}</title>
    <!-- Подключение CSS (например, Bootstrap) -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
    <style>
        body {
            display: flex;
            min-height: 100vh;
            margin: 0;
        }
        .sidebar {
            width: 250px;
            background-color: #f8f9fa;
            padding: 20px;
        }
        .content {
            flex-grow: 1;
            padding: 20px;
        }
        .sidebar a {
            display: block;
            padding: 10px;
            color: #212529;
            text-decoration: none;
        }
        .sidebar a:hover {
            background-color: #e9ecef;
            text-decoration: none;
        }
    </style>
    {% block head_extra %}{% endblock %}
</head>
<body>
    <div class="sidebar">
        <h2>Меню</h2>
        <ul class="nav flex-column">
            <li class="nav-item"><a href="{{ url_for('index') }}" class="nav-link">Главная</a></li>
            <li class="nav-item"><a href="{{ url_for('hardware_bp.upload_hardware') }}" class="nav-link">Загрузка оборудования</a></li>
            <li class="nav-item"><a href="{{ url_for('os_bp.upload_os') }}" class="nav-link">Загрузка данных ОС</a></li>
            <li class="nav-item"><a href="{{ url_for('akt_bp.upload_akt') }}" class="nav-link">Загрузка актов</a></li>
            <li class="nav-item"><a href="{{ url_for('asudoo_bp.update_asudoo') }}" class="nav-link">Обновить ASUDOO</a></li>
            <li class="nav-item"><a href="{{ url_for('reconcile_bp.reconcile_index') }}" class="nav-link">Сверка</a></li>
            <li class="nav-item"><a href="{{ url_for('report_bp.report_spisanie') }}" class="nav-link">Отчёты</a></li>
			<li class="nav-item"><a href="{{ url_for('report_bp.inventory_report') }}" class="nav-link">Отчет 	инвентаризации</a></li>
			<li class="nav-item"><a href="{{ url_for('report_bp.audit_report') }}" class="nav-link">Отчет аудита</a></li>
        </ul>
    </div>
    <div class="content">
        {% with messages = get_flashed_messages() %}
          {% if messages %}
            <div class="alert alert-info">
              {% for message in messages %}
                <p>{{ message }}</p>
              {% endfor %}
            </div>
          {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}
    </div>
    <!-- Подключение JS-библиотек (например, Bootstrap JS, если требуется) -->
    {% block scripts %}
	<script src="{{ url_for('static', filename='js/jquery.slim.min.js') }}"></script>
	<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
	{% endblock %}
</body>
</html>
# file:web_app_hw_out\app\templates\confirm_hardware.html
<!-- app/templates/confirm_hardware.html -->
{% extends "base.html" %}
{% block title %}Подтверждение данных оборудования{% endblock %}
{% block content %}
    <h1>Подтверждение данных оборудования</h1>
    <form method="POST">
        <input type="hidden" name="confirm" value="true">
        <table class="table table-bordered">
            <thead>
                <tr>
                    <th>Принять</th>
                    <th>Инв. номер</th>
                    <th>Название</th>
                    <th>Количество</th>
                    <th>СНИЛС</th>
                    <th>MAC</th>
                    <th>KUD</th>
                    <th>Дата постройки</th>
                    <th>Дата ввода</th>
                    <th>Описание</th>
                    <!-- Добавьте другие поля по необходимости -->
                </tr>
            </thead>
            <tbody>
                {% for hw in hardware_list %}
                <tr>
                    <td>
                        <input type="checkbox" name="accept" value="{{ loop.index0 }}" checked>
                    </td>
                    <td>{{ hw.inv }}</td>
                    <td>{{ hw.name }}</td>
                    <td>{{ hw.quantity }}</td>
                    <td>{{ hw.sn }}</td>
                    <td>{{ hw.mac }}</td>
                    <td>{{ hw.kud }}</td>
                    <td>{{ hw.date_build }}</td>
                    <td>{{ hw.date_input }}</td>
                    <td>{{ hw.description_text }}</td>
                    <!-- Добавьте другие поля по необходимости -->
                </tr>
                {% endfor %}
            </tbody>
        </table>
        <button type="submit" class="btn btn-success">Сохранить выбранное</button>
        <a href="{{ url_for('hardware_bp.upload_hardware') }}" class="btn btn-danger">Отмена</a>
    </form>
{% endblock %}
# file:web_app_hw_out\app\templates\index.html
<!-- app/templates/index.html -->
{% extends "base.html" %}
{% block title %}Главная{% endblock %}
{% block content %}
<h1>Движение по списанию оборудования</h1>
<p>Выберите действие из меню слева.</p>
{% endblock %}
# file:web_app_hw_out\app\templates\inventory_report.html
{% extends "base.html" %}
{% block title %}Отчет инвентаризации{% endblock %}
{% block content %}
<h1>Отчет инвентаризации</h1>
<table class="table table-bordered">
    <thead>
        <tr>
            <th>Инв. номер</th>
            <th>Наименование</th>
            <th>Количество</th>
            <th>Дата ввода</th>
            <!-- Добавьте другие заголовки -->
        </tr>
    </thead>
    <tbody>
        {% for hw in hardware_list %}
        <tr>
            <td>{{ hw.inv }}</td>
            <td>{{ hw.name }}</td>
            <td>{{ hw.quantity }}</td>
            <td>{{ hw.date_input }}</td>
            <!-- Добавьте другие поля -->
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}
# file:web_app_hw_out\app\templates\inventory_report_form.html
{% extends "base.html" %}
{% block title %}Отчет инвентаризации{% endblock %}
{% block content %}
<h1>Отчет инвентаризации</h1>
<form method="POST">
    <div class="form-group">
        <label for="equipment_type">Тип оборудования:</label>
        <input type="text" id="equipment_type" name="equipment_type" class="form-control">
    </div>
    <div class="form-group">
        <label for="date_from">Дата с:</label>
        <input type="date" id="date_from" name="date_from" class="form-control">
    </div>
    <div class="form-group">
        <label for="date_to">Дата по:</label>
        <input type="date" id="date_to" name="date_to" class="form-control">
    </div>
    <div class="form-group">
        <label for="export_format">Формат экспорта:</label>
        <select id="export_format" name="export_format" class="form-control">
            <option value="html">HTML</option>
            <option value="excel">Excel</option>
            <option value="csv">CSV</option>
            <option value="json">JSON</option>
        </select>
    </div>
    <button type="submit" class="btn btn-primary">Сформировать отчет</button>
</form>
{% endblock %}
# file:web_app_hw_out\app\templates\reconcile_doc.html
<!-- app/templates/reconcile_doc.html -->
{% extends "base.html" %}
{% block title %}Сверка: документ {{ doc['doc_name'] }} ({{ doc['doc_date'] }}){% endblock %}
{% block content %}
    <h1>Документ: {{ doc['doc_name'] }} ({{ doc['doc_date'] }})</h1>
    <table class="table table-bordered">
        <thead>
            <tr>
                <th>inv</th>
                <th>name</th>
                <th>quantity</th>
                <th>date_checked_akt</th>
                <th>date_checked_os</th>
            </tr>
        </thead>
        <tbody>
            {% for hw in hw_list %}
            <tr>
                <td>
                    <a href="{{ url_for('reconcile_bp.reconcile_hw', inv=hw['inv']) }}">
                        {{ hw['inv'] }}
                    </a>
                </td>
                <td>{{ hw['name'] }}</td>
                <td>{{ hw['quantity'] }}</td>
                <td>{{ hw['date_checked_akt'] }}</td>
                <td>{{ hw['date_checked_os'] }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
    <p><a href="{{ url_for('reconcile_bp.reconcile_index') }}" class="btn btn-secondary">Вернуться к списку документов</a></p>
{% endblock %}
# file:web_app_hw_out\app\templates\reconcile_hw.html
<!-- app/templates/reconcile_hw.html -->
{% extends "base.html" %}
{% block title %}Сверка инв.№{{ inv }}{% endblock %}
{% block content %}
    <h1>Сверка для инв.№{{ inv }}</h1>
    <!-- Блок hardware -->
    <div>
        {{ hw_html|safe }}
    </div>
    <!-- Блок OS -->
    <div>
        {{ os_html|safe }}
    </div>
    <!-- Блок AKT -->
    <div>
        {{ akt_html|safe }}
    </div>
    <p><a href="{{ url_for('reconcile_bp.reconcile_index') }}" class="btn btn-secondary">Вернуться к списку документов</a></p>
{% endblock %}
# file:web_app_hw_out\app\templates\reconcile_index.html
<!-- app/templates/reconcile_index.html -->
{% extends "base.html" %}
{% block title %}Сверка: выбор документа{% endblock %}
{% block content %}
    <h1>Сверка: выбор документа LANDOCS</h1>
    <ul class="list-group">
        {% for doc in landocs %}
        <li class="list-group-item">
            <a href="{{ url_for('reconcile_bp.reconcile_doc', doc_id=doc.doc_id) }}">
                doc_id={{ doc.doc_id }} | {{ doc.doc_name }} ({{ doc.doc_date }})
            </a>
        </li>
        {% endfor %}
    </ul>
{% endblock %}
# file:web_app_hw_out\app\templates\update_asudoo.html
<!-- app/templates/update_asudoo.html -->
{% extends "base.html" %}
{% block title %}Обновление таблицы ASUDOO{% endblock %}
{% block content %}
    <h1>Обновить таблицу ASUDOO (CSV)</h1>
    <form method="POST">
        <div class="form-group">
            <label for="csv_path">Путь к CSV-файлу:</label>
            <input type="text" id="csv_path" name="csv_path" value="{{ default_csv_path }}" class="form-control">
        </div>
        <button type="submit" class="btn btn-primary">Обновить</button>
    </form>
{% endblock %}
# file:web_app_hw_out\app\templates\upload_akt.html
<!-- app/templates/upload_akt.html -->
{% extends "base.html" %}
{% block title %}Загрузка актов (AKT) из PDF{% endblock %}
{% block content %}
    <h1>Загрузка актов (AKT) из PDF</h1>
    <form method="POST" enctype="multipart/form-data">
        <div class="form-group">
            <label for="file_pdf">Файл PDF:</label>
            <input type="file" id="file_pdf" name="file_pdf" accept=".pdf" class="form-control-file">
        </div>
        <button type="submit" class="btn btn-primary">Загрузить</button>
    </form>
{% endblock %}
# file:web_app_hw_out\app\templates\upload_hardware.html
<!-- app/templates/upload_hardware.html -->
{% extends "base.html" %}
{% block title %}Загрузка оборудования{% endblock %}
{% block content %}
    <h1>Загрузка данных об оборудовании (hardware)</h1>
    <form method="POST" enctype="multipart/form-data">
        <fieldset class="form-group">
            <legend>Информация о документе:</legend>
            <div class="form-group">
                <label for="doc_name">Название документа:</label>
                <input type="text" id="doc_name" name="doc_name" list="doc_name_list" required class="form-control">
                <datalist id="doc_name_list">
                    {% for name in doc_names %}
                        <option value="{{ name }}">
                    {% endfor %}
                </datalist>
            </div>
            <div class="form-group">
                <label for="doc_date">Дата документа (YYYY-MM-DD):</label>
                <input type="date" id="doc_date" name="doc_date" required class="form-control">
            </div>
            <div class="form-group">
                <label for="doc_description">Описание документа:</label>
                <input type="text" id="doc_description" name="doc_description" class="form-control">
            </div>
        </fieldset>
        <fieldset class="form-group">
            <legend>Файлы для загрузки:</legend>
            <div class="form-group">
                <label for="file_xlsx">Файл XLSX (оборудование):</label>
                <input type="file" id="file_xlsx" name="file_xlsx" accept=".xlsx" class="form-control-file">
            </div>
            <div class="form-group">
                <label for="file_docx">Файл DOCX (описательная часть):</label>
                <input type="file" id="file_docx" name="file_docx" accept=".docx" class="form-control-file">
            </div>
        </fieldset>
        <button type="submit" class="btn btn-primary">Загрузить</button>
    </form>
{% endblock %}
# file:web_app_hw_out\app\templates\upload_os.html
<!-- app/templates/upload_os.html -->
{% extends "base.html" %}
{% block title %}Загрузка данных ОС{% endblock %}
{% block content %}
    <h1>Загрузка данных ОС</h1>
    <form method="POST" enctype="multipart/form-data">
        <div class="form-group">
            <label for="file_csv">Файл CSV для OS:</label>
            <input type="file" id="file_csv" name="file_csv" accept=".csv" class="form-control-file">
        </div>
        <button type="submit" class="btn btn-primary">Загрузить</button>
    </form>
{% endblock %}