437 lines
20 KiB
Python
437 lines
20 KiB
Python
import sqlite3
|
||
import pandas as pd
|
||
import csv
|
||
from datetime import datetime
|
||
import sys
|
||
|
||
|
||
class FurnitureDatabase:
|
||
"""Класс для работы с базой данных"""
|
||
|
||
def __init__(self, db_name='furniture_company.db'):
|
||
self.db_name = db_name
|
||
self.conn = None
|
||
self.cursor = None
|
||
|
||
def connect(self):
|
||
"""Подключение к базе данных SQLite"""
|
||
try:
|
||
self.conn = sqlite3.connect(self.db_name)
|
||
self.cursor = self.conn.cursor()
|
||
print("✅ База данных подключена")
|
||
return True
|
||
except Exception as e:
|
||
print(f"❌ Не удалось подключиться к базе данных: {e}")
|
||
return False
|
||
|
||
def create_tables(self):
|
||
"""Создание таблиц в базе данных"""
|
||
try:
|
||
# Таблица типов материалов
|
||
self.cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS material_types (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(100) NOT NULL UNIQUE,
|
||
waste_percentage DECIMAL(5,4) NOT NULL
|
||
)
|
||
''')
|
||
|
||
# Таблица материалов
|
||
self.cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS materials (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(255) NOT NULL UNIQUE,
|
||
type_id INTEGER NOT NULL REFERENCES material_types(id),
|
||
price DECIMAL(15,2) NOT NULL,
|
||
quantity_in_stock DECIMAL(10,2) NOT NULL,
|
||
min_quantity DECIMAL(10,2) NOT NULL,
|
||
quantity_per_package DECIMAL(10,2) NOT NULL,
|
||
unit VARCHAR(50) NOT NULL
|
||
)
|
||
''')
|
||
|
||
# Таблица типов продукции
|
||
self.cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS product_types (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(100) NOT NULL UNIQUE,
|
||
coefficient DECIMAL(5,2) NOT NULL
|
||
)
|
||
''')
|
||
|
||
# Таблица продукции
|
||
self.cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS products (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
article BIGINT NOT NULL UNIQUE,
|
||
name VARCHAR(255) NOT NULL,
|
||
type_id INTEGER NOT NULL REFERENCES product_types(id),
|
||
min_partner_price DECIMAL(15,2) NOT NULL
|
||
)
|
||
''')
|
||
|
||
# Таблица расхода материалов
|
||
self.cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS material_consumption (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
material_id INTEGER NOT NULL REFERENCES materials(id),
|
||
product_id INTEGER NOT NULL REFERENCES products(id),
|
||
quantity_required DECIMAL(10,2) NOT NULL,
|
||
UNIQUE (material_id, product_id)
|
||
)
|
||
''')
|
||
|
||
self.conn.commit()
|
||
print("✅ Таблицы созданы")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"❌ Не удалось создать таблицы: {e}")
|
||
return False
|
||
|
||
def import_initial_data(self):
|
||
"""Импорт начальных данных"""
|
||
try:
|
||
# Импорт типов материалов
|
||
material_types = [
|
||
('Дерево', 0.0055), ('Древесная плита', 0.003), ('Текстиль', 0.0015),
|
||
('Стекло', 0.0045), ('Металл', 0.001), ('Пластик', 0.0005)
|
||
]
|
||
self.cursor.executemany('INSERT OR IGNORE INTO material_types (name, waste_percentage) VALUES (?, ?)',
|
||
material_types)
|
||
|
||
# Импорт типов продукции
|
||
product_types = [
|
||
('Кресла', 1.95), ('Полки', 2.5), ('Стеллажи', 4.35),
|
||
('Столы', 5.5), ('Тумбы', 7.6), ('Шкафы', 6.72)
|
||
]
|
||
self.cursor.executemany('INSERT OR IGNORE INTO product_types (name, coefficient) VALUES (?, ?)',
|
||
product_types)
|
||
|
||
# Импорт материалов
|
||
materials_data = [
|
||
('Цельный массив дерева Дуб 1000х600 мм', 1, 7450.00, 1500.00, 500.00, 7.20, 'м²'),
|
||
('Клееный массив дерева Дуб 1000х600 мм', 1, 4520.00, 300.00, 500.00, 7.20, 'м²'),
|
||
('Шпон облицовочный Дуб натуральный 2750х480 мм', 1, 2500.00, 2000.00, 1500.00, 19.80, 'м²'),
|
||
('Фанера 2200х1000 мм', 2, 2245.00, 450.00, 1000.00, 52.80, 'м²'),
|
||
('ДСП 2750х1830 мм', 2, 529.60, 1010.00, 1200.00, 181.08, 'м²'),
|
||
('МДФ 2070х1400 мм', 2, 417.24, 1550.00, 1000.00, 87.00, 'м²'),
|
||
('ДВП 2440х2050 мм', 2, 187.00, 1310.00, 1000.00, 190.00, 'м²'),
|
||
('ХДФ 2800x2070 мм', 2, 370.96, 1400.00, 1200.00, 90.00, 'м²'),
|
||
('Экокожа 140 см', 3, 1600.00, 1200.00, 1500.00, 100.00, 'пог.м'),
|
||
('Велюр 140 см', 3, 1300.00, 1300.00, 1500.00, 100.00, 'пог.м'),
|
||
('Шенилл 140 см', 3, 620.00, 950.00, 1500.00, 100.00, 'пог.м'),
|
||
('Рогожка 140 см', 3, 720.00, 960.00, 1500.00, 100.00, 'пог.м'),
|
||
('Закаленное стекло 2600х1800 мм', 4, 1148.00, 1000.00, 500.00, 196.56, 'м²'),
|
||
('Металлокаркас для стула', 5, 1100.00, 300.00, 250.00, 5.00, 'шт'),
|
||
('Металлокаркас каркас из профиля с траверсами для стола', 5, 6700.00, 100.00, 100.00, 1.00, 'шт'),
|
||
('Колесо для мебели поворотное', 6, 10.59, 1500.00, 1000.00, 500.00, 'шт'),
|
||
('Газ-лифт', 5, 730.00, 500.00, 250.00, 5.00, 'шт'),
|
||
('Металлическая крестовина для офисных кресел', 5, 2700.00, 500.00, 250.00, 5.00, 'шт'),
|
||
('Пластиковый комплект для стула', 6, 900.00, 300.00, 250.00, 100.00, 'шт'),
|
||
('Кромка ПВХ', 6, 35.80, 1500.00, 1000.00, 100.00, 'м')
|
||
]
|
||
self.cursor.executemany(
|
||
'INSERT OR IGNORE INTO materials (name, type_id, price, quantity_in_stock, min_quantity, quantity_per_package, unit) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||
materials_data
|
||
)
|
||
|
||
# Импорт продукции
|
||
products_data = [
|
||
(3028272, 'Кресло детское цвет Белый и Розовый', 1, 15324.99),
|
||
(3018556, 'Кресло офисное цвет Черный', 1, 21443.99),
|
||
(3549922, 'Кресло эргономичное цвет Темно-коричневый', 1, 24760.00),
|
||
(7028048, 'Полка настольная', 2, 2670.89),
|
||
(5759324, 'Стеллаж для документов цвет Дуб светлый 854х445х2105 мм', 3, 24899.00),
|
||
(5259474, 'Стеллаж цвет Белый с ящиками 854х445х2105 мм', 3, 16150.00),
|
||
(5118827, 'Стеллаж цвет Орех 400х370х2000 мм', 3, 2860.00),
|
||
(1029784, 'Стол для ноутбука на металлокаркасе 800х600х123 мм', 4, 14690.00),
|
||
(1028248, 'Стол компьютерный 700х600х500 мм', 4, 4105.89),
|
||
(1130981, 'Стол компьютерный на металлокаркасе 1400х600х750 мм', 4, 13899.00),
|
||
(1188827, 'Стол письменный 1100x750x600 мм', 4, 5148.00),
|
||
(1029272, 'Стол письменный с тумбочкой 4 ящика 1100x750x600 мм', 4, 15325.00),
|
||
(1016662, 'Стол руководителя письменный цвет Венге 1600x800x764 мм', 4, 43500.90),
|
||
(1658953, 'Стол руководителя письменный цвет Орех темный 2300x1000x750 мм', 4, 132500.00),
|
||
(6033136, 'Тумба выкатная 3 ящика', 5, 3750.00),
|
||
(6115947, 'Тумба офисная для оргтехники', 5, 2450.00),
|
||
(5559898, 'Узкий пенал стеллаж 5 полок цвет Орех 364х326x2000 мм', 3, 8348.00),
|
||
(4159043, 'Шкаф для книг 800x420x2000 мм', 6, 23390.99),
|
||
(4758375, 'Шкаф для одежды цвет Венге 602x420x2000 мм', 6, 12035.00),
|
||
(4588376, 'Шкаф-витрина 2 ящика 800x420x2000 мм', 6, 31991.00)
|
||
]
|
||
self.cursor.executemany(
|
||
'INSERT OR IGNORE INTO products (article, name, type_id, min_partner_price) VALUES (?, ?, ?, ?)',
|
||
products_data
|
||
)
|
||
|
||
# Импорт расхода материалов
|
||
consumption_data = [
|
||
(4, 1, 0.85), (8, 1, 1.5), (10, 1, 1.5), (11, 1, 1.5), (13, 1, 1),
|
||
(15, 1, 5), (16, 1, 1), (17, 1, 1), (18, 1, 1),
|
||
(4, 2, 1.25), (9, 2, 3.04), (10, 2, 1.5), (11, 2, 2.5), (13, 2, 1),
|
||
(15, 2, 5), (16, 2, 1), (17, 2, 1), (18, 2, 1),
|
||
(4, 3, 1.85), (9, 3, 4.22), (8, 3, 1.5), (13, 3, 1), (15, 3, 5),
|
||
(16, 3, 1), (17, 3, 1), (18, 3, 1),
|
||
(5, 4, 3.33), (19, 4, 6),
|
||
(2, 5, 2.9), (3, 5, 1.7), (6, 5, 2.7), (7, 5, 1.8),
|
||
]
|
||
self.cursor.executemany(
|
||
'INSERT OR IGNORE INTO material_consumption (material_id, product_id, quantity_required) VALUES (?, ?, ?)',
|
||
consumption_data
|
||
)
|
||
|
||
self.conn.commit()
|
||
print("✅ Данные импортированы")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"❌ Не удалось импортировать данные: {e}")
|
||
return False
|
||
|
||
def get_materials_with_required_quantity(self):
|
||
"""Получение списка материалов с расчетом требуемого количества"""
|
||
try:
|
||
query = """
|
||
SELECT m.id, m.name, mt.name AS type_name, m.price, m.quantity_in_stock,
|
||
m.min_quantity, m.unit, COALESCE(SUM(mc.quantity_required), 0) AS total_required
|
||
FROM materials m
|
||
JOIN material_types mt ON m.type_id = mt.id
|
||
LEFT JOIN material_consumption mc ON m.id = mc.material_id
|
||
GROUP BY m.id, m.name, mt.name, m.price, m.quantity_in_stock, m.min_quantity, m.unit
|
||
ORDER BY m.name
|
||
"""
|
||
self.cursor.execute(query)
|
||
return self.cursor.fetchall()
|
||
except Exception as e:
|
||
print(f"❌ Не удалось загрузить данные материалов: {e}")
|
||
return []
|
||
|
||
def get_material_statistics(self):
|
||
"""Получение статистики по материалам"""
|
||
try:
|
||
query = """
|
||
SELECT
|
||
COUNT(*) as total_materials,
|
||
SUM(quantity_in_stock) as total_quantity,
|
||
SUM(price * quantity_in_stock) as total_value,
|
||
COUNT(CASE WHEN quantity_in_stock < min_quantity THEN 1 END) as low_stock_count
|
||
FROM materials
|
||
"""
|
||
self.cursor.execute(query)
|
||
return self.cursor.fetchone()
|
||
except Exception as e:
|
||
print(f"❌ Не удалось получить статистику: {e}")
|
||
return None
|
||
|
||
def get_low_stock_materials(self):
|
||
"""Получение материалов с низким запасом"""
|
||
try:
|
||
query = """
|
||
SELECT m.name, m.quantity_in_stock, m.min_quantity, m.unit, mt.name as type_name
|
||
FROM materials m
|
||
JOIN material_types mt ON m.type_id = mt.id
|
||
WHERE m.quantity_in_stock < m.min_quantity
|
||
ORDER BY m.quantity_in_stock / m.min_quantity
|
||
"""
|
||
self.cursor.execute(query)
|
||
return self.cursor.fetchall()
|
||
except Exception as e:
|
||
print(f"❌ Не удалось получить материалы с низким запасом: {e}")
|
||
return []
|
||
|
||
def close(self):
|
||
"""Закрытие соединения с базой данных"""
|
||
if self.conn:
|
||
self.conn.close()
|
||
print("✅ Соединение с базой данных закрыто")
|
||
|
||
|
||
def print_materials_table(materials):
|
||
"""Красиво выводит таблицу материалов в консоль"""
|
||
if not materials:
|
||
print("❌ Нет данных о материалах")
|
||
return
|
||
|
||
print("\n" + "=" * 120)
|
||
print(
|
||
f"{'Наименование':<30} {'Тип':<15} {'Цена':<10} {'На складе':<10} {'Мин.запас':<10} {'Ед.изм.':<8} {'Требуется':<10}")
|
||
print("=" * 120)
|
||
|
||
low_stock_count = 0
|
||
critical_stock_count = 0
|
||
|
||
for material in materials:
|
||
material_id, name, type_name, price, stock, min_stock, unit, required = material
|
||
|
||
# Цветовая индикация
|
||
if stock < min_stock * 0.5:
|
||
indicator = "🔴" # Критический запас
|
||
critical_stock_count += 1
|
||
elif stock < min_stock:
|
||
indicator = "🟡" # Низкий запас
|
||
low_stock_count += 1
|
||
else:
|
||
indicator = "🟢" # Нормальный запас
|
||
|
||
print(
|
||
f"{indicator} {name:<28} {type_name:<13} {price:<9.2f} {stock:<9.2f} {min_stock:<9.2f} {unit:<6} {required:<9.2f}")
|
||
|
||
print("=" * 120)
|
||
print(
|
||
f"Всего материалов: {len(materials)} | 🟡 Низкий запас: {low_stock_count} | 🔴 Критический запас: {critical_stock_count}")
|
||
|
||
|
||
def print_statistics(stats):
|
||
"""Выводит статистику"""
|
||
if not stats:
|
||
print("❌ Не удалось получить статистику")
|
||
return
|
||
|
||
total_materials, total_quantity, total_value, low_stock_count = stats
|
||
|
||
print("\n" + "=" * 50)
|
||
print("📊 СТАТИСТИКА СКЛАДА")
|
||
print("=" * 50)
|
||
print(f"📦 Всего позиций: {total_materials}")
|
||
print(f"📐 Общее количество: {total_quantity:.2f}")
|
||
print(f"💰 Общая стоимость: {total_value:,.2f} руб.")
|
||
print(f"⚠️ Материалов с низким запасом: {low_stock_count}")
|
||
print("=" * 50)
|
||
|
||
|
||
def print_low_stock_materials(materials):
|
||
"""Выводит материалы с низким запасом"""
|
||
if not materials:
|
||
print("✅ Все материалы в норме! Нет позиций с низким запасом.")
|
||
return
|
||
|
||
print("\n" + "=" * 80)
|
||
print("⚠️ МАТЕРИАЛЫ С НИЗКИМ ЗАПАСОМ")
|
||
print("=" * 80)
|
||
|
||
for material in materials:
|
||
name, stock, min_stock, unit, type_name = material
|
||
percentage = (stock / min_stock * 100)
|
||
|
||
if percentage < 50:
|
||
indicator = "🔴"
|
||
status = "КРИТИЧЕСКИЙ"
|
||
else:
|
||
indicator = "🟡"
|
||
status = "НИЗКИЙ"
|
||
|
||
print(
|
||
f"{indicator} {name:<40} {stock:>6.1f} {unit:<4} (мин. {min_stock:>6.1f}) - {percentage:>5.1f}% - {status}")
|
||
|
||
|
||
def export_to_csv(materials, filename=None):
|
||
"""Экспорт данных в CSV"""
|
||
if not filename:
|
||
filename = f"material_stock_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||
|
||
try:
|
||
with open(filename, 'w', encoding='utf-8') as f:
|
||
# Заголовки
|
||
f.write("Наименование;Тип;Цена;На складе;Мин.запас;Ед.изм.;Требуется\n")
|
||
|
||
# Данные
|
||
for material in materials:
|
||
_, name, type_name, price, stock, min_stock, unit, required = material
|
||
f.write(f"{name};{type_name};{price:.2f};{stock:.2f};{min_stock:.2f};{unit};{required:.2f}\n")
|
||
|
||
print(f"✅ Данные экспортированы в файл: {filename}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"❌ Ошибка при экспорте: {e}")
|
||
return False
|
||
|
||
|
||
def safe_input(prompt):
|
||
"""Безопасный ввод с обработкой прерывания"""
|
||
try:
|
||
return input(prompt).strip()
|
||
except (KeyboardInterrupt, EOFError):
|
||
print("\n\n👋 Программа прервана пользователем. До свидания!")
|
||
sys.exit(0)
|
||
|
||
|
||
def main():
|
||
"""Главная функция приложения"""
|
||
print("🏭 СИСТЕМА УПРАВЛЕНИЯ СКЛАДОМ МАТЕРИАЛОВ")
|
||
print("Компания 'Образ плюс' - производство офисной мебели")
|
||
print("=" * 60)
|
||
|
||
# Инициализация базы данных
|
||
db = FurnitureDatabase()
|
||
if not db.connect():
|
||
return
|
||
|
||
if not db.create_tables():
|
||
return
|
||
|
||
# Проверяем, есть ли данные в базе
|
||
db.cursor.execute("SELECT COUNT(*) FROM materials")
|
||
if db.cursor.fetchone()[0] == 0:
|
||
print("📥 Импорт начальных данных...")
|
||
db.import_initial_data()
|
||
|
||
while True:
|
||
print("\n" + "=" * 40)
|
||
print("📋 ГЛАВНОЕ МЕНЮ")
|
||
print("=" * 40)
|
||
print("1. 📊 Показать все материалы")
|
||
print("2. ⚠️ Показать материалы с низким запасом")
|
||
print("3. 📈 Показать статистику")
|
||
print("4. 📥 Экспорт в CSV")
|
||
print("5. ❌ Выход")
|
||
print("=" * 40)
|
||
|
||
choice = safe_input("\nВыберите действие (1-5): ")
|
||
|
||
if choice == '1':
|
||
print("\n🔄 Загрузка данных...")
|
||
materials = db.get_materials_with_required_quantity()
|
||
print_materials_table(materials)
|
||
|
||
elif choice == '2':
|
||
print("\n🔄 Поиск материалов с низким запасом...")
|
||
low_stock_materials = db.get_low_stock_materials()
|
||
print_low_stock_materials(low_stock_materials)
|
||
|
||
elif choice == '3':
|
||
print("\n🔄 Расчет статистики...")
|
||
stats = db.get_material_statistics()
|
||
print_statistics(stats)
|
||
|
||
elif choice == '4':
|
||
print("\n🔄 Подготовка данных для экспорта...")
|
||
materials = db.get_materials_with_required_quantity()
|
||
if materials:
|
||
filename = safe_input("Введите имя файла (или нажмите Enter для автоимени): ")
|
||
if not filename:
|
||
filename = None
|
||
export_to_csv(materials, filename)
|
||
else:
|
||
print("❌ Нет данных для экспорта")
|
||
|
||
elif choice == '5':
|
||
print("\n👋 До свидания! Спасибо за использование системы.")
|
||
break
|
||
|
||
else:
|
||
print("❌ Неверный выбор. Пожалуйста, выберите от 1 до 5.")
|
||
|
||
# Пауза перед следующим шагом
|
||
if choice != '5':
|
||
safe_input("\nНажмите Enter чтобы продолжить...")
|
||
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except Exception as e:
|
||
print(f"\n❌ Произошла непредвиденная ошибка: {e}")
|
||
print("Пожалуйста, перезапустите программу.") |