From 2534096054a7045417ea3b4c64d92650465a93ab Mon Sep 17 00:00:00 2001 From: user_2k Date: Tue, 21 Apr 2026 09:26:47 +0500 Subject: [PATCH] 20260421 --- app.py | 380 ++++++++++++++++++++++++++ requirements.txt | 1 + static/css/style.css | 439 +++++++++++++++++++++++++++++++ static/js/main.js | 13 + stroy.db | Bin 0 -> 28672 bytes templates/base.html | 45 ++++ templates/counterparties.html | 36 +++ templates/counterparty_form.html | 43 +++ templates/expense_form.html | 117 ++++++++ templates/expense_list.html | 38 +++ templates/income_form.html | 69 +++++ templates/income_list.html | 36 +++ templates/index.html | 44 ++++ templates/material_form.html | 39 +++ templates/order_form.html | 151 +++++++++++ templates/orders.html | 59 +++++ templates/orders_list.html | 70 +++++ templates/partner_form.html | 20 ++ templates/partners.html | 26 ++ templates/supplier_form.html | 25 ++ templates/suppliers.html | 28 ++ 21 files changed, 1679 insertions(+) create mode 100644 app.py create mode 100644 requirements.txt create mode 100644 static/css/style.css create mode 100644 static/js/main.js create mode 100644 stroy.db create mode 100644 templates/base.html create mode 100644 templates/counterparties.html create mode 100644 templates/counterparty_form.html create mode 100644 templates/expense_form.html create mode 100644 templates/expense_list.html create mode 100644 templates/income_form.html create mode 100644 templates/income_list.html create mode 100644 templates/index.html create mode 100644 templates/material_form.html create mode 100644 templates/order_form.html create mode 100644 templates/orders.html create mode 100644 templates/orders_list.html create mode 100644 templates/partner_form.html create mode 100644 templates/partners.html create mode 100644 templates/supplier_form.html create mode 100644 templates/suppliers.html diff --git a/app.py b/app.py new file mode 100644 index 0000000..95ab47a --- /dev/null +++ b/app.py @@ -0,0 +1,380 @@ +from flask import Flask, render_template, request, redirect, url_for, flash, Response, jsonify +import sqlite3, csv, io +from datetime import date + +def validate_email(email): + if not email: + return True + return '@' in email and '.' in email.split('@')[-1] + +def validate_phone(phone): + if not phone: + return True + digits = ''.join(filter(str.isdigit, phone)) + return len(digits) == 11 + +app = Flask(__name__) +app.secret_key = 'stroy_key_2025' +DB_PATH = 'stroy.db' + +# ------------------------------------------------------- +# СОЗДАНИЕ 6 ТАБЛИЦ +# ------------------------------------------------------- + +def init_db(): + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + + # 1. Типы материалов + cur.execute("""CREATE TABLE IF NOT EXISTS material_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT + )""") + + # 2. Материалы + cur.execute("""CREATE TABLE IF NOT EXISTS materials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + unit TEXT NOT NULL DEFAULT 'шт', + current_price REAL NOT NULL DEFAULT 0, + material_type_id INTEGER NOT NULL, + FOREIGN KEY (material_type_id) REFERENCES material_types(id) + )""") + + # 3. Контрагенты (и поставщики и партнёры в одной таблице) + # type=1 — партнёр (расходная), type=2 — поставщик (приходная) + cur.execute("""CREATE TABLE IF NOT EXISTS counterparties ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + contact_person TEXT, + phone TEXT, + email TEXT, + address TEXT, + type INTEGER NOT NULL DEFAULT 1 + )""") + + # 4. Заказы + cur.execute("""CREATE TABLE IF NOT EXISTS purchase_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + number TEXT NOT NULL, + order_date TEXT NOT NULL, + delivery_date TEXT, + status TEXT NOT NULL DEFAULT 'Новый', + counterparty_id INTEGER NOT NULL, + FOREIGN KEY (counterparty_id) REFERENCES counterparties(id) + )""") + + # 5. Позиции заказа + cur.execute("""CREATE TABLE IF NOT EXISTS order_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + material_id INTEGER NOT NULL, + quantity REAL NOT NULL DEFAULT 1, + unit_price REAL NOT NULL DEFAULT 0, + FOREIGN KEY (order_id) REFERENCES purchase_orders(id), + FOREIGN KEY (material_id) REFERENCES materials(id) + )""") + + conn.commit() + + # Начальные данные + if cur.execute("SELECT COUNT(*) FROM material_types").fetchone()[0] == 0: + for t in [('Кирпич и блоки','Кирпич, блоки, камень'),('Цемент и смеси','Цемент, штукатурка, шпатлёвка'), + ('Пиломатериалы','Доски, брус, фанера'),('Кровельные материалы','Профнастил, черепица'), + ('Теплоизоляция','Минвата, пенопласт'),('Отделочные материалы','Плитка, гипсокартон')]: + cur.execute("INSERT INTO material_types (name, description) VALUES (?,?)", t) + + for m in [('Кирпич керамический М150',1,'шт',18.5),('Газобетонный блок D500',1,'шт',185.0), + ('Цемент ПЦ 500, 50 кг',2,'мешок',450.0),('Штукатурная смесь Ротбанд',2,'мешок',620.0), + ('Доска обрезная 50x150x6000',3,'шт',850.0),('Фанера ФСФ 18 мм',3,'лист',1850.0), + ('Профнастил НС35',4,'м²',580.0),('Черепица Braas',4,'шт',95.0), + ('Минвата Knauf 100 мм',5,'м²',320.0),('Пенопласт ПСБ-С 35',5,'м²',185.0), + ('Плитка керамическая 30x30',6,'м²',850.0),('Гипсокартон Knauf 12.5 мм',6,'лист',520.0)]: + cur.execute("INSERT INTO materials (name, material_type_id, unit, current_price) VALUES (?,?,?,?)", m) + + # type=1 партнёры, type=2 поставщики + for c in [('ООО СтройГрупп','Иванов И.И.','+7(495)111-22-33','info@stroygroup.ru','Москва, ул. Ленина, 10',1), + ('ИП Петров А.А.','Петров А.А.','+7(903)222-33-44','petrov@mail.ru','Московская обл., г. Химки',1), + ('АО РемСтрой','Сидоров С.С.','+7(495)333-44-55','rem@stroy.ru','Москва, пр-т Мира, 55',1), + ('СтройОптТорг','Козлов К.К.','+7(495)123-45-67','info@stroyopt.ru','Москва, ул. Строителей, 15',2), + ('КирпичМастер','Морозов М.М.','+7(499)234-56-78','sales@kirpich.ru','Подольск, пр-т Ленина, 42',2), + ('ЛесТорг Урал','Лесов Л.Л.','+7(343)345-67-89','les@ural.com','Екатеринбург, ул. Лесная, 8',2)]: + cur.execute("INSERT INTO counterparties (name,contact_person,phone,email,address,type) VALUES (?,?,?,?,?,?)", c) + + # Заказы + cur.execute("INSERT INTO purchase_orders (number,order_date,delivery_date,status,counterparty_id) VALUES ('ЗК-001','2026-01-15','2026-01-20','Выполнен',4)") + cur.execute("INSERT INTO purchase_orders (number,order_date,delivery_date,status,counterparty_id) VALUES ('ЗК-002','2026-02-10','2026-02-15','Выполнен',1)") + cur.execute("INSERT INTO purchase_orders (number,order_date,delivery_date,status,counterparty_id) VALUES ('ЗК-003','2026-03-01',NULL,'Новый',5)") + + # Позиции заказов + cur.execute("INSERT INTO order_items (order_id,material_id,quantity,unit_price) VALUES (1,1,5000,18.5)") + cur.execute("INSERT INTO order_items (order_id,material_id,quantity,unit_price) VALUES (1,3,100,450.0)") + cur.execute("INSERT INTO order_items (order_id,material_id,quantity,unit_price) VALUES (2,5,50,850.0)") + cur.execute("INSERT INTO order_items (order_id,material_id,quantity,unit_price) VALUES (2,9,200,320.0)") + cur.execute("INSERT INTO order_items (order_id,material_id,quantity,unit_price) VALUES (3,7,100,580.0)") + + conn.commit() + conn.close() + +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + +# ------------------------------------------------------- +# ГЛАВНАЯ — материалы +# ------------------------------------------------------- + +@app.route('/') +def index(): + search = request.args.get('search','') + type_filter = request.args.get('type_id','') + conn = get_db(); cur = conn.cursor() + sql = "SELECT m.*, mt.name AS type_name FROM materials m JOIN material_types mt ON m.material_type_id=mt.id WHERE 1=1" + params = [] + if search: sql += " AND m.name LIKE ?"; params.append('%'+search+'%') + if type_filter: sql += " AND m.material_type_id=?"; params.append(type_filter) + sql += " ORDER BY m.name" + cur.execute(sql, params) + materials = [dict(r) for r in cur.fetchall()] + cur.execute("SELECT * FROM material_types ORDER BY name"); types = cur.fetchall() + stat = { + 'materials': cur.execute("SELECT COUNT(*) FROM materials").fetchone()[0], + 'counterparties': cur.execute("SELECT COUNT(*) FROM counterparties").fetchone()[0], + 'partners': cur.execute("SELECT COUNT(*) FROM counterparties WHERE type=1").fetchone()[0], + 'suppliers': cur.execute("SELECT COUNT(*) FROM counterparties WHERE type=2").fetchone()[0], + 'orders': cur.execute("SELECT COUNT(*) FROM purchase_orders").fetchone()[0], + } + conn.close() + return render_template('index.html', materials=materials, types=types, search=search, type_filter=type_filter, stat=stat) + +@app.route('/materials/add', methods=['GET','POST']) +def material_add(): + conn = get_db(); cur = conn.cursor() + cur.execute("SELECT * FROM material_types ORDER BY name"); types = cur.fetchall(); conn.close() + if request.method == 'POST': + conn = get_db(); cur = conn.cursor() + cur.execute("INSERT INTO materials (name,material_type_id,unit,current_price) VALUES (?,?,?,?)", + (request.form['name'],request.form['material_type_id'],request.form['unit'],request.form['current_price'])) + conn.commit(); conn.close() + flash('Материал добавлен','success'); return redirect(url_for('index')) + return render_template('material_form.html', material=None, types=types, action='add') + +@app.route('/materials/edit/', methods=['GET','POST']) +def material_edit(mid): + conn = get_db(); cur = conn.cursor() + cur.execute("SELECT * FROM materials WHERE id=?",(mid,)); material = cur.fetchone() + cur.execute("SELECT * FROM material_types ORDER BY name"); types = cur.fetchall(); conn.close() + if not material: flash('Не найдено','error'); return redirect(url_for('index')) + if request.method == 'POST': + conn = get_db(); cur = conn.cursor() + cur.execute("UPDATE materials SET name=?,material_type_id=?,unit=?,current_price=? WHERE id=?", + (request.form['name'],request.form['material_type_id'],request.form['unit'],request.form['current_price'],mid)) + conn.commit(); conn.close() + flash('Материал обновлён','success'); return redirect(url_for('index')) + return render_template('material_form.html', material=material, types=types, action='edit') + +# ------------------------------------------------------- +# КОНТРАГЕНТЫ +# ------------------------------------------------------- + +@app.route('/counterparties') +def counterparties_list(): + ctype = request.args.get('type','') + conn = get_db(); cur = conn.cursor() + sql = "SELECT c.*, COUNT(o.id) AS order_count FROM counterparties c LEFT JOIN purchase_orders o ON c.id=o.counterparty_id WHERE 1=1" + params = [] + if ctype: sql += " AND c.type=?"; params.append(ctype) + sql += " GROUP BY c.id ORDER BY c.name" + cur.execute(sql, params); counterparties = cur.fetchall(); conn.close() + return render_template('counterparties.html', counterparties=counterparties, ctype=ctype) + +@app.route('/counterparties/add', methods=['GET','POST']) +def counterparty_add(): + is_ajax = request.args.get('ajax')=='1' + default_type = request.args.get('type','1') + if request.method == 'POST': + name = request.form.get('name','').strip() + phone = request.form.get('phone','').strip() + email = request.form.get('email','').strip() + + # Проверки + if not name: + flash('Название обязательно', 'error') + elif phone and not validate_phone(phone): + flash('Неверный формат телефона. Должно быть 10 или 11 цифр', 'error') + elif email and not validate_email(email): + flash('Неверный формат email. Нужен символ @ и точка', 'error') + else: + conn = get_db(); cur = conn.cursor() + cur.execute("INSERT INTO counterparties (name,contact_person,phone,email,address,type) VALUES (?,?,?,?,?,?)", + (name,request.form.get('contact_person',''),phone,email,request.form.get('address',''),request.form.get('type',1))) + new_id = cur.lastrowid; conn.commit(); conn.close() + if is_ajax: return jsonify({'id':new_id,'name':name}) + flash('Контрагент добавлен','success') + return redirect(url_for('counterparties_list')) + + return render_template('counterparty_form.html', counterparty=None, action='add', default_type=default_type) + +@app.route('/counterparties/edit/', methods=['GET','POST']) +def counterparty_edit(cid): + conn = get_db(); cur = conn.cursor() + cur.execute("SELECT * FROM counterparties WHERE id=?",(cid,)); counterparty = cur.fetchone() + conn.close() + if not counterparty: + flash('Не найдено','error') + return redirect(url_for('counterparties_list')) + + if request.method == 'POST': + name = request.form.get('name','').strip() + phone = request.form.get('phone','').strip() + email = request.form.get('email','').strip() + + # Проверки + if not name: + flash('Название обязательно', 'error') + elif phone and not validate_phone(phone): + flash('Неверный формат телефона. Должно быть ровно 11 цифр', 'error') + elif email and not validate_email(email): + flash('Неверный формат email. Нужен символ @ и точка', 'error') + else: + conn = get_db(); cur = conn.cursor() + cur.execute("UPDATE counterparties SET name=?,contact_person=?,phone=?,email=?,address=?,type=? WHERE id=?", + (name,request.form.get('contact_person',''),phone,email,request.form.get('address',''),request.form.get('type',1),cid)) + conn.commit(); conn.close() + flash('Контрагент обновлён','success') + return redirect(url_for('counterparties_list')) + + return render_template('counterparty_form.html', counterparty=counterparty, action='edit', default_type=counterparty['type']) + +# ------------------------------------------------------- +# ЗАКАЗЫ +# ------------------------------------------------------- + +@app.route('/orders') +def orders_list(): + sort = request.args.get('sort', 'date_desc') + search = request.args.get('search', '').strip() + + conn = get_db() + cur = conn.cursor() + + # Базовый SQL с поиском + sql = """ + SELECT o.*, c.name AS counterparty_name, c.type AS counterparty_type, + COUNT(oi.id) AS items_count + FROM purchase_orders o + JOIN counterparties c ON o.counterparty_id = c.id + LEFT JOIN order_items oi ON o.id = oi.order_id + """ + params = [] + + if search: + sql += " WHERE o.number LIKE ? OR c.name LIKE ? OR o.status LIKE ?" + search_pattern = f'%{search}%' + params = [search_pattern, search_pattern, search_pattern] + + sql += " GROUP BY o.id" + + # Сортировка + if sort == 'date_asc': + sql += " ORDER BY o.order_date ASC" + elif sort == 'date_desc': + sql += " ORDER BY o.order_date DESC" + elif sort == 'price_asc': + sql += " ORDER BY (SELECT SUM(quantity * unit_price) FROM order_items WHERE order_id = o.id) ASC" + elif sort == 'price_desc': + sql += " ORDER BY (SELECT SUM(quantity * unit_price) FROM order_items WHERE order_id = o.id) DESC" + else: + sql += " ORDER BY o.order_date DESC" + + cur.execute(sql, params) + orders_raw = cur.fetchall() + + orders = [] + for o in orders_raw: + # Считаем итого по позициям + cur.execute("SELECT SUM(quantity * unit_price) FROM order_items WHERE order_id = ?", (o['id'],)) + total = cur.fetchone()[0] or 0 + orders.append({**dict(o), 'total': round(total, 2)}) + + conn.close() + + return render_template('orders_list.html', orders=orders, sort=sort, search=search) + + +@app.route('/orders/add', methods=['GET','POST']) +def order_add(): + conn = get_db(); cur = conn.cursor() + cur.execute("SELECT * FROM counterparties ORDER BY type, name"); counterparties = cur.fetchall() + cur.execute("SELECT m.*, mt.name AS type_name FROM materials m JOIN material_types mt ON m.material_type_id=mt.id ORDER BY m.name") + materials = cur.fetchall(); conn.close() + if request.method == 'POST': + conn = get_db(); cur = conn.cursor() + cur.execute("INSERT INTO purchase_orders (number,order_date,delivery_date,status,counterparty_id) VALUES (?,?,?,?,?)", + (request.form['number'],request.form['order_date'],request.form.get('delivery_date',''), + request.form.get('status','Новый'),request.form['counterparty_id'])) + order_id = cur.lastrowid + # Сохраняем позиции + mat_ids = request.form.getlist('material_id[]') + quantities = request.form.getlist('quantity[]') + prices = request.form.getlist('unit_price[]') + for mid, qty, price in zip(mat_ids, quantities, prices): + if mid and qty and float(qty) > 0: + cur.execute("INSERT INTO order_items (order_id,material_id,quantity,unit_price) VALUES (?,?,?,?)", + (order_id, mid, qty, price)) + conn.commit(); conn.close() + flash('Заказ добавлен','success'); return redirect(url_for('orders_list')) + today = date.today().strftime('%Y-%m-%d') + return render_template('order_form.html', order=None, items=[], counterparties=counterparties, materials=materials, action='add', today=today) + +@app.route('/orders/edit/', methods=['GET','POST']) +def order_edit(oid): + conn = get_db(); cur = conn.cursor() + cur.execute("SELECT * FROM purchase_orders WHERE id=?",(oid,)); order = cur.fetchone() + cur.execute("SELECT oi.*, m.name AS material_name FROM order_items oi JOIN materials m ON oi.material_id=m.id WHERE oi.order_id=?",(oid,)) + items = cur.fetchall() + cur.execute("SELECT * FROM counterparties ORDER BY type, name"); counterparties = cur.fetchall() + cur.execute("SELECT m.*, mt.name AS type_name FROM materials m JOIN material_types mt ON m.material_type_id=mt.id ORDER BY m.name") + materials = cur.fetchall(); conn.close() + if not order: flash('Не найдено','error'); return redirect(url_for('orders_list')) + if request.method == 'POST': + conn = get_db(); cur = conn.cursor() + cur.execute("UPDATE purchase_orders SET number=?,order_date=?,delivery_date=?,status=?,counterparty_id=? WHERE id=?", + (request.form['number'],request.form['order_date'],request.form.get('delivery_date',''), + request.form.get('status','Новый'),request.form['counterparty_id'],oid)) + cur.execute("DELETE FROM order_items WHERE order_id=?",(oid,)) + mat_ids = request.form.getlist('material_id[]') + quantities = request.form.getlist('quantity[]') + prices = request.form.getlist('unit_price[]') + for mid, qty, price in zip(mat_ids, quantities, prices): + if mid and qty and float(qty) > 0: + cur.execute("INSERT INTO order_items (order_id,material_id,quantity,unit_price) VALUES (?,?,?,?)", + (oid, mid, qty, price)) + conn.commit(); conn.close() + flash('Заказ обновлён','success'); return redirect(url_for('orders_list')) + return render_template('order_form.html', order=order, items=items, counterparties=counterparties, materials=materials, action='edit', today=order['order_date']) + +@app.route('/api/material/') +def api_material(mid): + conn = get_db(); cur = conn.cursor() + cur.execute("SELECT * FROM materials WHERE id=?",(mid,)); m = cur.fetchone(); conn.close() + if not m: return jsonify({}), 404 + return jsonify({'price': m['current_price'], 'unit': m['unit'], 'name': m['name']}) + +@app.route('/export/csv') +def export_csv(): + conn = get_db(); cur = conn.cursor() + cur.execute("SELECT m.name, mt.name AS type_name, m.unit, m.current_price FROM materials m JOIN material_types mt ON m.material_type_id=mt.id ORDER BY m.name") + rows = cur.fetchall(); conn.close() + output = io.StringIO(); writer = csv.writer(output) + writer.writerow(['Наименование','Тип','Ед.','Цена']) + for r in rows: writer.writerow([r['name'],r['type_name'],r['unit'],r['current_price']]) + output.seek(0) + return Response('\ufeff'+output.read(), mimetype='text/csv; charset=utf-8-sig', + headers={'Content-Disposition':'attachment; filename=materials.csv'}) + +if __name__ == '__main__': + init_db() + app.run(debug=True, port=5000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0647450 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +flask==3.0.3 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..ba2c25e --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,439 @@ +/* ============================================================ + СтройСклад — Вариант №6 + Стиль: Индустриальный, боковая навигация + Цвета варианта: #FAEBD7, #DEB887, #A0522D + Шрифт: Verdana + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600;700&display=swap'); + +:root { + --cream: #FAEBD7; + --sand: #DEB887; + --brown: #A0522D; + --dark: #1C1008; + --white: #FFFFFF; + --light-bg: #F9F4EE; + --border: #E8D5BC; + --text: #1C1008; + --text-mid: #6B4C30; + --text-light: #A0826A; + --shadow: rgba(160, 82, 45, 0.10); + --success: #3D7A35; + --error: #B83030; + --sidebar-w: 220px; +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { font-size: 14px; } + +body { + font-family: Verdana, Geneva, sans-serif; + background: var(--light-bg); + color: var(--text); + line-height: 1.6; + min-height: 100vh; + display: flex; +} + +/* ─── БОКОВАЯ НАВИГАЦИЯ ─────────────────────────────────────── */ +.sidebar { + width: var(--sidebar-w); + min-height: 100vh; + background: var(--dark); + display: flex; + flex-direction: column; + position: fixed; + left: 0; + top: 0; + bottom: 0; + z-index: 200; + border-right: 3px solid var(--brown); +} + +.sidebar-logo { + padding: 1.5rem 1.2rem 1.2rem; + border-bottom: 1px solid rgba(222,184,135,0.15); +} +.sidebar-logo .logo-icon { + width: 36px; + height: 36px; + margin-bottom: 0.6rem; + display: block; +} +.sidebar-logo .logo-text { + font-size: 1.1rem; + font-weight: 700; + color: var(--cream); + text-transform: uppercase; + letter-spacing: 0.12em; + display: block; + line-height: 1.2; +} +.sidebar-logo .logo-sub { + font-size: 0.65rem; + color: var(--sand); + opacity: 0.7; + letter-spacing: 0.04em; + display: block; + margin-top: 0.2rem; +} + +.sidebar-nav { + flex: 1; + padding: 1rem 0; +} +.nav-section-title { + font-size: 0.6rem; + font-weight: 700; + color: rgba(222,184,135,0.45); + text-transform: uppercase; + letter-spacing: 0.12em; + padding: 0.8rem 1.2rem 0.3rem; +} +.nav-link { + display: flex; + align-items: center; + gap: 0.65rem; + padding: 0.65rem 1.2rem; + color: rgba(250,235,215,0.65); + text-decoration: none; + font-size: 0.85rem; + font-weight: 500; + border-left: 3px solid transparent; + transition: all 0.15s; +} +.nav-link:hover { + background: rgba(222,184,135,0.08); + color: var(--cream); + border-left-color: rgba(222,184,135,0.4); +} +.nav-link.active { + background: rgba(160,82,45,0.25); + color: var(--sand); + border-left-color: var(--brown); + font-weight: 700; +} +.nav-icon { font-size: 1rem; width: 18px; text-align: center; } + +.sidebar-footer { + padding: 1rem 1.2rem; + border-top: 1px solid rgba(222,184,135,0.15); + font-size: 0.65rem; + color: rgba(222,184,135,0.35); + line-height: 1.5; +} + +/* ─── ОСНОВНОЙ КОНТЕНТ ──────────────────────────────────────── */ +.main-wrapper { + margin-left: var(--sidebar-w); + flex: 1; + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.topbar { + background: var(--white); + border-bottom: 1px solid var(--border); + padding: 0.9rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + position: sticky; + top: 0; + z-index: 100; +} +.topbar-title { + font-size: 1.2rem; + font-weight: 700; + color: var(--text); + letter-spacing: -0.01em; +} +.topbar-title span { + display: inline-block; + width: 4px; + height: 1.1em; + background: var(--brown); + border-radius: 2px; + margin-right: 0.6rem; + vertical-align: middle; +} +.topbar-actions { display: flex; gap: 0.5rem; } + +.page-body { + flex: 1; + padding: 1.8rem 2rem; +} + +/* ─── FLASH ─────────────────────────────────────────────────── */ +.flash-container { margin-bottom: 1.2rem; } +.flash { + padding: 0.7rem 1rem 0.7rem 1.2rem; + border-radius: 6px; + margin-bottom: 0.4rem; + font-size: 0.85rem; + font-weight: 600; + border-left: 4px solid; +} +.flash-success { background: #F0FAF0; border-color: var(--success); color: #2A5C25; } +.flash-error { background: #FDF2F2; border-color: var(--error); color: var(--error); } + +/* ─── СТАТИСТИКА ────────────────────────────────────────────── */ +.stats-bar { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + margin-bottom: 1.5rem; +} +.stat-card { + background: var(--white); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem 1.3rem; + position: relative; + overflow: hidden; +} +.stat-card::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 3px; + background: var(--brown); +} +.stat-label { + font-size: 0.65rem; + font-weight: 700; + color: var(--text-light); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 0.4rem; +} +.stat-value { + font-size: 1.8rem; + font-weight: 700; + color: var(--brown); + line-height: 1; +} + +/* ─── ФИЛЬТРЫ ───────────────────────────────────────────────── */ +.filter-bar { + background: var(--white); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem 1.3rem; + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; + margin-bottom: 1.2rem; +} +.filter-group { display: flex; flex-direction: column; gap: 0.3rem; } +.filter-group label { + font-size: 0.65rem; + font-weight: 700; + color: var(--text-light); + text-transform: uppercase; + letter-spacing: 0.08em; +} +.filter-btns { flex-direction: row; align-items: flex-end; } + +/* ─── ИНПУТЫ ────────────────────────────────────────────────── */ +.form-input { + font-family: Verdana, sans-serif; + font-size: 0.85rem; + border: 1.5px solid var(--border); + border-radius: 6px; + padding: 0.48rem 0.8rem; + background: var(--white); + color: var(--text); + outline: none; + transition: border-color 0.18s, box-shadow 0.18s; + min-width: 130px; +} +.form-input:focus { + border-color: var(--brown); + box-shadow: 0 0 0 3px rgba(160,82,45,0.1); +} +.form-textarea { resize: vertical; min-height: 80px; width: 100%; } + +/* ─── КНОПКИ ────────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.48rem 1rem; + border-radius: 6px; + font-family: Verdana, sans-serif; + font-size: 0.82rem; + font-weight: 700; + cursor: pointer; + border: 1.5px solid transparent; + text-decoration: none; + transition: all 0.15s; + letter-spacing: 0.01em; +} +.btn:hover { transform: translateY(-1px); } +.btn:active { transform: none; } +.btn-primary { background: var(--brown); color: var(--white); border-color: var(--brown); } +.btn-primary:hover { background: #8B4520; border-color: #8B4520; } +.btn-secondary { background: var(--white); color: var(--text-mid); border-color: var(--border); } +.btn-secondary:hover { border-color: var(--brown); color: var(--brown); } +.btn-edit { background: var(--white); color: #3A6A18; border-color: #B0D080; font-size: 0.78rem; } +.btn-edit:hover { background: #F0FAE8; } +.btn-danger { background: var(--white); color: var(--error); border-color: #DDAAAA; font-size: 0.78rem; } +.btn-danger:hover { background: #FDF2F2; } +.btn-sm { padding: 0.3rem 0.65rem; font-size: 0.76rem; } + +/* ─── ТАБЛИЦА ───────────────────────────────────────────────── */ +.table-wrapper { + background: var(--white); + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 6px var(--shadow); +} +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 0.83rem; +} +.data-table thead tr { + background: var(--cream); + border-bottom: 2px solid var(--sand); +} +.data-table th { + padding: 0.7rem 1rem; + text-align: left; + font-weight: 700; + font-size: 0.67rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-mid); + white-space: nowrap; +} +.data-table td { + padding: 0.62rem 1rem; + border-bottom: 1px solid var(--border); + vertical-align: middle; + color: var(--text); +} +.data-table tbody tr:last-child td { border-bottom: none; } +.data-table tbody tr:hover { background: #FBF6F0; } +.calc-col { color: var(--text-mid); } +.highlight-col strong { color: var(--brown); font-size: 0.92rem; } +.low-stock { color: var(--error); font-weight: 700; } +.table-meta { + padding: 0.6rem 0.2rem; + font-size: 0.75rem; + color: var(--text-light); +} + +/* ─── БЕЙДЖ ─────────────────────────────────────────────────── */ +.badge { + display: inline-block; + background: var(--cream); + color: var(--brown); + border-radius: 4px; + padding: 0.12rem 0.5rem; + font-size: 0.7rem; + font-weight: 700; + border: 1px solid var(--sand); + white-space: nowrap; + letter-spacing: 0.02em; +} +code { + background: var(--cream); + color: var(--brown); + padding: 0.1rem 0.35rem; + border-radius: 3px; + font-size: 0.76rem; + border: 1px solid var(--sand); +} + +/* ─── ФОРМА ─────────────────────────────────────────────────── */ +.form-card { + background: var(--white); + border: 1px solid var(--border); + border-radius: 8px; + padding: 2rem; + max-width: 860px; +} +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.2rem; +} +.form-group { display: flex; flex-direction: column; gap: 0.3rem; } +.form-group.full-width { grid-column: 1 / -1; } +.form-label { + font-size: 0.68rem; + font-weight: 700; + color: var(--text-mid); + text-transform: uppercase; + letter-spacing: 0.08em; +} +.required { color: var(--error); } +.form-actions { + display: flex; + gap: 0.75rem; + margin-top: 1.6rem; + padding-top: 1.2rem; + border-top: 1px solid var(--border); +} + +/* ─── РАСЧЁТ ────────────────────────────────────────────────── */ +.calc-preview { + background: var(--cream); + border: 1px solid var(--sand); + border-radius: 8px; + padding: 1.2rem 1.5rem; + margin-top: 1.3rem; +} +.calc-title { + font-weight: 700; + color: var(--brown); + margin-bottom: 0.8rem; + font-size: 0.82rem; + text-transform: uppercase; + letter-spacing: 0.06em; +} +.calc-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.38rem 0; + border-bottom: 1px solid rgba(160,82,45,0.12); + font-size: 0.85rem; + color: var(--text-mid); +} +.calc-row:last-child { border-bottom: none; } +.total-row { + font-weight: 700; + color: var(--brown); + font-size: 1rem; + padding-top: 0.6rem; + color: var(--text); +} +.preview-qty-input { width: 80px; text-align: center; } + +/* ─── RESPONSIVE ────────────────────────────────────────────── */ +@media (max-width: 900px) { + .sidebar { display: none; } + .main-wrapper { margin-left: 0; } + .stats-bar { grid-template-columns: 1fr 1fr; } + .form-grid { grid-template-columns: 1fr; } + .form-group.full-width { grid-column: 1; } +} + +/* ─── СОРТИРОВКА ────────────────────────────────────────────── */ +.sort-bar { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + font-size: 0.8rem; + color: var(--text-light); + flex-wrap: wrap; +} diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..af0825b --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,13 @@ +// main.js — СтройСклад + +// Подтверждение удаления (резерв) +document.querySelectorAll('[data-confirm]').forEach(el => { + el.addEventListener('click', e => { + if (!confirm(el.dataset.confirm)) e.preventDefault(); + }); +}); + +// Подсветка строк таблицы при клике +document.querySelectorAll('.data-table tbody tr').forEach(row => { + row.style.cursor = 'default'; +}); diff --git a/stroy.db b/stroy.db new file mode 100644 index 0000000000000000000000000000000000000000..9c5468efce88cf764a4d5a3f46b029a200bba344 GIT binary patch literal 28672 zcmeI4-*Xe!70367w6eg~W|Co;=?udqG{q`jRX;3@+orW+1jFE%;Fy`ZdGILGQZ%t8 zv63=zC!N8D0y7~)Xqtur3U!mFeKfx&GPd!Qm;QLk-G@GQI@9M&|Av0=l~>Zr6+@ep zr?cqC?!D)pJ?C?-gzuiSdi}(Bp=z^JC1);MWhv!3MOBp%#uP=_j%Nr@Zwq24;2q#y zZQJj*yIqOpls@ztJ+5N@Ao68jsrMV(&ftJHB0vO)01+SpM1Tl4VySV#U@UUkGG`o$Cm9Bva>TdOLwU= zwQR9ksLl&RVDLEe6tP2@!=oq1r&yw;vQ{iqXUa|?XLl2hw}cNLpUfORG9ex5_e_p9 z&T}|3nVA^Nyw1usCwDqqv1g<}rN5AmcG1x0P860lHF)iEtV9$&_pM-P=l=cb1zpCc z;~X7_hA&PXon2xjqvkPvihvbsb8p&?9MyQ4%KQl7+7hw`r91++DceOZZ_gIqww?JI zPgKaYxK*mzYOT`kbj`r-%USr#dgD&J*U2FTx$;d@*Y4 zE7)y;TAU1oc1EM>`574~znK3h=nIp-uX~jKn;2#1JnDj67xKAMv6{`{Lts0VQc+5^ z(3MY@ifualL9yquh1ph2{GHi+-mxpqkoh~S^W`?JTjJ4-XQDCt7k{dUcJAJ-{(MYA zZGJwQI{_ccW^;FntvT>CbKTFeD@pNP(!$Y6yy?_F_maEVK0RxWV;7s>LQnBMm;zKrGv?pa>v%X}4$w;@nR+e5xQ!1z71Z@3G%#)V_|EZQIO^^sse89l*&3uPNZ z9b`Vo>#BRjy@0V-_)WB|^Se^F(6Rx^bw~*5Wp}|{JQ09N)BHNli>qhCDEIOcOcG-5 zd8m+sp?r0K!3SZsFrkjqE|2KasN13!O1C=pOXygD8+90T2dt+)4JB(ABr=Oh-q!x9P1y##v%(pC2=97Ee76>Y5g-CYfCvx) zB0vO)01+Sp&z^u8)c2~JHCm#M&3GZ8?^ipDq4eirJ329@6DRfu?p zjT!|*tp9uaex;ayG~YLWZ0?BsBXTElF_Mc6MZ$ewpo2CdKm>>Y5g-CYfCvx)B0vO) z01@~<5*X?=l<8fX%0EONcT1X_9V=#Qr&uBxON&JN8Zx_ek^I(rjDy<^CAR|u+p_H< z-zzfOBIjF|*=>=nUUsjrRQ#P(oUc8;t4|sO+YIGcV`N88ok7_WziRO%mP!k0WhMCS zu%R3g(!PwmXIMPmunV&4NbtoTLmA$os{Hr<#5#M$$!04e-8~cN4;jjsJUa~SNXawY z3bWKWvvw#jU_er!`LvZ_XE<2mpR&Y3ACJ&?^}(Q_ydm^@N&01m96qx2$isgw^Zr*@ zI{r=q&jayzTo|KI>SF;zncIU=f9OcXi$Z}b&CI=fnepGi+C?PSMY8|CNU9@IkD3N; zR!iuHa#Ses<^6B-&v3%{0A55~?zM)fkF}(RuY_Ee7FOBvNA%}FswAQ`0NmL z2xFC%msJt*P`Ewa|DPjX=TLAHu8M&p$+L3)_C>BLM(+*9Tr~CG8<9xgdwt@P0BuBo z2oM1xKm>>Y5g-CYfCvzQXGx%}_b5X{)=MYC`~!YVEVV@a^b(e2SR(QjtoZ6!ay^nq zcNt%b;jw?He{eV*O{G%S;GmUGJNDd2rRtRCV@?fg)kdK<>c>$8z2Tmuuy@P zboyPRNBQ1&te4(W`3wGWYnWlqvz%p$@-t2P9JRyz8ow>Bc3{yhDZ{}JV&bv2 z2|f?UQ_*BHX^FWOFWZ=9Z;y!Ik!bfAkOXph*#N{)gdYR`dkFsTfPD=;^>@`CWh`YS zPKNm{@eeb(ImJH|w-cbae*>0b)*?=l`n4Fp)})?DSkj0V^`AJUTDjc{IpJHAt`+h` zTrBo=v!s}RH9x`|ZA5?w5CI}U1c(3;AOb{y2oM1xKm>@uS10gIJ*4c~)3->Y5g-CYfCvx)B0vO)01+Sp zM1Tl4L%d3x{!_5NW MuoimZl2`x#Z~MMzvj6}9 literal 0 HcmV?d00001 diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..496d3a1 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,45 @@ + + + + + + {% block title %}Стройка{% endblock %} + + + + +
+
+
{% block page_title %}Стройка{% endblock %}
+
{% block topbar_actions %}{% endblock %}
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %}
{% for cat,msg in messages %}
{{ msg }}
{% endfor %}
{% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+
+ + + diff --git a/templates/counterparties.html b/templates/counterparties.html new file mode 100644 index 0000000..1807a87 --- /dev/null +++ b/templates/counterparties.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block title %}Контрагенты — Стройка{% endblock %} +{% block page_title %}{% if ctype=='1' %}Партнёры{% elif ctype=='2' %}Поставщики{% else %}Все контрагенты{% endif %}{% endblock %} +{% block topbar_actions %} + + Добавить +{% endblock %} +{% block content %} + +
+ + + + {% for c in counterparties %} + + + + + + + + + + + + {% else %} + + {% endfor %} + +
#ТипНазваниеКонтактное лицоТелефонEmailАдресЗаказовДействия
{{ c.id }}{% if c.type==2 %}Поставщик{% else %}Партнёр{% endif %}{{ c.name }}{{ c.contact_person or '—' }}{{ c.phone or '—' }}{{ c.email or '—' }}{{ c.address or '—' }}{{ c.order_count }}✏️ Изменить
Нет данных
+
+{% endblock %} diff --git a/templates/counterparty_form.html b/templates/counterparty_form.html new file mode 100644 index 0000000..ad10adc --- /dev/null +++ b/templates/counterparty_form.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% block title %}{% if action=='add' %}Новый контрагент{% else %}Редактировать{% endif %} — Стройка{% endblock %} +{% block page_title %}{% if action=='add' %}Новый контрагент{% else %}Редактировать: {{ counterparty.name }}{% endif %}{% endblock %} +{% block topbar_actions %}← Назад{% endblock %} +{% block content %} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + Отмена +
+
+
+{% endblock %} diff --git a/templates/expense_form.html b/templates/expense_form.html new file mode 100644 index 0000000..57fe8e2 --- /dev/null +++ b/templates/expense_form.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} +{% block title %}{% if action=='add' %}Новая расходная{% else %}Редактировать{% endif %} — Стройка{% endblock %} +{% block page_title %}{% if action=='add' %}Новая расходная накладная{% else %}Редактировать расходную #{{ order.id }}{% endif %}{% endblock %} +{% block topbar_actions %}← Назад{% endblock %} +{% block content %} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
📊 Расчёт стоимости (расходная)
+
Стоимость товара:0.00 ₽
+
Скидка:−0.00 ₽
+
ИТОГО:0.00 ₽
+
+
+ + Отмена +
+
+
+ +{% endblock %} diff --git a/templates/expense_list.html b/templates/expense_list.html new file mode 100644 index 0000000..31cff0a --- /dev/null +++ b/templates/expense_list.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block title %}Расходные накладные — Стройка{% endblock %} +{% block page_title %}Расходные накладные{% endblock %} +{% block topbar_actions %}+ Новая расходная{% endblock %} +{% block content %} + +
+ + + + {% for o in orders %} + + + + + + + + + + + + + + {% else %} + + {% endfor %} + +
НомерДатаТип материалаПартнёрМатериалКол-воСтоимость, ₽Скидка %Скидка, ₽ИТОГО, ₽Действия
{{ o.number }}{{ o.order_date }}{{ o.type_name }}{{ o.partner_name }}{{ o.material_name }}{{ o.quantity }} {{ o.unit }}{{ "%.2f"|format(o.goods) }}{% if o.discount > 0 %}{{ o.discount }}%{% else %}—{% endif %}{% if o.discount_sum > 0 %}−{{ "%.2f"|format(o.discount_sum) }}{% else %}—{% endif %}{{ "%.2f"|format(o.total) }}✏️ Изменить
Накладных пока нет
+
+
Всего: {{ orders|length }}
+{% endblock %} diff --git a/templates/income_form.html b/templates/income_form.html new file mode 100644 index 0000000..30e5e8f --- /dev/null +++ b/templates/income_form.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% block title %}{% if action=='add' %}Новая приходная{% else %}Редактировать{% endif %} — Стройка{% endblock %} +{% block page_title %}{% if action=='add' %}Новая приходная накладная{% else %}Редактировать приходную #{{ order.id }}{% endif %}{% endblock %} +{% block topbar_actions %}← Назад{% endblock %} +{% block content %} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
📊 Расчёт стоимости (приходная)
+
Стоимость товара:0.00 ₽
+
Транспортные расходы:0.00 ₽
+
ИТОГО:0.00 ₽
+
+
+ + Отмена +
+
+
+ +{% endblock %} diff --git a/templates/income_list.html b/templates/income_list.html new file mode 100644 index 0000000..575bc1c --- /dev/null +++ b/templates/income_list.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block title %}Приходные накладные — Стройка{% endblock %} +{% block page_title %}Приходные накладные{% endblock %} +{% block topbar_actions %}+ Новая приходная{% endblock %} +{% block content %} + +
+ + + + {% for o in orders %} + + + + + + + + + + + + {% else %} + + {% endfor %} + +
НомерДатаПоставщикМатериалКол-воСтоимость товара, ₽Транспорт, ₽ИТОГО, ₽Действия
{{ o.number }}{{ o.order_date }}{{ o.supplier_name }}{{ o.material_name }}{{ o.quantity }} {{ o.unit }}{{ "%.2f"|format(o.goods) }}{{ "%.2f"|format(o.transport) }}{{ "%.2f"|format(o.total) }}✏️ Изменить
Накладных пока нет
+
+
Всего: {{ orders|length }}
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..bc40b10 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% block title %}Материалы — Стройка{% endblock %} +{% block page_title %}Склад материалов{% endblock %} +{% block topbar_actions %}+ Добавить материал{% endblock %} +{% block content %} +
+
Материалов
{{ stat.materials }}
+
Поставщиков
{{ stat.suppliers }}
+
Партнёров
{{ stat.partners }}
+
Заказов
{{ stat.orders }}
+
+
+
+
+ +
+
+ + Сброс +
+
+
+ + + + {% for m in materials %} + + + + + + + + {% else %} + + {% endfor %} + +
НаименованиеТипЕд.Цена, ₽Действия
{{ m.name }}{{ m.type_name }}{{ m.unit }}{{ "%.2f"|format(m.current_price) }}✏️ Изменить
Материалы не найдены
+
+
Найдено: {{ materials|length }} материалов
+{% endblock %} diff --git a/templates/material_form.html b/templates/material_form.html new file mode 100644 index 0000000..fbe11e1 --- /dev/null +++ b/templates/material_form.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}{% if action=='add' %}Новый материал{% else %}Редактировать{% endif %} — Стройка{% endblock %} +{% block page_title %}{% if action=='add' %}Новый материал{% else %}Редактировать: {{ material.name }}{% endif %}{% endblock %} +{% block topbar_actions %}← Назад{% endblock %} +{% block content %} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + Отмена +
+
+
+{% endblock %} diff --git a/templates/order_form.html b/templates/order_form.html new file mode 100644 index 0000000..423d4cb --- /dev/null +++ b/templates/order_form.html @@ -0,0 +1,151 @@ +{% extends "base.html" %} +{% block title %}{% if action=='add' %}Новый заказ{% else %}Редактировать заказ{% endif %} — Стройка{% endblock %} +{% block page_title %}{% if action=='add' %}Новый заказ{% else %}Редактировать заказ {{ order.number }}{% endif %}{% endblock %} +{% block topbar_actions %}← Назад{% endblock %} +{% block content %} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Позиции заказа +
+
+ + + + {% for item in items %} + + + + + + + + + {% endfor %} + +
МатериалЕд.КоличествоЦена, ₽Сумма, ₽
+ + {{ item.unit if item else '—' }}{{ "%.2f"|format(item.quantity * item.unit_price) }} ₽
+
+ +
+ + +
+
📊 Итого по заказу
+
ИТОГО:0.00 ₽
+
+ +
+ + Отмена +
+
+
+ + +{% endblock %} diff --git a/templates/orders.html b/templates/orders.html new file mode 100644 index 0000000..db16d1c --- /dev/null +++ b/templates/orders.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} +{% block title %}Заявки — Стройка{% endblock %} +{% block page_title %}Заявки на поставку{% endblock %} +{% block topbar_actions %} + + Новая заявка +{% endblock %} + +{% block content %} + + + + +
+ + + + + + + + + + + + + + + + + + {% for o in orders %} + + + + + + + + + + + + + + {% else %} + + {% endfor %} + +
№ДатаПартнёрМатериалКол-воСтоимость товара, ₽Транспорт, ₽Скидка, %Скидка, ₽ИТОГО, ₽Действия
{{ o.id }}{{ o.order_date }}{{ o.partner_name }}{{ o.material_name }}{{ o.quantity }} {{ o.unit }}{{ "%.2f"|format(o.goods) }}{{ "%.2f"|format(o.transport) }}{% if o.discount > 0 %}{{ o.discount }}%{% else %}—{% endif %}{% if o.discount_sum > 0 %}−{{ "%.2f"|format(o.discount_sum) }}{% else %}—{% endif %}{{ "%.2f"|format(o.total) }}✏️ Изменить
Заявок пока нет
+
+
Всего заявок: {{ orders|length }}
+ +{% endblock %} diff --git a/templates/orders_list.html b/templates/orders_list.html new file mode 100644 index 0000000..c2cc44c --- /dev/null +++ b/templates/orders_list.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} +{% block title %}Заказы — Стройка{% endblock %} +{% block page_title %}Заказы{% endblock %} +{% block topbar_actions %}+ Новый заказ{% endblock %} +{% block content %} + + +
+
+ + +
+
+ + {% if search %} + Сброс + {% endif %} +
+ +
+ + + +
+ + + + + + + + + + + + + + + + {% for o in orders %} + + + + + + + + + + + + {% else %} + + {% endfor %} + +
НомерДатаДата доставкиКонтрагентТипСтатусПозицийИТОГО, ₽Действия
{{ o.number }}{{ o.order_date }}{{ o.delivery_date or '—' }}{{ o.counterparty_name }}{% if o.counterparty_type==2 %}Приходная{% else %}Расходная{% endif %}{{ o.status }}{{ o.items_count }}{{ "%.2f"|format(o.total) }}✏️ Изменить
Заказов пока нет
+
+
+ Всего: {{ orders|length }} заказов + {% if search %} + 🔍 Результаты поиска: «{{ search }}» + {% endif %} +
+{% endblock %} diff --git a/templates/partner_form.html b/templates/partner_form.html new file mode 100644 index 0000000..b07b996 --- /dev/null +++ b/templates/partner_form.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block title %}{% if action=='add' %}Новый партнёр{% else %}Редактировать партнёра{% endif %} — Стройка{% endblock %} +{% block page_title %}{% if action=='add' %}Новый партнёр{% else %}Редактировать: {{ partner.name }}{% endif %}{% endblock %} +{% block topbar_actions %}← Назад{% endblock %} +{% block content %} +
+
+
+
+
+
+
+
+
+ + Отмена +
+
+
+{% endblock %} diff --git a/templates/partners.html b/templates/partners.html new file mode 100644 index 0000000..6589f6e --- /dev/null +++ b/templates/partners.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block title %}Партнёры — Стройка{% endblock %} +{% block page_title %}Партнёры{% endblock %} +{% block topbar_actions %}+ Добавить партнёра{% endblock %} +{% block content %} +
+ + + + {% for p in partners %} + + + + + + + + + + {% else %} + + {% endfor %} + +
#НазваниеТелефонEmailАдресНакладныхДействия
{{ p.id }}{{ p.name }}{{ p.phone or '—' }}{{ p.email or '—' }}{{ p.address or '—' }}{{ p.order_count }}✏️ Изменить
Партнёров нет
+
+{% endblock %} diff --git a/templates/supplier_form.html b/templates/supplier_form.html new file mode 100644 index 0000000..97b5e52 --- /dev/null +++ b/templates/supplier_form.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}{% if action=='add' %}Новый поставщик{% else %}Редактировать поставщика{% endif %} — Стройка{% endblock %} +{% block page_title %}{% if action=='add' %}Новый поставщик{% else %}Редактировать: {{ supplier.name }}{% endif %}{% endblock %} +{% block topbar_actions %}← Назад{% endblock %} +{% block content %} +
+
+
+
+ + +
+
+
+
+
+
+
+
+ + Отмена +
+
+
+{% endblock %} diff --git a/templates/suppliers.html b/templates/suppliers.html new file mode 100644 index 0000000..5b52c59 --- /dev/null +++ b/templates/suppliers.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block title %}Поставщики — Стройка{% endblock %} +{% block page_title %}Поставщики{% endblock %} +{% block topbar_actions %}+ Добавить поставщика{% endblock %} +{% block content %} +
+ + + + {% for s in suppliers %} + + + + + + + + + + + + {% else %} + + {% endfor %} + +
#НазваниеТелефонEmailАдресРасст., кмТариф р/кг/кмНакладныхДействия
{{ s.id }}{{ s.name }}{{ s.phone or '—' }}{{ s.email or '—' }}{{ s.address or '—' }}{{ s.distance }}{{ s.transport_rate }}{{ s.order_count }}✏️ Изменить
Поставщиков нет
+
+{% endblock %}