commit 211f5a942bad1240d964feff3b9eb3417cf64b1d Author: kosarewmihail Date: Fri Feb 20 12:52:28 2026 +0500 аптечный склад diff --git a/Resource/categories_import.xlsx b/Resource/categories_import.xlsx new file mode 100644 index 0000000..0190baa Binary files /dev/null and b/Resource/categories_import.xlsx differ diff --git a/Resource/manufacturers_import.xlsx b/Resource/manufacturers_import.xlsx new file mode 100644 index 0000000..0788341 Binary files /dev/null and b/Resource/manufacturers_import.xlsx differ diff --git a/Resource/masterpol.png b/Resource/masterpol.png new file mode 100644 index 0000000..c192a72 Binary files /dev/null and b/Resource/masterpol.png differ diff --git a/Resource/medicines_import.xlsx b/Resource/medicines_import.xlsx new file mode 100644 index 0000000..c45961e Binary files /dev/null and b/Resource/medicines_import.xlsx differ diff --git a/Resource/Мастер пол.ico b/Resource/Мастер пол.ico new file mode 100644 index 0000000..9744b0a Binary files /dev/null and b/Resource/Мастер пол.ico differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..c8cf065 --- /dev/null +++ b/app.py @@ -0,0 +1,196 @@ +from flask import Flask, render_template, request, redirect, url_for, flash +import sqlite3 +from datetime import datetime, timedelta + +app = Flask(__name__) +app.secret_key = 'apteka_secret' + +def get_db(): + conn = sqlite3.connect('pharmacy.db') + conn.row_factory = sqlite3.Row + return conn + +def calculate_order_price(medicine_id, quantity): + """Расчет стоимости заказа с учетом срока годности и сезона""" + conn = get_db() + medicine = conn.execute(''' + SELECT price, expiration_date, season_coeff FROM medicines WHERE id = ? + ''', (medicine_id,)).fetchone() + conn.close() + + if not medicine: + return 0 + + base = medicine['price'] * quantity + + date_str = medicine['expiration_date'][:10] + exp_date = datetime.strptime(date_str, '%Y-%m-%d').date() + days_left = (exp_date - datetime.now().date()).days + + if days_left < 0: + exp_factor = 0.1 + elif days_left < 30: + exp_factor = 0.7 + elif days_left < 90: + exp_factor = 0.9 + else: + exp_factor = 1.0 + + season_factor = medicine['season_coeff'] + + return round(base * exp_factor * season_factor, 2) + +# ========== ГЛАВНАЯ ========== +@app.route('/') +def index(): + conn = get_db() + medicines = conn.execute(''' + SELECT m.*, c.name as category_name, man.name as manufacturer_name + FROM medicines m + LEFT JOIN categories c ON m.category_id = c.id + LEFT JOIN manufacturers man ON m.manufacturer_id = man.id + ORDER BY m.name + ''').fetchall() + conn.close() + return render_template('index.html', medicines=medicines, calculate=calculate_order_price) + +# ========== ПРОИЗВОДИТЕЛИ ========== +@app.route('/manufacturers') +def manufacturers(): + conn = get_db() + mans = conn.execute('SELECT * FROM manufacturers ORDER BY name').fetchall() + conn.close() + return render_template('manufacturers.html', manufacturers=mans) + +@app.route('/add_manufacturer', methods=['GET', 'POST']) +def add_manufacturer(): + if request.method == 'POST': + name = request.form['name'] + country = request.form.get('country', '') + phone = request.form.get('phone', '') + email = request.form.get('email', '') + + conn = get_db() + conn.execute(''' + INSERT INTO manufacturers (name, country, phone, email) + VALUES (?, ?, ?, ?) + ''', (name, country, phone, email)) + conn.commit() + conn.close() + flash('Производитель добавлен!', 'success') + return redirect(url_for('manufacturers')) + + return render_template('add_manufacturer.html') + +@app.route('/edit_manufacturer/', methods=['GET', 'POST']) +def edit_manufacturer(id): + conn = get_db() + if request.method == 'POST': + name = request.form['name'] + country = request.form.get('country', '') + phone = request.form.get('phone', '') + email = request.form.get('email', '') + + conn.execute(''' + UPDATE manufacturers + SET name=?, country=?, phone=?, email=? + WHERE id=? + ''', (name, country, phone, email, id)) + conn.commit() + conn.close() + flash('Производитель обновлен!', 'success') + return redirect(url_for('manufacturers')) + + man = conn.execute('SELECT * FROM manufacturers WHERE id = ?', (id,)).fetchone() + conn.close() + return render_template('edit_manufacturer.html', manufacturer=man) + +@app.route('/delete_manufacturer/', methods=['POST']) +def delete_manufacturer(id): + conn = get_db() + conn.execute('DELETE FROM manufacturers WHERE id = ?', (id,)) + conn.commit() + conn.close() + flash('Производитель удален!', 'success') + return redirect(url_for('manufacturers')) + +# ========== КАТЕГОРИИ ========== +@app.route('/categories') +def categories(): + conn = get_db() + cats = conn.execute('SELECT * FROM categories ORDER BY name').fetchall() + conn.close() + return render_template('categories.html', categories=cats) + +@app.route('/add_category', methods=['GET', 'POST']) +def add_category(): + if request.method == 'POST': + name = request.form['name'] + description = request.form.get('description', '') + + conn = get_db() + conn.execute('INSERT INTO categories (name, description) VALUES (?, ?)', + (name, description)) + conn.commit() + conn.close() + flash('Категория добавлена!', 'success') + return redirect(url_for('categories')) + + return render_template('add_category.html') + +@app.route('/edit_category/', methods=['GET', 'POST']) +def edit_category(id): + conn = get_db() + if request.method == 'POST': + name = request.form['name'] + description = request.form.get('description', '') + + conn.execute('UPDATE categories SET name=?, description=? WHERE id=?', + (name, description, id)) + conn.commit() + conn.close() + flash('Категория обновлена!', 'success') + return redirect(url_for('categories')) + + category = conn.execute('SELECT * FROM categories WHERE id = ?', (id,)).fetchone() + conn.close() + return render_template('edit_category.html', category=category) + +@app.route('/delete_category/', methods=['POST']) +def delete_category(id): + conn = get_db() + conn.execute('DELETE FROM categories WHERE id = ?', (id,)) + conn.commit() + conn.close() + flash('Категория удалена!', 'success') + return redirect(url_for('categories')) + +# ========== ЛЕКАРСТВА ========== +@app.route('/add_medicine', methods=['GET', 'POST']) +def add_medicine(): + conn = get_db() + if request.method == 'POST': + name = request.form['name'] + category_id = request.form['category_id'] + manufacturer_id = request.form['manufacturer_id'] + price = request.form['price'] + expiration_date = request.form['expiration_date'] + stock_quantity = request.form['stock_quantity'] + season_coeff = request.form.get('season_coeff', 1.0) + + conn.execute(''' + INSERT INTO medicines (name, category_id, manufacturer_id, price, expiration_date, stock_quantity, season_coeff) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (name, category_id, manufacturer_id, price, expiration_date, stock_quantity, season_coeff)) + conn.commit() + conn.close() + flash('Лекарство добавлено!', 'success') + return redirect(url_for('index')) + + categories = conn.execute('SELECT * FROM categories').fetchall() + manufacturers = conn.execute('SELECT * FROM manufacturers').fetchall() + conn.close() + return render_template('add_medicine.html', categories=categories, manufacturers=manufacturers) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/create_tables.py b/create_tables.py new file mode 100644 index 0000000..3317662 --- /dev/null +++ b/create_tables.py @@ -0,0 +1,56 @@ +import sqlite3 + +conn = sqlite3.connect('pharmacy.db') +cursor = conn.cursor() + +# Категории лекарств +cursor.execute(''' +CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT +) +''') + +# Производители +cursor.execute(''' +CREATE TABLE IF NOT EXISTS manufacturers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + country TEXT, + phone TEXT, + email TEXT UNIQUE +) +''') + +# Лекарства +cursor.execute(''' +CREATE TABLE IF NOT EXISTS medicines ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + category_id INTEGER, + manufacturer_id INTEGER, + price REAL NOT NULL, + expiration_date DATE NOT NULL, + stock_quantity INTEGER DEFAULT 0, + season_coeff REAL DEFAULT 1.0, + FOREIGN KEY (category_id) REFERENCES categories(id), + FOREIGN KEY (manufacturer_id) REFERENCES manufacturers(id) +) +''') + +# Заказы (для истории продаж) +cursor.execute(''' +CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + medicine_id INTEGER, + quantity INTEGER NOT NULL, + order_date DATE NOT NULL, + total_price REAL, + FOREIGN KEY (medicine_id) REFERENCES medicines(id) +) +''') + +print("✅ Таблицы созданы") +conn.commit() +conn.close() \ No newline at end of file diff --git a/load_data.py b/load_data.py new file mode 100644 index 0000000..cfb2d6a --- /dev/null +++ b/load_data.py @@ -0,0 +1,39 @@ +import sqlite3 +import pandas as pd + +conn = sqlite3.connect('pharmacy.db') +cursor = conn.cursor() + +# Загрузка категорий +print("📦 Загрузка категорий...") +df = pd.read_excel('Resource/categories_import.xlsx') +for _, row in df.iterrows(): + cursor.execute(''' + INSERT OR IGNORE INTO categories (id, name, description) + VALUES (?, ?, ?) + ''', (row['ID'], row['CategoryName'], row['Description'])) + +# Загрузка производителей +print("🏭 Загрузка производителей...") +df = pd.read_excel('Resource/manufacturers_import.xlsx') +for _, row in df.iterrows(): + cursor.execute(''' + INSERT OR IGNORE INTO manufacturers (id, name, country, phone, email) + VALUES (?, ?, ?, ?, ?) + ''', (row['ID'], row['ManufacturerName'], row['Country'], row['Phone'], row['Email'])) + +# Загрузка лекарств +print("💊 Загрузка лекарств...") +df = pd.read_excel('Resource/medicines_import.xlsx') +for _, row in df.iterrows(): + cursor.execute(''' + INSERT OR IGNORE INTO medicines + (id, name, category_id, manufacturer_id, price, expiration_date, stock_quantity, season_coeff) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', (row['ID'], row['MedicineName'], row['CategoryID'], row['ManufacturerID'], + row['Price'], row['ExpirationDate'], row['StockQty'], row['SeasonCoeff'])) + +conn.commit() +conn.close() +print("✅ Данные загружены!") +input("Нажми Enter для выхода...") \ No newline at end of file diff --git a/pharmacy.db b/pharmacy.db new file mode 100644 index 0000000..0005db9 Binary files /dev/null and b/pharmacy.db differ diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..6445077 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,319 @@ +/* Подключаем шрифт Segoe UI */ +@import url('https://fonts.googleapis.com/css2?family=Segoe+UI:wght@300;400;600;700&display=swap'); + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #F4E8D3; + margin: 0; + padding: 0; +} + +.main-container { + background-color: #ffffff; + min-height: 100vh; + padding: 20px; +} + +.header { + background: linear-gradient(135deg, #67BA80 0%, #4CAF50 100%); + padding: 20px; + border-radius: 10px; + margin-bottom: 30px; + color: white; +} + +.logo-container { + display: flex; + align-items: center; + gap: 20px; +} + +.logo { + height: 60px; + width: auto; + object-fit: contain; +} + +.header-title { + font-size: 2.2em; + font-weight: 700; + margin: 0; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +.page-title { + color: #2c3e50; + font-weight: 600; +} + +.partner-card { + border: 2px solid #67BA80; + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; + background: #ffffff; + box-shadow: 0 4px 12px rgba(103, 186, 128, 0.15); +} + +.partner-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 2px solid #e9ecef; + padding-bottom: 15px; + margin-bottom: 15px; +} + +.partner-name-line { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.partner-type { + font-size: 1.3em; + font-weight: 600; + color: #6c757d; + background: #f8f9fa; + padding: 6px 14px; + border-radius: 20px; +} + +.partner-name { + font-size: 1.4em; + font-weight: 700; + color: #2c3e50; + margin: 0; +} + +.divider { + color: #dee2e6; + font-weight: bold; + font-size: 1.4em; +} + +.discount-info { + display: flex; + align-items: center; + gap: 15px; +} + +.discount-badge { + font-size: 1.1em; + padding: 8px 16px; + border-radius: 25px; + background-color: #67BA80; + color: white; +} + +.sales-amount { + font-size: 1em; + color: #6c757d; + font-weight: 500; +} + +.partner-info-row { + margin-bottom: 0; + padding: 6px 0; + line-height: 1.4; +} + +.partner-label { + font-weight: 600; + color: #495057; + min-width: 100px; + display: inline-block; + font-size: 1em; +} + +.partner-value { + font-size: 1em; + color: #2c3e50; +} + +.partner-buttons-container { + display: flex; + gap: 12px; + justify-content: flex-start; + margin-top: 20px; + padding-top: 15px; + border-top: 2px solid #e9ecef; +} + +.btn-action { + padding: 10px 20px; + font-size: 0.95em; + display: flex; + align-items: center; + gap: 8px; + min-width: 180px; + justify-content: center; + border: none; + border-radius: 8px; + font-weight: 600; + transition: all 0.3s ease; +} + +.btn-edit { + background-color: #67BA80; + color: white; +} + +.btn-add { + background-color: #6c757d; + color: white; +} + +.btn-history { + background-color: #495057; + color: white; +} + +.btn-action:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + color: white; +} + +.btn-new-partner { + background-color: #67BA80; + color: white; + border: none; + padding: 12px 24px; + font-weight: 600; + border-radius: 8px; +} + +.btn-new-partner:hover { + background-color: #5aa870; + color: white; +} + +.rating-badge { + font-size: 0.9em; + padding: 6px 12px; + border-radius: 15px; +} + +.alert { + border-radius: 10px; + border: none; +} +.partner-info-row:last-child { + margin-bottom: 10px; +} + +.volume-info { + background-color: #f8f9fa; + padding: 8px 12px; + border-radius: 6px; + margin-top: 10px; +} + +.volume-label { + font-weight: 600; + color: #495057; +} + +.volume-value { + color: #2c3e50; + font-weight: 500; +} + +/* Стили для страницы истории продаж */ +/* Стили для таблицы продаж */ +.table-responsive { + border-radius: 8px; + overflow: hidden; +} + +.table { + margin-bottom: 0; +} + +.table th { + background-color: #67BA80; + color: white; + font-weight: 600; + padding: 12px; + text-align: center; +} + +.table td { + padding: 10px; + text-align: center; + vertical-align: middle; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(103, 186, 128, 0.05); +} + +.table-striped tbody tr:hover { + background-color: rgba(103, 186, 128, 0.1); +} + +.table tfoot { + background-color: #e8f5e8; + font-weight: 600; +} + +.table tfoot td { + border-top: 2px solid #67BA80; +} + +.text-end { + text-align: right !important; +} + +/* Адаптивность для таблицы */ +@media (max-width: 768px) { + .table-responsive { + font-size: 0.9em; + } + + .table th, .table td { + padding: 6px; + } +} +/* Кастомные кнопки под цветовую схему */ +.btn-custom-primary { + background-color: #67BA80; + color: white; + border: none; + padding: 10px 20px; + font-weight: 600; + border-radius: 8px; + transition: all 0.3s ease; +} +.btn-custom-primary:hover { + background-color: #5aa870; + color: white; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(103, 186, 128, 0.3); +} + +.btn-custom-edit { + background-color: #6c757d; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; +} +.btn-custom-edit:hover { + background-color: #5a6268; + color: white; +} + +/* Таблицы */ +.table th { + background-color: #67BA80; + color: white; + font-weight: 600; +} + +/* Бейджи популярности */ +.badge.bg-success { + background-color: #67BA80 !important; +} diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..0ed9d45 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,37 @@ +document.addEventListener('DOMContentLoaded', function() { + // Кнопка нового партнера + const newPartnerBtn = document.querySelector('.btn-new-partner'); + if (newPartnerBtn) { + newPartnerBtn.addEventListener('click', function() { + // Переход на страницу добавления партнера + window.location.href = '/add_partner'; + }); + } + + // Кнопка "Редактирование" + document.querySelectorAll('.btn-edit').forEach(btn => { + btn.addEventListener('click', function() { + const partnerId = this.getAttribute('data-partner-id'); + // Переход на страницу редактирования + window.location.href = `/edit_partner/${partnerId}`; + }); + }); + + // Кнопка "Добавление продажи" + document.querySelectorAll('.btn-add').forEach(btn => { + btn.addEventListener('click', function() { + const partnerId = this.getAttribute('data-partner-id'); + // Переход на страницу добавления продажи + window.location.href = `/add_sale/${partnerId}`; + }); + }); + + // Кнопка "История реализации" + document.querySelectorAll('.btn-history').forEach(btn => { + btn.addEventListener('click', function() { + const partnerId = this.getAttribute('data-partner-id'); + // Переход на страницу истории реализации + window.location.href = `/sales_history/${partnerId}`; + }); + }); +}); \ No newline at end of file diff --git a/static/masterpol.png b/static/masterpol.png new file mode 100644 index 0000000..96a5a71 Binary files /dev/null and b/static/masterpol.png differ diff --git a/templates/add_category.html b/templates/add_category.html new file mode 100644 index 0000000..a0df92e --- /dev/null +++ b/templates/add_category.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block content %} +

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

+ +
+
+ + +
+
+ + +
+ + Отмена +
+{% endblock %} \ No newline at end of file diff --git a/templates/add_manufacturer.html b/templates/add_manufacturer.html new file mode 100644 index 0000000..f357d21 --- /dev/null +++ b/templates/add_manufacturer.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block content %} +

Добавить производителя

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Отмена +
+{% endblock %} \ No newline at end of file diff --git a/templates/add_medicine.html b/templates/add_medicine.html new file mode 100644 index 0000000..2ab67c6 --- /dev/null +++ b/templates/add_medicine.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block content %} +

Добавить лекарство

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Отмена +
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..e57dc34 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,69 @@ + + + + + + {% block title %}Аптечный склад{% endblock %} + + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ +
+

Аптечный склад © 2026

+
+ + + + \ No newline at end of file diff --git a/templates/categories.html b/templates/categories.html new file mode 100644 index 0000000..852fa31 --- /dev/null +++ b/templates/categories.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block content %} +

Категории лекарств

+Добавить категорию + + + + + + + + + + + {% for c in categories %} + + + + + + {% endfor %} + +
НазваниеОписаниеДействия
{{ c.name }}{{ c.description or '-' }} + ✏️ +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/components/header.html b/templates/components/header.html new file mode 100644 index 0000000..f12d327 --- /dev/null +++ b/templates/components/header.html @@ -0,0 +1,6 @@ +
+
+ +

Управление партнерами

+
+
\ No newline at end of file diff --git a/templates/components/partner_card.html b/templates/components/partner_card.html new file mode 100644 index 0000000..2d8b837 --- /dev/null +++ b/templates/components/partner_card.html @@ -0,0 +1,73 @@ +
+
+ +
+
+ + {% if partner.partner_type_name %} + {{ partner.partner_type_name }} + {% else %} + Тип не указан + {% endif %} + + | + {{ partner.name }} +
+ +
+ + Скидка {{ partner.discount_percent }}% + + + {{ partner.total_quantity }} ед. + +
+
+ + +
+ Директор: + {{ partner.director or 'Не указан' }} +
+ +
+ Телефон: + + {% if partner.phone %} + {{ partner.phone }} + {% else %} + Не указан + {% endif %} + +
+ +
+ Рейтинг: + + + {{ partner.rating }} + + +
+ +
+ Объем продаж: + + {{ "%.2f"|format(partner.total_sales) }} руб. + +
+ + +
+ + + +
+
+
\ No newline at end of file diff --git a/templates/edit_category.html b/templates/edit_category.html new file mode 100644 index 0000000..db664f1 --- /dev/null +++ b/templates/edit_category.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block content %} +

Редактировать категорию

+ +
+
+ + +
+
+ + +
+ + Отмена +
+{% endblock %} \ No newline at end of file diff --git a/templates/edit_manufacturer.html b/templates/edit_manufacturer.html new file mode 100644 index 0000000..f36060e --- /dev/null +++ b/templates/edit_manufacturer.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block content %} +

Редактировать производителя

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Отмена +
+{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..054a960 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block content %} +

Список лекарств

+Добавить лекарство + + + + + + + + + + + + + + + {% for m in medicines %} + + + + + + + + + + {% endfor %} + +
НазваниеКатегорияПроизводительЦенаСрок годностиВ наличииТест заказа (10 шт)
{{ m.name }}{{ m.category_name }}{{ m.manufacturer_name }}{{ m.price }} ₽{{ m.expiration_date }}{{ m.stock_quantity }} + {% set total = calculate(m.id, 10) %} + {{ total }} ₽ + (с учетом скидок) +
+{% endblock %} \ No newline at end of file diff --git a/templates/manufacturers.html b/templates/manufacturers.html new file mode 100644 index 0000000..057f141 --- /dev/null +++ b/templates/manufacturers.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block content %} +

Производители

+Добавить производителя + + + + + + + + + + + + + {% for m in manufacturers %} + + + + + + + + {% endfor %} + +
НазваниеСтранаТелефонEmailДействия
{{ m.name }}{{ m.country or '-' }}{{ m.phone or '-' }}{{ m.email or '-' }} + ✏️ +
+ +
+
+{% endblock %} \ No newline at end of file