commit 92f04e0e8a15262e629f8eb808243636d9f57d9e Author: zaininlabad Date: Tue Jul 14 01:16:33 2026 +0300 Initial commit diff --git a/1c.xlsx b/1c.xlsx new file mode 100644 index 0000000..4d7d05c Binary files /dev/null and b/1c.xlsx differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ecc66c2 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Проект + +Это веб-приложение, основанные на фреймворке Flask. Основные функции: + +## Структура проекта +- `app.py` — основной файл приложения. +- `config.py` — настройки проекта (включая путь к базе данных и секретный ключ). +- `database/` + - `db.py` — логика работы с базой данных. + - `init_db.py` — инициализация базы данных. +- `pages/` — пустая папка, возможно, должна содержать страницы веб-сайта. +- `services/` + - `contractors.py` — управление контрагентами. + - `documents.py` — работа с документами. + - `excel_import.py` и `sbis_import.py` — импорт данных из Excel и SBIS. +- `static/` + - `style.css` — стилевой лист для веб-сайта. +- `templates/` + - `base.html` — основной шаблон. + - `contractors.html` — страница с контрагентами. + - `documents_1c.html` и `documents_sbis.html` — страницы с документами из 1С и SBIS. + +## Функционал +- Работа с SQLite-базой данных (`data.db`). +- Импорт данных из Excel и SBIS. +- Отображение страниц с документами и контрагентами. + +## Запуск +Для запуска приложения выполните: +```bash +python app.py +``` \ No newline at end of file diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000..54bc1e8 Binary files /dev/null and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..fade761 Binary files /dev/null and b/__pycache__/config.cpython-313.pyc differ diff --git a/__pycache__/routes.cpython-313.pyc b/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000..847deaa Binary files /dev/null and b/__pycache__/routes.cpython-313.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..b1939ec --- /dev/null +++ b/app.py @@ -0,0 +1,44 @@ +import os +import sqlite3 + +from flask import Flask, g, redirect, url_for + +from config import DB_PATH, SECRET_KEY +from database.init_db import init_db + + +def create_app(): + app = Flask(__name__) + app.secret_key = SECRET_KEY + app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 + + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + + with sqlite3.connect(DB_PATH) as conn: + init_db(conn) + + @app.before_request + def open_db(): + if "db" not in g: + g.db = sqlite3.connect(DB_PATH) + g.db.row_factory = sqlite3.Row + + @app.teardown_appcontext + def close_db(exception): + db = g.pop("db", None) + if db is not None: + db.close() + + from routes import bp + + app.register_blueprint(bp) + + @app.route("/") + def index(): + return redirect(url_for("main.documents_1c")) + + return app + + +if __name__ == "__main__": + create_app().run(debug=True, host="0.0.0.0", port=5000) diff --git a/config.py b/config.py new file mode 100644 index 0000000..6641cda --- /dev/null +++ b/config.py @@ -0,0 +1,2 @@ +DB_PATH = "data/data.db" +SECRET_KEY = "change-me-in-production" diff --git a/data/data.db b/data/data.db new file mode 100644 index 0000000..df2793d Binary files /dev/null and b/data/data.db differ diff --git a/database/__pycache__/db.cpython-313.pyc b/database/__pycache__/db.cpython-313.pyc new file mode 100644 index 0000000..ca8266c Binary files /dev/null and b/database/__pycache__/db.cpython-313.pyc differ diff --git a/database/__pycache__/init_db.cpython-313.pyc b/database/__pycache__/init_db.cpython-313.pyc new file mode 100644 index 0000000..30fed08 Binary files /dev/null and b/database/__pycache__/init_db.cpython-313.pyc differ diff --git a/database/db.py b/database/db.py new file mode 100644 index 0000000..07d672e --- /dev/null +++ b/database/db.py @@ -0,0 +1,10 @@ +import sqlite3 + +from config import DB_PATH + + +def get_connection(): + return sqlite3.connect( + DB_PATH, + check_same_thread=False + ) \ No newline at end of file diff --git a/database/init_db.py b/database/init_db.py new file mode 100644 index 0000000..e9bafdd --- /dev/null +++ b/database/init_db.py @@ -0,0 +1,69 @@ +from datetime import datetime + + +def init_db(conn): + + cursor = conn.cursor() + + # Контрагенты + cursor.execute(""" + CREATE TABLE IF NOT EXISTS contractors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE, + active INTEGER DEFAULT 1 + ) + """) + + # Документы из 1С + cursor.execute(""" + CREATE TABLE IF NOT EXISTS documents_1c ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + contractor_id INTEGER, + + doc_type TEXT, + doc_date TEXT, + doc_number TEXT, + + amount REAL, + + state TEXT, + comment TEXT, + + created_at TEXT, + updated_at TEXT, + + FOREIGN KEY(contractor_id) + REFERENCES contractors(id) + ) + """) + + # Документы из СБИС + cursor.execute(""" + CREATE TABLE IF NOT EXISTS documents_sbis ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + raw_text TEXT, + + doc_date TEXT, + doc_number TEXT, + + contractor TEXT, + + doc_type TEXT, + + amount REAL, + + description TEXT, + + state TEXT, + responsible TEXT, + + comment TEXT, + + created_at TEXT, + updated_at TEXT + ) + """) + + conn.commit() \ No newline at end of file diff --git a/migrate.py b/migrate.py new file mode 100644 index 0000000..f272b85 --- /dev/null +++ b/migrate.py @@ -0,0 +1,44 @@ +import sqlite3 +import os + +def add_row_order_column(): + # Путь к вашей базе данных + db_path = 'data/data.db' # или ваш путь + + if not os.path.exists(db_path): + print(f"База данных не найдена по пути: {db_path}") + return + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + try: + # Проверяем, существует ли колонка + cursor.execute("PRAGMA table_info(documents_sbis)") + columns = [column[1] for column in cursor.fetchall()] + + if 'row_order' not in columns: + print("Добавляем колонку row_order...") + cursor.execute("ALTER TABLE documents_sbis ADD COLUMN row_order INTEGER DEFAULT 0") + conn.commit() + print("Колонка row_order успешно добавлена!") + + # Обновляем существующие записи + cursor.execute(""" + UPDATE documents_sbis + SET row_order = id + WHERE row_order = 0 + """) + conn.commit() + print("Существующие записи обновлены.") + else: + print("Колонка row_order уже существует.") + + except Exception as e: + print(f"Ошибка: {e}") + conn.rollback() + finally: + conn.close() + +if __name__ == "__main__": + add_row_order_column() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6121141 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask>=3.0.0 +pandas>=2.0.0 +openpyxl>=3.1.0 diff --git a/routes.py b/routes.py new file mode 100644 index 0000000..9c97dac --- /dev/null +++ b/routes.py @@ -0,0 +1,349 @@ +from flask import Blueprint, flash, g, redirect, render_template, request, url_for + +from services.contractors import ( + activate_contractor, + add_contractor, + add_contractors_bulk, + delete_contractor, + disable_contractor, + get_all_contractors, +) +from services.documents import ( + get_distinct_states_1c, + get_distinct_states_sbis, + update_comment_1c, + update_comment_sbis, +) +from services.excel_import import import_excel +from services.sbis_import import import_sbis_excel + +bp = Blueprint("main", __name__) + + +def _filter_params(): + return { + "contractor": request.values.get("contractor", ""), + "search": request.values.get("search", ""), + "status": request.values.get("status", ""), + } + + +def _redirect_docs(endpoint): + params = {k: v for k, v in _filter_params().items() if v} + return redirect(url_for(endpoint, **params)) + + +def _query_1c(contractor=None, search=None, status=None): + sql = """ + SELECT + d.id, + c.name AS contractor, + d.doc_type, + d.doc_date, + d.doc_number, + d.amount, + d.state, + d.comment, + d.updated_at + FROM documents_1c d + LEFT JOIN contractors c ON d.contractor_id = c.id + WHERE 1=1 + """ + params = [] + + if contractor: + sql += " AND c.name = ?" + params.append(contractor) + + if search: + sql += " AND d.doc_number LIKE ?" + params.append(f"%{search}%") + + if status: + sql += " AND d.state = ?" + params.append(status) + + sql += " ORDER BY d.updated_at DESC" + + return g.db.execute(sql, params).fetchall() + + +def _query_sbis(contractor=None, search=None, status=None): + sql = """ + SELECT + id, doc_date, doc_number, contractor, + doc_type, amount, state, responsible, + comment, updated_at + FROM documents_sbis + WHERE 1=1 + """ + params = [] + + if contractor: + sql += " AND contractor = ?" + params.append(contractor) + + if search: + sql += " AND doc_number LIKE ?" + params.append(f"%{search}%") + + if status: + sql += " AND state = ?" + params.append(status) + + # Сортируем как в Excel: сначала по дате документа DESC, затем по ID DESC + # (чем позже добавлен документ, тем выше он в списке) + sql += " ORDER BY doc_date DESC, id DESC" + + return g.db.execute(sql, params).fetchall() + +def _contractor_names_from_docs(table): + if table == "1c": + sql = """ + SELECT DISTINCT c.name + FROM documents_1c d + JOIN contractors c ON d.contractor_id = c.id + WHERE c.name IS NOT NULL + ORDER BY c.name + """ + else: + sql = """ + SELECT DISTINCT contractor + FROM documents_sbis + WHERE contractor IS NOT NULL AND TRIM(contractor) != '' + ORDER BY contractor + """ + return [r[0] for r in g.db.execute(sql).fetchall()] + + +@bp.route("/documents/1c", methods=["GET", "POST"]) +def documents_1c(): + filters = _filter_params() + + if request.method == "POST": + action = request.form.get("action", "upload") + + if action == "comment": + doc_id = request.form.get("doc_id") + comment = request.form.get("comment", "") + if doc_id and update_comment_1c(g.db, int(doc_id), comment): + flash("Комментарий сохранён.", "success") + else: + flash("Не удалось сохранить комментарий.", "error") + return _redirect_docs("main.documents_1c") + + file = request.files.get("file") + if not file or not file.filename: + flash("Выберите Excel-файл для загрузки.", "error") + else: + try: + imported, updated, skipped = import_excel(file, g.db) + flash( + f"Импорт 1С: добавлено {imported}, обновлено {updated}, " + f"пропущено (не в списке контрагентов) {skipped}.", + "success", + ) + except Exception as exc: + flash(f"Ошибка импорта: {exc}", "error") + return redirect(url_for("main.documents_1c")) + + rows = _query_1c( + contractor=filters["contractor"] or None, + search=filters["search"] or None, + status=filters["status"] or None, + ) + + return render_template( + "documents_1c.html", + rows=rows, + contractors=_contractor_names_from_docs("1c"), + statuses=get_distinct_states_1c(g.db), + selected_contractor=filters["contractor"], + search=filters["search"], + selected_status=filters["status"], + ) + + +@bp.route("/documents/sbis", methods=["GET", "POST"]) +def documents_sbis(): + filters = _filter_params() + + if request.method == "POST": + action = request.form.get("action", "upload") + + if action == "comment": + doc_id = request.form.get("doc_id") + comment = request.form.get("comment", "") + if doc_id and update_comment_sbis(g.db, int(doc_id), comment): + flash("Комментарий сохранён.", "success") + else: + flash("Не удалось сохранить комментарий.", "error") + return _redirect_docs("main.documents_sbis") + + file = request.files.get("file") + if not file or not file.filename: + flash("Выберите Excel-файл для загрузки.", "error") + else: + try: + imported, updated, skipped = import_sbis_excel(file, g.db) + flash( + f"Импорт СБИС: добавлено {imported}, обновлено {updated}, " + f"пропущено (не в списке контрагентов) {skipped}.", + "success", + ) + except Exception as exc: + flash(f"Ошибка импорта: {exc}", "error") + return redirect(url_for("main.documents_sbis")) + + # Параметры пагинации + page = request.args.get('page', 1, type=int) + per_page = 150 # Количество записей на странице + offset = (page - 1) * per_page + + # Получаем общее количество записей с учётом фильтров + count_sql = """ + SELECT COUNT(*) as total + FROM documents_sbis + WHERE 1=1 + """ + count_params = [] + + contractor = filters["contractor"] or None + search = filters["search"] or None + status = filters["status"] or None + + if contractor: + count_sql += " AND contractor = ?" + count_params.append(contractor) + + if search: + count_sql += " AND doc_number LIKE ?" + count_params.append(f"%{search}%") + + if status: + count_sql += " AND state = ?" + count_params.append(status) + + total = g.db.execute(count_sql, count_params).fetchone()['total'] + total_pages = (total + per_page - 1) // per_page if total > 0 else 1 + + # Основной запрос с пагинацией + # В методе documents_sbis замените SQL запрос на: + sql = """ + SELECT + id, doc_date, doc_number, contractor, + doc_type, amount, state, responsible, + comment, updated_at + FROM documents_sbis + WHERE 1=1 + """ + params = [] + + if contractor: + sql += " AND contractor = ?" + params.append(contractor) + + if search: + sql += " AND doc_number LIKE ?" + params.append(f"%{search}%") + + if status: + sql += " AND state = ?" + params.append(status) + + # Сортировка по дате документа и ID + sql += " ORDER BY doc_date DESC, id DESC LIMIT ? OFFSET ?" + params.extend([per_page, offset]) + rows = g.db.execute(sql, params).fetchall() + + return render_template( + "documents_sbis.html", + rows=rows, + contractors=_contractor_names_from_docs("sbis"), + statuses=get_distinct_states_sbis(g.db), + selected_contractor=filters["contractor"], + search=filters["search"], + selected_status=filters["status"], + page=page, + total_pages=total_pages, + total=total + ) +def _query_sbis_with_pagination(contractor=None, search=None, status=None, limit=50, offset=0): + sql = """ + SELECT + id, doc_date, doc_number, contractor, + doc_type, amount, state, responsible, + comment, updated_at + FROM documents_sbis + WHERE 1=1 + ORDER BY doc_date DESC + """ + params = [] + + if contractor: + sql += " AND contractor = ?" + params.append(contractor) + + if search: + sql += " AND doc_number LIKE ?" + params.append(f"%{search}%") + + + if status: + sql += " AND state = ?" + params.append(status) + + sql += " ORDER BY updated_at DESC LIMIT ? OFFSET ?" + params.extend([limit, offset]) + + return g.db.execute(sql, params).fetchall() + +@bp.route("/contractors", methods=["GET", "POST"]) +def contractors(): + if request.method == "POST": + action = request.form.get("action") + + if action == "add": + name = request.form.get("name", "").strip() + if not name: + flash("Введите название контрагента.", "error") + elif add_contractor(g.db, name): + flash(f"Контрагент «{name}» добавлен.", "success") + else: + flash(f"Контрагент «{name}» уже существует.", "error") + + elif action == "bulk_add": + text = request.form.get("names", "") + if not text.strip(): + flash("Введите хотя бы одно название.", "error") + else: + added, exists = add_contractors_bulk(g.db, text) + flash( + f"Добавлено: {added}. Уже были в списке: {exists}.", + "success", + ) + + elif action == "activate": + contractor_id = request.form.get("contractor_id") + if contractor_id: + activate_contractor(g.db, int(contractor_id)) + flash("Контрагент активирован.", "success") + + elif action == "deactivate": + contractor_id = request.form.get("contractor_id") + if contractor_id: + disable_contractor(g.db, int(contractor_id)) + flash("Контрагент деактивирован.", "success") + + elif action == "delete": + contractor_id = request.form.get("contractor_id") + if contractor_id: + delete_contractor(g.db, int(contractor_id)) + flash("Контрагент удалён.", "success") + + return redirect(url_for("main.contractors")) + + return render_template( + "contractors.html", + contractors=get_all_contractors(g.db), + ) diff --git a/sbis.xlsx b/sbis.xlsx new file mode 100644 index 0000000..ba92589 Binary files /dev/null and b/sbis.xlsx differ diff --git a/services/__pycache__/contractors.cpython-313.pyc b/services/__pycache__/contractors.cpython-313.pyc new file mode 100644 index 0000000..8100590 Binary files /dev/null and b/services/__pycache__/contractors.cpython-313.pyc differ diff --git a/services/__pycache__/documents.cpython-313.pyc b/services/__pycache__/documents.cpython-313.pyc new file mode 100644 index 0000000..21d18ab Binary files /dev/null and b/services/__pycache__/documents.cpython-313.pyc differ diff --git a/services/__pycache__/excel_import.cpython-313.pyc b/services/__pycache__/excel_import.cpython-313.pyc new file mode 100644 index 0000000..4c7cc6a Binary files /dev/null and b/services/__pycache__/excel_import.cpython-313.pyc differ diff --git a/services/__pycache__/sbis_import.cpython-313.pyc b/services/__pycache__/sbis_import.cpython-313.pyc new file mode 100644 index 0000000..90a7832 Binary files /dev/null and b/services/__pycache__/sbis_import.cpython-313.pyc differ diff --git a/services/contractors.py b/services/contractors.py new file mode 100644 index 0000000..87458c8 --- /dev/null +++ b/services/contractors.py @@ -0,0 +1,115 @@ +def get_contractors(conn): + + cursor = conn.cursor() + + cursor.execute(""" + SELECT id, name + FROM contractors + WHERE active = 1 + ORDER BY name + """) + + return cursor.fetchall() + + +def get_all_contractors(conn): + + cursor = conn.cursor() + + cursor.execute(""" + SELECT id, name, active + FROM contractors + ORDER BY name + """) + + return cursor.fetchall() + + +def add_contractor(conn, name): + + cursor = conn.cursor() + + cursor.execute(""" + INSERT OR IGNORE INTO contractors(name) + VALUES(?) + """, (name,)) + + conn.commit() + return cursor.rowcount > 0 + + +def add_contractors_bulk(conn, text): + cursor = conn.cursor() + added = 0 + exists = 0 + + for line in text.splitlines(): + name = line.strip() + if not name: + continue + + cursor.execute( + """ + INSERT OR IGNORE INTO contractors(name) + VALUES(?) + """, + (name,), + ) + + if cursor.rowcount: + added += 1 + else: + exists += 1 + + conn.commit() + return added, exists + + +def activate_contractor(conn, contractor_id): + + cursor = conn.cursor() + + cursor.execute(""" + UPDATE contractors + SET active = 1 + WHERE id = ? + """, (contractor_id,)) + + conn.commit() + + +def disable_contractor(conn, contractor_id): + + cursor = conn.cursor() + + cursor.execute(""" + UPDATE contractors + SET active = 0 + WHERE id = ? + """, (contractor_id,)) + + conn.commit() + + +def delete_contractor(conn, contractor_id): + + cursor = conn.cursor() + + cursor.execute( + """ + UPDATE documents_1c + SET contractor_id = NULL + WHERE contractor_id = ? + """, + (contractor_id,), + ) + + cursor.execute( + """ + DELETE FROM contractors + WHERE id = ? + """, + (contractor_id,), + ) + + conn.commit() diff --git a/services/documents.py b/services/documents.py new file mode 100644 index 0000000..85437eb --- /dev/null +++ b/services/documents.py @@ -0,0 +1,55 @@ +from datetime import datetime + + +def update_comment_1c(conn, doc_id, comment): + cursor = conn.cursor() + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + cursor.execute( + """ + UPDATE documents_1c + SET comment = ?, updated_at = ? + WHERE id = ? + """, + (comment.strip(), now, doc_id), + ) + conn.commit() + return cursor.rowcount > 0 + + +def update_comment_sbis(conn, doc_id, comment): + cursor = conn.cursor() + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + cursor.execute( + """ + UPDATE documents_sbis + SET comment = ?, updated_at = ? + WHERE id = ? + """, + (comment.strip(), now, doc_id), + ) + conn.commit() + return cursor.rowcount > 0 + + +def get_distinct_states_1c(conn): + rows = conn.execute( + """ + SELECT DISTINCT state + FROM documents_1c + WHERE state IS NOT NULL AND TRIM(state) != '' + ORDER BY state COLLATE NOCASE + """ + ).fetchall() + return [r[0] for r in rows] + + +def get_distinct_states_sbis(conn): + rows = conn.execute( + """ + SELECT DISTINCT state + FROM documents_sbis + WHERE state IS NOT NULL AND TRIM(state) != '' + ORDER BY state COLLATE NOCASE + """ + ).fetchall() + return [r[0] for r in rows] diff --git a/services/excel_import.py b/services/excel_import.py new file mode 100644 index 0000000..77dd64a --- /dev/null +++ b/services/excel_import.py @@ -0,0 +1,121 @@ +import pandas as pd +from datetime import datetime + +from services.contractors import get_contractors + + +def _find_column(df, aliases): + for name in aliases: + if name in df.columns: + return name + return None + + +def import_excel(file, conn): + contractors = { + name: contractor_id + for contractor_id, name in get_contractors(conn) + } + + cursor = conn.cursor() + df = pd.read_excel(file) + + required = { + "contractor": _find_column(df, ["Контрагент"]), + "doc_type": _find_column(df, ["Вид документа", "Тип документа"]), + "doc_date": _find_column(df, ["Дата", "Дата документа"]), + "doc_number": _find_column(df, ["Номер", "Номер документа"]), + "amount": _find_column(df, ["Сумма"]), + "state": _find_column(df, ["Состояние ЭДО", "Состояние", "Статус"]), + } + + missing = [key for key, col in required.items() if col is None] + if missing: + raise ValueError( + f"В файле не найдены обязательные колонки: {', '.join(missing)}" + ) + + imported = 0 + updated = 0 + skipped = 0 + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + for _, row in df.iterrows(): + contr = str(row[required["contractor"]]).strip() + + if contr not in contractors: + skipped += 1 + continue + + contractor_id = contractors[contr] + doc_type = row[required["doc_type"]] + doc_date = row[required["doc_date"]] + number = row[required["doc_number"]] + summ = row[required["amount"]] + state = row[required["state"]] + + amount = float(summ) if pd.notna(summ) else 0 + + cursor.execute( + """ + SELECT id + FROM documents_1c + WHERE contractor_id = ? + AND doc_type = ? + AND doc_date = ? + AND doc_number = ? + AND amount = ? + """, + ( + contractor_id, + str(doc_type), + str(doc_date), + str(number), + amount, + ), + ) + + document = cursor.fetchone() + + if document: + cursor.execute( + """ + UPDATE documents_1c + SET state = ?, updated_at = ? + WHERE id = ? + """, + (str(state), now, document[0]), + ) + updated += 1 + else: + cursor.execute( + """ + INSERT INTO documents_1c ( + contractor_id, + doc_type, + doc_date, + doc_number, + amount, + state, + comment, + created_at, + updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + contractor_id, + str(doc_type), + str(doc_date), + str(number), + amount, + str(state), + "", + now, + now, + ), + ) + imported += 1 + + conn.commit() + return imported, updated, skipped diff --git a/services/sbis_import.py b/services/sbis_import.py new file mode 100644 index 0000000..6ac61fa --- /dev/null +++ b/services/sbis_import.py @@ -0,0 +1,152 @@ +import pandas as pd +from datetime import datetime + +from services.contractors import get_contractors + + +def _find_column(df, aliases): + for name in aliases: + if name in df.columns: + return name + return None + + +def import_sbis_excel(file, conn): + contractors = { + name: contractor_id + for contractor_id, name in get_contractors(conn) + } + + cursor = conn.cursor() + df = pd.read_excel(file) + + required = { + "contractor": _find_column( + df, ["Контрагент", "Наименование контрагента", "Организация"] + ), + "doc_date": _find_column(df, ["Дата", "Дата документа"]), + "doc_number": _find_column(df, ["Номер", "Номер документа"]), + "doc_type": _find_column(df, ["Вид документа", "Тип документа", "Тип"]), + "amount": _find_column(df, ["Сумма"]), + "state": _find_column(df, ["Состояние", "Статус", "Состояние документа"]), + } + + missing = [key for key, col in required.items() if col is None] + if missing: + raise ValueError( + f"В файле не найдены обязательные колонки: {', '.join(missing)}" + ) + + responsible_col = _find_column( + df, ["Ответственный", "Ответственный сотрудник"] + ) + description_col = _find_column(df, ["Описание", "Комментарий", "Примечание"]) + + imported = 0 + updated = 0 + skipped = 0 + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Получаем максимальный row_order для новых записей + cursor.execute("SELECT COALESCE(MAX(row_order), 0) FROM documents_sbis") + max_order = cursor.fetchone()[0] or 0 + + for idx, (_, row) in enumerate(df.iterrows()): + contr = str(row[required["contractor"]]).strip() + + if contr not in contractors: + skipped += 1 + continue + + doc_type = row[required["doc_type"]] + doc_date = row[required["doc_date"]] + number = row[required["doc_number"]] + summ = row[required["amount"]] + state = row[required["state"]] + + amount = float(summ) if pd.notna(summ) else 0 + responsible = ( + str(row[responsible_col]).strip() + if responsible_col and pd.notna(row[responsible_col]) + else None + ) + description = ( + str(row[description_col]).strip() + if description_col and pd.notna(row[description_col]) + else None + ) + + # Проверяем существование документа + cursor.execute( + """ + SELECT id + FROM documents_sbis + WHERE contractor = ? + AND doc_type = ? + AND doc_date = ? + AND doc_number = ? + AND amount = ? + """, + ( + contr, + str(doc_type), + str(doc_date), + str(number), + amount, + ), + ) + + document = cursor.fetchone() + + if document: + cursor.execute( + """ + UPDATE documents_sbis + SET state = ?, + responsible = COALESCE(?, responsible), + description = COALESCE(?, description), + updated_at = ? + WHERE id = ? + """, + (str(state), responsible, description, now, document[0]), + ) + updated += 1 + else: + row_order = max_order + idx + 1 + cursor.execute( + """ + INSERT INTO documents_sbis ( + raw_text, + doc_date, + doc_number, + contractor, + doc_type, + amount, + description, + state, + responsible, + created_at, + updated_at, + row_order + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + None, + str(doc_date), + str(number), + contr, + str(doc_type), + amount, + description, + str(state), + responsible, + now, + now, + row_order, + ), + ) + imported += 1 + + conn.commit() + return imported, updated, skipped \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..ad47949 --- /dev/null +++ b/static/style.css @@ -0,0 +1,967 @@ +:root { + --bg: #0b0f14; + --bg-elevated: #111820; + --surface: #151c26; + --surface-hover: #1a2330; + --surface-muted: #1e2836; + --text: #e8edf4; + --text-secondary: #9aa8bc; + --text-muted: #6b7a90; + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.14); + --primary: #5b8def; + --primary-hover: #7aa3f5; + --primary-glow: rgba(91, 141, 239, 0.25); + --accent: #22d3a8; + --accent-glow: rgba(34, 211, 168, 0.2); + --warn: #f5b942; + --warn-bg: rgba(245, 185, 66, 0.12); + --success: #34d399; + --success-bg: rgba(52, 211, 153, 0.12); + --error: #f87171; + --error-bg: rgba(248, 113, 113, 0.12); + --neutral-bg: rgba(255, 255, 255, 0.06); + --radius: 14px; + --radius-sm: 10px; + --shadow: 0 4px 24px rgba(0, 0, 0, 0.35); + --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.45); + --transition: 0.18s ease; +} + +* { + box-sizing: border-box; +} + +html { + color-scheme: dark; +} + +body { + margin: 0; + font-family: "Inter", "Segoe UI", system-ui, sans-serif; + background: var(--bg); + background-image: + radial-gradient(ellipse 80% 50% at 50% -20%, rgba(91, 141, 239, 0.15), transparent), + radial-gradient(ellipse 60% 40% at 100% 0%, rgba(34, 211, 168, 0.06), transparent); + color: var(--text); + line-height: 1.55; + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} + +.container { + max-width: 95%; /* Или 1400px, чтобы таблица занимала больше места на больших экранах */ + margin: 0 auto; + padding: 0 1.5rem; +} + +/* ── Header ── */ + +.header { + position: sticky; + top: 0; + z-index: 100; + background: rgba(11, 15, 20, 0.85); + backdrop-filter: blur(16px); + border-bottom: 1px solid var(--border); + margin-bottom: 2rem; +} + +.header-inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + padding: 1rem 0 0; + flex-wrap: wrap; +} + +.logo-wrap { + display: flex; + align-items: center; + gap: 0.75rem; + text-decoration: none; + color: inherit; +} + +.logo-icon { + width: 40px; + height: 40px; + border-radius: 12px; + background: linear-gradient(135deg, var(--primary), #3d6fd4); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + box-shadow: 0 4px 16px var(--primary-glow); +} + +.logo-text h1 { + margin: 0; + font-size: 1.15rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.logo-text span { + display: block; + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 400; + margin-top: 0.1rem; +} + +.tabs { + display: flex; + gap: 0.35rem; + padding-bottom: 0; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 4px; +} + +.tab { + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.55rem 1rem; + text-decoration: none; + color: var(--text-secondary); + border-radius: 9px; + font-weight: 500; + font-size: 0.9rem; + transition: color var(--transition), background var(--transition); + white-space: nowrap; +} + +.tab:hover { + color: var(--text); + background: var(--surface-hover); +} + +.tab.active { + color: var(--text); + background: var(--surface-muted); + box-shadow: inset 0 0 0 1px var(--border-strong); +} + +.tab-icon { + opacity: 0.7; + font-size: 0.85rem; +} + +.tab.active .tab-icon { + opacity: 1; +} + +/* ── Main ── */ + +main.container { + padding-bottom: 3rem; +} + +.page-header { + margin-bottom: 1.75rem; +} + +.page-header h2 { + margin: 0 0 0.35rem; + font-size: 1.65rem; + font-weight: 700; + letter-spacing: -0.03em; +} + +.page-header p { + margin: 0; + color: var(--text-secondary); + font-size: 0.95rem; + max-width: 640px; +} + +/* ── Alerts ── */ + +.messages { + margin-bottom: 1.25rem; +} + +.alert { + padding: 0.85rem 1.1rem; + border-radius: var(--radius-sm); + margin-bottom: 0.5rem; + font-size: 0.9rem; + border: 1px solid transparent; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.alert-success { + background: var(--success-bg); + color: var(--success); + border-color: rgba(52, 211, 153, 0.25); +} + +.alert-error { + background: var(--error-bg); + color: var(--error); + border-color: rgba(248, 113, 113, 0.25); +} + +/* ── Panels ── */ + +.panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem; + margin-bottom: 1.25rem; + box-shadow: var(--shadow); +} + +.panel-header { + display: flex; + align-items: flex-start; + gap: 0.85rem; + margin-bottom: 1.25rem; +} + +.panel-icon { + width: 42px; + height: 42px; + border-radius: 11px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.15rem; + flex-shrink: 0; +} + +.panel-icon-blue { + background: rgba(91, 141, 239, 0.15); + color: var(--primary); +} + +.panel-icon-green { + background: rgba(34, 211, 168, 0.12); + color: var(--accent); +} + +.panel-icon-purple { + background: rgba(167, 139, 250, 0.12); + color: #a78bfa; +} + +.panel-title h3 { + margin: 0 0 0.25rem; + font-size: 1.05rem; + font-weight: 600; +} + +.panel-title p { + margin: 0; + color: var(--text-muted); + font-size: 0.875rem; + line-height: 1.45; +} + +.panel > h3 { + margin: 1.5rem 0 1rem; + font-size: 1rem; + font-weight: 600; + color: var(--text-secondary); +} + +.panel-divider { + height: 1px; + background: var(--border); + margin: 1.25rem 0; +} + +.hint { + color: var(--text-muted); + font-size: 0.875rem; +} + +/* ── Upload zone ── */ + +.upload-zone { + border: 1.5px dashed var(--border-strong); + border-radius: var(--radius-sm); + padding: 1.5rem; + background: var(--bg-elevated); + transition: border-color var(--transition), background var(--transition); +} + +.upload-zone:hover { + border-color: rgba(91, 141, 239, 0.4); + background: rgba(91, 141, 239, 0.04); +} + +.upload-form { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; +} + +.file-input-wrap { + flex: 1; + min-width: 220px; +} + +.file-input-wrap input[type="file"] { + width: 100%; + padding: 0.65rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--text-secondary); + font: inherit; + font-size: 0.875rem; + cursor: pointer; +} + +.file-input-wrap input[type="file"]::file-selector-button { + padding: 0.4rem 0.85rem; + margin-right: 0.75rem; + border: none; + border-radius: 7px; + background: var(--surface-muted); + color: var(--text); + font: inherit; + font-size: 0.825rem; + cursor: pointer; + transition: background var(--transition); +} + +.file-input-wrap input[type="file"]::file-selector-button:hover { + background: var(--primary); + color: white; +} + +/* ── Forms ── */ + +.add-form, +.filters { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: end; +} + +.filters { + padding: 1rem; + background: var(--bg-elevated); + border-radius: var(--radius-sm); + border: 1px solid var(--border); + margin-bottom: 1.25rem; +} + +.filters label { + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.8rem; + font-weight: 500; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +input[type="text"], +select { + padding: 0.6rem 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--text); + font: inherit; + font-size: 0.9rem; + min-width: 200px; + transition: border-color var(--transition), box-shadow var(--transition); +} + +input[type="text"]:focus, +select:focus { + outline: none; + border-color: rgba(91, 141, 239, 0.5); + box-shadow: 0 0 0 3px var(--primary-glow); +} + +input[type="text"]::placeholder { + color: var(--text-muted); +} + +select option { + background: var(--surface); + color: var(--text); +} + +.add-form input[type="text"] { + flex: 1; + min-width: 260px; +} + +/* ── Buttons ── */ + +.btn { + padding: 0.6rem 1.15rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + background: var(--surface-muted); + color: var(--text); + cursor: pointer; + font: inherit; + font-size: 0.875rem; + font-weight: 500; + transition: background var(--transition), border-color var(--transition), transform 0.1s ease; + white-space: nowrap; +} + +.btn:hover { + background: var(--surface-hover); + border-color: rgba(255, 255, 255, 0.2); +} + +.btn:active { + transform: scale(0.98); +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary), #4a7de0); + color: white; + border-color: transparent; + box-shadow: 0 4px 14px var(--primary-glow); +} + +.btn-primary:hover { + background: linear-gradient(135deg, var(--primary-hover), #5b8def); + box-shadow: 0 6px 20px var(--primary-glow); +} + +.btn-danger { + color: var(--error); + border-color: rgba(248, 113, 113, 0.3); + background: var(--error-bg); +} + +.btn-danger:hover { + background: rgba(248, 113, 113, 0.2); + border-color: rgba(248, 113, 113, 0.45); +} + +.btn-sm { + padding: 0.35rem 0.75rem; + font-size: 0.8rem; +} + +.btn-ghost { + background: transparent; + border-color: var(--border); + color: var(--text-secondary); +} + +.btn-ghost:hover { + color: var(--text); + background: var(--surface-hover); +} + +/* ── Metrics ── */ + +.metrics { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; + margin-bottom: 1.25rem; +} + +.metric { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem 1.35rem; + position: relative; + overflow: hidden; + box-shadow: var(--shadow); + transition: transform var(--transition), border-color var(--transition); +} + +.metric:hover { + transform: translateY(-2px); + border-color: var(--border-strong); +} + +.metric::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + border-radius: var(--radius) var(--radius) 0 0; +} + +.metric-total::before { background: var(--primary); } +.metric-pending::before { background: var(--warn); } +.metric-completed::before { background: var(--accent); } +.metric-sbis::before { background: #a78bfa; } + +.metric-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.metric-icon { + width: 36px; + height: 36px; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; +} + +.metric-total .metric-icon { background: rgba(91, 141, 239, 0.15); } +.metric-pending .metric-icon { background: var(--warn-bg); } +.metric-completed .metric-icon { background: var(--accent-glow); } +.metric-sbis .metric-icon { background: rgba(167, 139, 250, 0.12); } + +.metric-value { + display: block; + font-size: 2.1rem; + font-weight: 700; + letter-spacing: -0.03em; + line-height: 1.1; +} + +.metric-label { + color: var(--text-muted); + font-size: 0.825rem; + font-weight: 500; +} + +/* ── Tables ── */ + +.table-wrap { + overflow-x: auto; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + width: 100%; /* Явно указываем ширину */ + margin: 0; /* Убираем возможные отступы, которые "съедают" место */ +} + +table { + width: 100%; + min-width: 1200px; /* Сохраняем это, чтобы на маленьких экранах была прокрутка */ + border-collapse: collapse; + font-size: 0.875rem; + table-layout: auto; /* Позволяет столбцам расширяться по контенту */ +} + +th { + padding: 1rem 1.25rem; /* Было 0.75rem 1rem */ + background: var(--bg-elevated); + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + text-align: left; + border-bottom: 1px solid var(--border); + white-space: nowrap; /* Это важно, чтобы заголовки не переносились */ +} + +td { + padding: 1rem 1.25rem; /* Было 0.75rem 1rem */ + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + /* Добавьте это, чтобы содержимое ячеек не "схлопывалось" */ + white-space: nowrap; +} + +tbody tr { + transition: background var(--transition); +} + +tbody tr:hover { + background: rgba(255, 255, 255, 0.025); +} + +tbody tr:last-child td { + border-bottom: none; +} + +td:first-child:not(.col-note) { + color: var(--text); + font-weight: 500; +} + +.col-note { + width: 36px; + padding: 0.8rem 0.5rem !important; + text-align: center; +} + +.note-indicator { + font-size: 0.9rem; + filter: drop-shadow(0 0 6px rgba(245, 185, 66, 0.5)); +} + +tr.has-note { + background: rgba(245, 185, 66, 0.04); +} + +tr.has-note td:first-child { + box-shadow: inset 3px 0 0 var(--warn); +} + +tr.has-note:hover { + background: rgba(245, 185, 66, 0.07); +} + +.col-comment { + min-width: 120px; +} + +.comment-details { + font-size: 0.825rem; +} + +.comment-summary { + cursor: pointer; + color: var(--primary); + font-weight: 500; + list-style: none; + user-select: none; +} + +.comment-summary::-webkit-details-marker { + display: none; +} + +.comment-summary:hover { + color: var(--primary-hover); +} + +.comment-form { + margin-top: 0.5rem; + min-width: 220px; +} + +.comment-form textarea { + width: 100%; + padding: 0.55rem 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-elevated); + color: var(--text); + font: inherit; + font-size: 0.825rem; + resize: vertical; + min-height: 52px; +} + +.comment-form textarea:focus { + outline: none; + border-color: rgba(91, 141, 239, 0.5); + box-shadow: 0 0 0 3px var(--primary-glow); +} + +.comment-actions { + margin-top: 0.4rem; + display: flex; + justify-content: flex-end; +} + +.num-cell { + font-variant-numeric: tabular-nums; + font-family: "Inter", monospace; +} + +.doc-number { + font-family: "Inter", monospace; + font-size: 0.85rem; + color: var(--primary); +} + +/* ── Status badges ── */ + +.status { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.2rem 0.65rem; + border-radius: 999px; + background: var(--neutral-bg); + font-size: 0.78rem; + font-weight: 500; + color: var(--text-secondary); + white-space: nowrap; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; +} + +.status::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + background: var(--text-muted); +} + +.status-ok { + background: var(--success-bg); + color: var(--success); +} + +.status-ok::before { background: var(--success); } + +.status-warn { + background: var(--warn-bg); + color: var(--warn); +} + +.status-warn::before { background: var(--warn); } + +.status-error { + background: var(--error-bg); + color: var(--error); +} + +.status-error::before { background: var(--error); } + +.status-neutral { + background: var(--neutral-bg); + color: var(--text-muted); +} + +.status-neutral::before { background: var(--text-muted); } + +/* ── Empty state ── */ + +.empty-state { + text-align: center; + padding: 3rem 1.5rem; + color: var(--text-muted); +} + +.empty-icon { + font-size: 2.5rem; + margin-bottom: 0.75rem; + opacity: 0.5; +} + +.empty-state p { + margin: 0; + font-size: 0.95rem; +} + +.empty-state .empty-hint { + margin-top: 0.35rem; + font-size: 0.825rem; + opacity: 0.7; +} + +/* ── Contractors ── */ + +.contractor-grid { + display: grid; + gap: 0.65rem; +} + +.contractor-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.85rem 1rem; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + transition: border-color var(--transition), background var(--transition); +} + +.contractor-card:hover { + border-color: var(--border-strong); + background: var(--surface-hover); +} + +.contractor-info { + display: flex; + align-items: center; + gap: 0.85rem; + min-width: 0; +} + +.contractor-avatar { + width: 38px; + height: 38px; + border-radius: 10px; + background: linear-gradient(135deg, rgba(91, 141, 239, 0.2), rgba(34, 211, 168, 0.15)); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.85rem; + font-weight: 700; + color: var(--primary); + flex-shrink: 0; + text-transform: uppercase; +} + +.contractor-name { + font-weight: 500; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.contractor-meta { + display: flex; + flex-direction: column; + gap: 0.3rem; + min-width: 0; +} + +.contractor-inactive { + opacity: 0.75; +} + +.contractor-actions { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + flex-shrink: 0; +} + +.bulk-form { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.bulk-textarea { + width: 100%; + padding: 0.75rem 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-elevated); + color: var(--text); + font: inherit; + font-size: 0.9rem; + line-height: 1.5; + resize: vertical; + min-height: 140px; +} + +.bulk-textarea:focus { + outline: none; + border-color: rgba(91, 141, 239, 0.5); + box-shadow: 0 0 0 3px var(--primary-glow); +} + +.bulk-textarea::placeholder { + color: var(--text-muted); +} + +.bulk-actions { + display: flex; + justify-content: flex-start; +} + +textarea { + font-family: inherit; +} + +.inline-form { + margin: 0; + flex-shrink: 0; +} + + +.pagination .btn-ghost { + padding: 8px 16px; + border: 1px solid #ddd; + border-radius: 6px; + background: white; + color: #333; + text-decoration: none; + transition: all 0.2s; +} + +.pagination .btn-ghost:hover { + background: #f5f5f5; + border-color: #bbb; +} + +.count-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.5rem; + height: 1.5rem; + padding: 0 0.45rem; + border-radius: 999px; + background: var(--surface-muted); + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); + margin-left: 0.5rem; +} + +.section-label { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0 1rem; + font-size: 0.95rem; + font-weight: 600; + color: var(--text); +} + +/* ── Responsive ── */ + +@media (max-width: 768px) { + .header-inner { + flex-direction: column; + align-items: stretch; + } + + .tabs { + overflow-x: auto; + flex-wrap: nowrap; + } + + .metrics { + grid-template-columns: 1fr; + } + + .filters { + flex-direction: column; + align-items: stretch; + } + + input[type="text"], + select { + min-width: 100%; + } + + .upload-form { + flex-direction: column; + align-items: stretch; + } + + .contractor-card { + flex-direction: column; + align-items: stretch; + } + + .contractor-actions { + justify-content: flex-end; + } +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..3f4208b --- /dev/null +++ b/templates/base.html @@ -0,0 +1,56 @@ + + + + + + {% block title %}Контроль ЭДО{% endblock %} + + + + + + +
+ +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {% if category == 'success' %}✓{% else %}⚠{% endif %} + {{ message }} +
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + diff --git a/templates/contractors.html b/templates/contractors.html new file mode 100644 index 0000000..6fa32b8 --- /dev/null +++ b/templates/contractors.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} + +{% block title %}Контрагенты — Контроль ЭДО{% endblock %} + +{% block content %} + + +
+
+
+
+

Добавить контрагентов

+

Один контрагент — одна строка. Можно вставить список целиком.

+
+
+ +
+ + +
+ +
+
+ +
+ +
+ + + +
+
+ +
+ + + {% if contractors %} +
+ {% for contractor_id, name, active in contractors %} +
+
+
{{ name[:2] }}
+
+ {{ name }} + {% if active %} + Активен + {% else %} + Неактивен + {% endif %} +
+
+
+ {% if active %} +
+ + + +
+ {% else %} +
+ + + +
+ {% endif %} +
+ + + +
+
+
+ {% endfor %} +
+ {% else %} +
+
👥
+

Список пуст

+

Добавьте контрагентов перед импортом документов

+
+ {% endif %} +
+{% endblock %} diff --git a/templates/documents_1c.html b/templates/documents_1c.html new file mode 100644 index 0000000..960c7c3 --- /dev/null +++ b/templates/documents_1c.html @@ -0,0 +1,160 @@ +{% extends "base.html" %} + +{% block title %}Документы 1С — Контроль ЭДО{% endblock %} + +{% block content %} + + +
+
+
+
+

Загрузка Excel из 1С

+

Импортируются только контрагенты из списка. Повторная загрузка обновит статус существующего документа.

+
+
+
+
+ +
+ +
+ +
+
+
+ +
+
+ + + + +
+ + + + {% if rows %} +
+ + + + + + + + + + + + + + + + {% for row in rows %} + {% set has_comment = row.comment and row.comment.strip() %} + + + + + + + + + + + + {% endfor %} + +
КонтрагентТипДатаНомерСуммаСтатусОбновлёнЗаметка
+ {% if has_comment %} + 💬 + {% endif %} + {{ row.contractor or '—' }}{{ row.doc_type }} + {% if row.doc_date %} + {% if row.doc_date is string %} + {{ row.doc_date.replace('-', '.').split(' ')[0].split('-')[::-1]|join('.') }} + {% else %} + {{ row.doc_date.strftime('%d.%m.%Y') }} + {% endif %} + {% else %} + — + {% endif %} + {{ row.doc_number }}{{ "%.2f"|format(row.amount) if row.amount is not none else '—' }} + {% set st = (row.state or '')|lower %} + {% if 'заверш' in st %} + {{ row.state }} + {% elif 'подпис' in st %} + {{ row.state }} + {% elif 'ошиб' in st %} + {{ row.state }} + {% else %} + {{ row.state or '—' }} + {% endif %} + + {% if row.updated_at %} + {% if row.updated_at is string %} + {{ row.updated_at.replace('-', '.').split(' ')[0].split('-')[::-1]|join('.') }} + {% else %} + {{ row.updated_at.strftime('%d.%m.%Y') }} + {% endif %} + {% else %} + — + {% endif %} + +
+ + {% if has_comment %}Изменить{% else %}Добавить{% endif %} + +
+ + + + + + +
+ +
+
+
+
+
+ {% else %} +
+
📭
+

Документов пока нет

+

Добавьте контрагентов и загрузите Excel из 1С

+
+ {% endif %} +
+{% endblock %} diff --git a/templates/documents_sbis.html b/templates/documents_sbis.html new file mode 100644 index 0000000..994e9d2 --- /dev/null +++ b/templates/documents_sbis.html @@ -0,0 +1,186 @@ +{% extends "base.html" %} + +{% block title %}Документы СБИС — Контроль ЭДО{% endblock %} + +{% block content %} + + +
+
+
+
+

Загрузка Excel из СБИС

+

Импортируются только контрагенты из списка. Повторная загрузка обновит статус существующего документа.

+
+
+
+
+ +
+ +
+ +
+
+
+ +
+
+ + + + +
+ + + + {% if rows %} +
+ + + + + + + + + + + + + + + + + {% for row in rows %} + {% set has_comment = row.comment and row.comment.strip() %} + + + + + + + + + + + + + {% endfor %} + +
ДатаНомерКонтрагентТипСуммаСтатусОтветственныйОбновлёнЗаметка
+ {% if has_comment %} + 💬 + {% endif %} + + {% if row.doc_date %} + {% if row.doc_date is string %} + {% set date_parts = row.doc_date.split(' ')[0].split('-') %} + {{ date_parts[2] }}.{{ date_parts[1] }}.{{ date_parts[0] }} + {% else %} + {{ row.doc_date.strftime('%d.%m.%Y') }} + {% endif %} + {% else %} + — + {% endif %} + {{ row.doc_number }}{{ row.contractor }}{{ row.doc_type or '—' }}{{ "%.2f"|format(row.amount) if row.amount is not none else '—' }} + {% set st = (row.state or '')|lower %} + {% if 'заверш' in st or 'успеш' in st %} + {{ row.state }} + {% elif 'подпис' in st or 'обработ' in st %} + {{ row.state }} + {% elif 'удал' in st or 'ошиб' in st %} + {{ row.state }} + {% else %} + {{ row.state or '—' }} + {% endif %} + {{ row.responsible or '—' }} + {% if row.updated_at %} + {% if row.updated_at is string %} + {% set date_parts = row.updated_at.split(' ')[0].split('-') %} + {{ date_parts[2] }}.{{ date_parts[1] }}.{{ date_parts[0] }} + {% else %} + {{ row.updated_at.strftime('%d.%m.%Y') }} + {% endif %} + {% else %} + — + {% endif %} + +
+ + {% if has_comment %}Изменить{% else %}Добавить{% endif %} + +
+ + + + + + +
+ +
+
+
+
+
+ + {% if total_pages > 1 %} + + {% endif %} + + {% else %} +
+
📭
+

Документов СБИС пока нет

+

Добавьте контрагентов и загрузите Excel из СБИС

+
+ {% endif %} +
+{% endblock %}