diff --git a/Categories_import.xlsx b/Categories_import.xlsx new file mode 100644 index 0000000..317a00e Binary files /dev/null and b/Categories_import.xlsx differ diff --git a/Clients_import.xlsx b/Clients_import.xlsx new file mode 100644 index 0000000..52b7265 Binary files /dev/null and b/Clients_import.xlsx differ diff --git a/Materials_import.xlsx b/Materials_import.xlsx new file mode 100644 index 0000000..31f0f4d Binary files /dev/null and b/Materials_import.xlsx differ diff --git a/OrderItems_import.xlsx b/OrderItems_import.xlsx new file mode 100644 index 0000000..b9ef8e1 Binary files /dev/null and b/OrderItems_import.xlsx differ diff --git a/Orders_import.xlsx b/Orders_import.xlsx new file mode 100644 index 0000000..f789f33 Binary files /dev/null and b/Orders_import.xlsx differ diff --git a/Products_import.xlsx b/Products_import.xlsx new file mode 100644 index 0000000..756421d Binary files /dev/null and b/Products_import.xlsx differ diff --git a/README.md b/README.md deleted file mode 100644 index 61c75c5..0000000 --- a/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# MY PROJECT - - - -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin http://192.168.0.185/Danil/my-project.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -- [ ] [Set up project integrations](http://192.168.0.185/Danil/my-project/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README - -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/Resource/Categories_import.xlsx b/Resource/Categories_import.xlsx new file mode 100644 index 0000000..317a00e Binary files /dev/null and b/Resource/Categories_import.xlsx differ diff --git a/Resource/Clients_import.xlsx b/Resource/Clients_import.xlsx new file mode 100644 index 0000000..52b7265 Binary files /dev/null and b/Resource/Clients_import.xlsx differ diff --git a/Resource/Materials_import.xlsx b/Resource/Materials_import.xlsx new file mode 100644 index 0000000..31f0f4d Binary files /dev/null and b/Resource/Materials_import.xlsx differ diff --git a/Resource/OfficeStyle.png b/Resource/OfficeStyle.png new file mode 100644 index 0000000..170e5cf Binary files /dev/null and b/Resource/OfficeStyle.png differ diff --git a/Resource/OrderItems_import.xlsx b/Resource/OrderItems_import.xlsx new file mode 100644 index 0000000..b9ef8e1 Binary files /dev/null and b/Resource/OrderItems_import.xlsx differ diff --git a/Resource/Orders_import.xlsx b/Resource/Orders_import.xlsx new file mode 100644 index 0000000..f789f33 Binary files /dev/null and b/Resource/Orders_import.xlsx differ diff --git a/Resource/Products_import.xlsx b/Resource/Products_import.xlsx new file mode 100644 index 0000000..756421d Binary files /dev/null and b/Resource/Products_import.xlsx differ diff --git a/Resource/favicon.ico b/Resource/favicon.ico new file mode 100644 index 0000000..170e5cf Binary files /dev/null and b/Resource/favicon.ico differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..0cff216 --- /dev/null +++ b/main.py @@ -0,0 +1,653 @@ +import sqlite3 +import pandas as pd +import os +import sys +from datetime import datetime + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +RESOURCE_DIR = os.path.join(BASE_DIR, "Resource") +DB_PATH = os.path.join(BASE_DIR, "partners.db") + + +def create_tables(db_connection): + """Создает все необходимые таблицы в базе данных""" + cursor = db_connection.cursor() + + cursor.executescript(''' + -- Типы контрагентов + CREATE TABLE IF NOT EXISTS ContractorType ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type_name VARCHAR(100) NOT NULL UNIQUE + ); + + -- Контрагенты/Клиенты + CREATE TABLE IF NOT EXISTS Clients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + contractor_type_id INTEGER REFERENCES ContractorType(id), + name VARCHAR(255) NOT NULL, + director VARCHAR(255), + email VARCHAR(255), + phone VARCHAR(50), + address TEXT, + inn VARCHAR(20) UNIQUE, + rating DECIMAL(3,2) DEFAULT 0.0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Категории продукции + CREATE TABLE IF NOT EXISTS Categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_name VARCHAR(100) NOT NULL UNIQUE, + coefficient DECIMAL(5,2) NOT NULL DEFAULT 1.0 + ); + + -- Таблица материалов + CREATE TABLE IF NOT EXISTS Materials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + material_name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + category VARCHAR(50), + color VARCHAR(50), + density DECIMAL(10,2), + thickness DECIMAL(10,2), + supplier VARCHAR(255), + country VARCHAR(100), + wear_class VARCHAR(20), + price_per_unit DECIMAL(15,2), + unit VARCHAR(20) DEFAULT 'шт', + in_stock INTEGER DEFAULT 0, + production_time_days INTEGER DEFAULT 1, + daily_capacity INTEGER DEFAULT 10, + cost_price DECIMAL(15,2), + markup_percent DECIMAL(5,2) DEFAULT 30, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Каталог продукции + CREATE TABLE IF NOT EXISTS Catalog ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER REFERENCES Categories(id), + material_id INTEGER REFERENCES Materials(id), + product_name VARCHAR(255) NOT NULL, + article VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + min_price DECIMAL(15,2) NOT NULL, + quantity_in_stock INTEGER DEFAULT 0, + production_time_days INTEGER DEFAULT 1, + daily_capacity INTEGER DEFAULT 10, + cost_price DECIMAL(15,2), + profit_per_unit DECIMAL(15,2), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Таблица корзин + CREATE TABLE IF NOT EXISTS Cart ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER REFERENCES Clients(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status VARCHAR(50) DEFAULT 'активна' + ); + + -- Таблица товаров в корзине + CREATE TABLE IF NOT EXISTS CartItems ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + cart_id INTEGER REFERENCES Cart(id), + product_id INTEGER REFERENCES Catalog(id), + quantity INTEGER NOT NULL CHECK (quantity > 0), + price_at_moment DECIMAL(15,2) NOT NULL, + added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Заказы + CREATE TABLE IF NOT EXISTS Orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER REFERENCES Clients(id), + order_date DATE NOT NULL, + total_amount DECIMAL(15,2) NOT NULL, + status VARCHAR(50) DEFAULT 'Новый', + production_days INTEGER DEFAULT 1, + production_start_date DATE, + production_end_date DATE, + production_status VARCHAR(50) DEFAULT 'Ожидает', + assembly_percent INTEGER DEFAULT 15, + delivery_percent INTEGER DEFAULT 10, + assembly_cost DECIMAL(15,2) DEFAULT 0, + delivery_cost DECIMAL(15,2) DEFAULT 0, + base_amount DECIMAL(15,2) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Состав заказа + CREATE TABLE IF NOT EXISTS OrderItems ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL REFERENCES Orders(id), + product_id INTEGER NOT NULL REFERENCES Catalog(id), + quantity INTEGER NOT NULL CHECK (quantity > 0), + price_at_moment DECIMAL(15,2) NOT NULL + ); + + -- Индексы для оптимизации + CREATE INDEX IF NOT EXISTS idx_client_type ON Clients(contractor_type_id); + CREATE INDEX IF NOT EXISTS idx_client_inn ON Clients(inn); + CREATE INDEX IF NOT EXISTS idx_catalog_category ON Catalog(category_id); + CREATE INDEX IF NOT EXISTS idx_catalog_material ON Catalog(material_id); + CREATE INDEX IF NOT EXISTS idx_catalog_article ON Catalog(article); + CREATE INDEX IF NOT EXISTS idx_material_name ON Materials(material_name); + CREATE INDEX IF NOT EXISTS idx_material_category ON Materials(category); + CREATE INDEX IF NOT EXISTS idx_material_supplier ON Materials(supplier); + CREATE INDEX IF NOT EXISTS idx_cart_client ON Cart(client_id); + CREATE INDEX IF NOT EXISTS idx_cart_status ON Cart(status); + CREATE INDEX IF NOT EXISTS idx_cart_items_cart ON CartItems(cart_id); + CREATE INDEX IF NOT EXISTS idx_order_client ON Orders(client_id); + CREATE INDEX IF NOT EXISTS idx_order_date ON Orders(order_date); + CREATE INDEX IF NOT EXISTS idx_order_status ON Orders(status); + CREATE INDEX IF NOT EXISTS idx_production_status ON Orders(production_status); + CREATE INDEX IF NOT EXISTS idx_orderitems_order ON OrderItems(order_id); + CREATE INDEX IF NOT EXISTS idx_orderitems_product ON OrderItems(product_id); + ''') + + db_connection.commit() + print("✅ Таблицы успешно созданы") + + +def calculate_discount(total_quantity): + """Рассчитывает скидку на основе общего количества заказанной продукции""" + if total_quantity >= 300000: + return 15, '15% (более 300000 ед.)' + elif total_quantity >= 50000: + return 10, '10% (50000-300000 ед.)' + elif total_quantity >= 10000: + return 5, '5% (10000-50000 ед.)' + else: + return 0, '0% (до 10000 ед.)' + + +def get_clients(db_path=DB_PATH, sort_by='rating', ascending=False): + """Получает данные о клиентах с возможностью сортировки""" + try: + conn = sqlite3.connect(db_path) + query = """ + SELECT + ct.type_name AS "Тип клиента", + c.name AS "Наименование клиента", + c.director AS "Директор", + c.email AS "Электронная почта", + c.phone AS "Телефон", + c.address AS "Адрес", + c.inn AS "ИНН", + c.rating AS "Рейтинг", + c.created_at AS "Дата создания", + COALESCE(SUM(oi.quantity), 0) as total_quantity, + COALESCE(SUM(o.total_amount), 0) as total_amount + FROM Clients c + JOIN ContractorType ct ON c.contractor_type_id = ct.id + LEFT JOIN Orders o ON c.id = o.client_id + LEFT JOIN OrderItems oi ON o.id = oi.order_id + GROUP BY c.id + """ + + if sort_by == 'discount': + query += " ORDER BY total_quantity" + elif sort_by == 'rating': + query += " ORDER BY c.rating" + elif sort_by == 'name': + query += " ORDER BY c.name" + elif sort_by == 'type': + query += " ORDER BY ct.type_name, c.name" + else: + query += " ORDER BY c.rating" + + query += " DESC" if not ascending else " ASC" + + df = pd.read_sql_query(query, conn) + conn.close() + + if not df.empty: + df['Скидка_процент'] = df['total_quantity'].apply(lambda x: calculate_discount(x)[0]) + df['Скидка_текст'] = df['total_quantity'].apply(lambda x: calculate_discount(x)[1]) + df['Общая_сумма'] = df['total_amount'].apply(lambda x: f"{x:,.2f} руб.") + + return df + + except Exception as e: + print(f"❌ Ошибка: {e}") + return None + + +def get_orders_history(db_path=DB_PATH, limit=50, sort_by='date_desc'): + """Получает историю заказов с разными вариантами сортировки""" + try: + conn = sqlite3.connect(db_path) + query = """ + SELECT + o.order_date AS "Дата заказа", + c.name AS "Клиент", + GROUP_CONCAT(cat.product_name || ' x' || oi.quantity, ', ') AS "Товары", + SUM(oi.quantity) AS "Количество", + o.total_amount AS "Сумма заказа", + o.status AS "Статус" + FROM Orders o + JOIN Clients c ON o.client_id = c.id + LEFT JOIN OrderItems oi ON o.id = oi.order_id + LEFT JOIN Catalog cat ON oi.product_id = cat.id + GROUP BY o.id + """ + + if sort_by == 'date_desc': + query += " ORDER BY o.order_date DESC, o.created_at DESC" + elif sort_by == 'date_asc': + query += " ORDER BY o.order_date ASC, o.created_at ASC" + elif sort_by == 'quantity_desc': + query += " ORDER BY SUM(oi.quantity) DESC" + elif sort_by == 'amount_desc': + query += " ORDER BY o.total_amount DESC" + elif sort_by == 'status': + query += " ORDER BY o.status, o.order_date DESC" + else: + query += " ORDER BY o.order_date DESC, o.created_at DESC" + + query += " LIMIT ?" + + df = pd.read_sql_query(query, conn, params=(limit,)) + conn.close() + return df + except Exception as e: + print(f"❌ Ошибка: {e}") + return None + + +def print_client_cards(clients_df): + """Выводит карточки клиентов в красивом формате""" + if clients_df is None or clients_df.empty: + print("📭 Нет данных о клиентах") + return + + for index, row in clients_df.iterrows(): + print("╔" + "═" * 79 + "╗") + print(f"║ {'КАРТОЧКА КЛИЕНТА':^77} ║") + print("╠" + "═" * 79 + "╣") + + type_text = f"Тип: {row['Тип клиента']}" + name_text = f"Наименование: {row['Наименование клиента']}" + director_text = f"Директор: {row['Директор']}" + rating_text = f"Рейтинг: {row['Рейтинг']}" + quantity_text = f"Заказано единиц: {row['total_quantity']:,}" + amount_text = f"На сумму: {row['total_amount']:,.2f} руб." + discount_text = f"Скидка: {row['Скидка_текст']}" + email_text = f"Email: {row['Электронная почта']}" + phone_text = f"Телефон: {row['Телефон']}" + inn_text = f"ИНН: {row['ИНН']}" + + address = row['Адрес'] if row['Адрес'] else 'Не указан' + if len(address) > 70: + address = address[:67] + "..." + address_text = f"Адрес: {address}" + + print(f"║ {type_text:<77} ║") + print(f"║ {name_text:<77} ║") + print(f"║ {director_text:<77} ║") + print(f"║ {rating_text:<77} ║") + print(f"║ {quantity_text:<77} ║") + print(f"║ {amount_text:<77} ║") + print(f"║ {discount_text:<77} ║") + print(f"║ {'':<77} ║") + print(f"║ {email_text:<77} ║") + print(f"║ {phone_text:<77} ║") + print(f"║ {inn_text:<77} ║") + print(f"║ {address_text:<77} ║") + print("╚" + "═" * 79 + "╝") + print() + + +def print_orders_cards(orders_df): + """Выводит карточки заказов в виде таблицы""" + if orders_df is None or orders_df.empty: + print("📭 Нет данных о заказах") + return + + print("╔" + "═" * 167 + "╗") + print(f"║ {'ИСТОРИЯ ЗАКАЗОВ':^165} ║") + print("╠" + "═" * 167 + "╣") + + headers = ["Дата", "Клиент", "Товары", "Кол-во", "Статус", "Сумма"] + print("║ {:<12} {:<25} {:<55} {:>12} {:<15} {:>15} ║".format( + headers[0], headers[1], headers[2], headers[3], headers[4], headers[5] + )) + print("╠" + "═" * 167 + "╣") + + for index, row in orders_df.iterrows(): + date = str(row['Дата заказа'])[:10] + client = str(row['Клиент'])[:25] if len(str(row['Клиент'])) > 25 else str(row['Клиент']) + products = str(row['Товары'])[:55] if len(str(row['Товары'])) > 55 else str(row['Товары']) + quantity = f"{int(row['Количество']):,}" + status = str(row['Статус'])[:15] if len(str(row['Статус'])) > 15 else str(row['Статус']) + amount = f"{float(row['Сумма заказа']):,.2f}" + + line = f"║ {date:<12} {client:<25} {products:<55} {quantity:>12} {status:<15} {amount:>15} ║" + print(line) + + print("╚" + "═" * 167 + "╝") + + total_quantity = orders_df['Количество'].sum() + total_amount = orders_df['Сумма заказа'].sum() + print(f"📊 ИТОГО: {len(orders_df)} заказов, {total_quantity:,.0f} единиц, общая сумма: {total_amount:,.2f} руб.") + + status_stats = orders_df['Статус'].value_counts() + print("📊 Статусы заказов:") + for status, count in status_stats.items(): + print(f" - {status}: {count}") + + +def simple_choice(options): + """Упрощенная функция выбора""" + print("\n" + "=" * 60) + print("ВЫБЕРИТЕ ОПЦИЮ:") + print("=" * 60) + for i, option in enumerate(options, 1): + print(f"{i}. {option}") + + while True: + try: + choice = input(f"\nВведите номер (1-{len(options)}): ").strip() + if choice.isdigit(): + choice_num = int(choice) + if 1 <= choice_num <= len(options): + return options[choice_num - 1] + print(f"❌ Пожалуйста, введите число от 1 до {len(options)}") + except KeyboardInterrupt: + print("\n👋 Программа прервана пользователем.") + sys.exit(0) + + +def safe_float(value, default=None): + """Безопасное преобразование в float""" + if pd.isna(value) or value == '-': + return default + try: + return float(value) + except (ValueError, TypeError): + return default + + +def safe_int(value, default=None): + """Безопасное преобразование в int""" + if pd.isna(value) or value == '-': + return default + try: + return int(float(value)) + except (ValueError, TypeError): + return default + + +def load_data_from_files(): + """Загружает данные из Excel файлов в базу данных""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + try: + print("\n📁 Введите имена файлов (или нажмите Enter для значений по умолчанию):") + clients_file = input("Файл с клиентами (Enter = Clients_import.xlsx): ").strip() or "Clients_import.xlsx" + categories_file = input("Файл с категориями (Enter = Categories_import.xlsx): ").strip() or "Categories_import.xlsx" + materials_file = input("Файл с материалами (Enter = Materials_import.xlsx): ").strip() or "Materials_import.xlsx" + catalog_file = input("Файл с каталогом (Enter = Products_import.xlsx): ").strip() or "Products_import.xlsx" + orders_file = input("Файл с заказами (Enter = Orders_import.xlsx): ").strip() or "Orders_import.xlsx" + orderitems_file = input("Файл с составом заказов (Enter = OrderItems_import.xlsx): ").strip() or "OrderItems_import.xlsx" + + # Формируем полные пути + clients_file = os.path.join(RESOURCE_DIR, os.path.basename(clients_file)) + categories_file = os.path.join(RESOURCE_DIR, os.path.basename(categories_file)) + materials_file = os.path.join(RESOURCE_DIR, os.path.basename(materials_file)) + catalog_file = os.path.join(RESOURCE_DIR, os.path.basename(catalog_file)) + orders_file = os.path.join(RESOURCE_DIR, os.path.basename(orders_file)) + orderitems_file = os.path.join(RESOURCE_DIR, os.path.basename(orderitems_file)) + + print(f"\n📂 Загружаем файлы из папки: {RESOURCE_DIR}") + + # 1. Загрузка типов контрагентов + print("\n1. 📊 Загрузка типов контрагентов...") + clients_df = pd.read_excel(clients_file) + + if 'Тип партнера' in clients_df.columns: + contractor_types = clients_df['Тип партнера'].unique() + for contractor_type in contractor_types: + cursor.execute("INSERT OR IGNORE INTO ContractorType (type_name) VALUES (?)", (contractor_type,)) + print(f" Загружено {len(contractor_types)} типов") + + # 2. Загрузка клиентов + print("2. 👥 Загрузка клиентов...") + loaded_clients = 0 + for _, row in clients_df.iterrows(): + type_value = row.get('Тип партнера', 'Клиент') + cursor.execute("SELECT id FROM ContractorType WHERE type_name = ?", (type_value,)) + result = cursor.fetchone() + contractor_type_id = result[0] if result else 1 + + cursor.execute("""INSERT OR IGNORE INTO Clients + (contractor_type_id, name, director, email, phone, address, inn, rating) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + (contractor_type_id, + row.get('Наименование партнера', ''), + row.get('Директор', ''), + row.get('Электронная почта партнера', ''), + row.get('Телефон партнера', ''), + row.get('Юридический адрес партнера', ''), + row.get('ИНН', ''), + safe_float(row.get('Рейтинг', 0.0), 0.0)) + ) + loaded_clients += 1 + print(f" Загружено {loaded_clients} клиентов") + + # 3. Загрузка категорий + print("3. 📦 Загрузка категорий...") + categories_df = pd.read_excel(categories_file) + loaded_categories = 0 + for _, row in categories_df.iterrows(): + cursor.execute("INSERT OR IGNORE INTO Categories (category_name, coefficient) VALUES (?, ?)", + (row.get('Тип продукции', ''), + safe_float(row.get('Коэффициент типа продукции', 1.0), 1.0)) + ) + loaded_categories += 1 + print(f" Загружено {loaded_categories} категорий") + + # 4. Загрузка материалов + print("4. 📦 Загрузка материалов...") + materials_df = pd.read_excel(materials_file) + + loaded_materials = 0 + for _, row in materials_df.iterrows(): + cursor.execute(""" + INSERT OR IGNORE INTO Materials + (material_name, description, category, color, density, thickness, + supplier, country, wear_class, price_per_unit, unit, in_stock, + production_time_days, daily_capacity, markup_percent) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + row.get('Наименование материала', ''), + row.get('Описание', ''), + row.get('Категория', ''), + row.get('Цвет', ''), + safe_float(row.get('Плотность', None)), + safe_float(row.get('Толщина', None)), + row.get('Поставщик', ''), + row.get('Страна', ''), + row.get('Класс износостойкости', ''), + safe_float(row.get('Цена за ед.', 0), 0), + row.get('Единица', 'шт'), + safe_int(row.get('В наличии', 0), 0), + 1, 10, 30 + )) + loaded_materials += 1 + print(f" ✅ Загружено {loaded_materials} материалов") + + # 5. Загрузка каталога + print("5. 🏷️ Загрузка каталога продукции...") + catalog_df = pd.read_excel(catalog_file) + + loaded_products = 0 + for index, row in catalog_df.iterrows(): + prod_days = 1 + if 'production_time_days' in catalog_df.columns: + val = row['production_time_days'] + if not pd.isna(val): + try: + prod_days = int(float(val)) + except: + prod_days = 1 + + cursor.execute(""" + INSERT OR IGNORE INTO Catalog + (category_id, material_id, product_name, article, description, min_price, quantity_in_stock, + production_time_days, daily_capacity) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + safe_int(row.get('category_id', 1), 1), + safe_int(row.get('material_id', None)), + row.get('product_name', ''), + row.get('article', ''), + row.get('description', ''), + safe_float(row.get('min_price', 0), 0), + safe_int(row.get('quantity_in_stock', 0), 0), + prod_days, + 10 + )) + loaded_products += 1 + + print(f" ✅ Загружено {loaded_products} товаров") + + # 6. Загрузка заказов + print("6. 📝 Загрузка заказов...") + orders_df = pd.read_excel(orders_file) + loaded_orders = 0 + for _, row in orders_df.iterrows(): + # Преобразование дат + date_val = row.get('order_date') + if pd.isna(date_val): + order_date = datetime.now().strftime('%Y-%m-%d') + else: + if hasattr(date_val, 'strftime'): + order_date = date_val.strftime('%Y-%m-%d') + else: + order_date = str(date_val) + + prod_start = row.get('production_start_date') + if pd.isna(prod_start): + prod_start = None + else: + if hasattr(prod_start, 'strftime'): + prod_start = prod_start.strftime('%Y-%m-%d') + else: + prod_start = str(prod_start) + + prod_end = row.get('production_end_date') + if pd.isna(prod_end): + prod_end = None + else: + if hasattr(prod_end, 'strftime'): + prod_end = prod_end.strftime('%Y-%m-%d') + else: + prod_end = str(prod_end) + + cursor.execute(""" + INSERT INTO Orders + (id, client_id, order_date, total_amount, status, + production_days, production_start_date, production_end_date, production_status, + assembly_percent, delivery_percent, assembly_cost, delivery_cost, base_amount) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + safe_int(row.get('id')), + safe_int(row.get('client_id')), + order_date, + safe_float(row.get('total_amount'), 0), + row.get('status', 'Новый'), + safe_int(row.get('production_days', 1)), + prod_start, + prod_end, + row.get('production_status', 'Ожидает'), + safe_int(row.get('assembly_percent', 15)), + safe_int(row.get('delivery_percent', 10)), + safe_float(row.get('assembly_cost', 0), 0), + safe_float(row.get('delivery_cost', 0), 0), + safe_float(row.get('base_amount', 0), 0) + )) + loaded_orders += 1 + print(f" ✅ Загружено {loaded_orders} заказов") + + # 7. Загрузка состава заказов (OrderItems) + print("7. 📦 Загрузка состава заказов...") + orderitems_df = pd.read_excel(orderitems_file) + loaded_items = 0 + for _, row in orderitems_df.iterrows(): + cursor.execute(""" + INSERT INTO OrderItems (order_id, product_id, quantity, price_at_moment) + VALUES (?, ?, ?, ?) + """, ( + safe_int(row.get('order_id')), + safe_int(row.get('product_id')), + safe_int(row.get('quantity'), 1), + safe_float(row.get('price_at_moment'), 0) + )) + loaded_items += 1 + print(f" ✅ Загружено {loaded_items} позиций") + + conn.commit() + print(f"✅ Данные успешно загружены!") + + except Exception as e: + print(f"❌ Ошибка при загрузке данных: {e}") + import traceback + traceback.print_exc() + conn.rollback() + finally: + conn.close() + + +def display_menu(): + """Главное меню программы""" + menu_options = [ + 'КАРТОЧКИ КЛИЕНТОВ (сортировка по рейтингу)', + 'КАРТОЧКИ КЛИЕНТОВ (сортировка по скидкам)', + 'ИСТОРИЯ ЗАКАЗОВ', + 'ЗАПОЛНЕНИЕ БД из файлов', + 'ВЫХОД' + ] + + while True: + choice = simple_choice(menu_options) + + if choice == 'КАРТОЧКИ КЛИЕНТОВ (сортировка по рейтингу)': + print("\n🎯 КАРТОЧКИ КЛИЕНТОВ (сортировка по рейтингу)") + print("═" * 80) + clients = get_clients(sort_by='rating', ascending=False) + print_client_cards(clients) + + elif choice == 'КАРТОЧКИ КЛИЕНТОВ (сортировка по скидкам)': + print("\n📊 КАРТОЧКИ КЛИЕНТОВ (сортировка по скидкам)") + print("═" * 80) + clients = get_clients(sort_by='discount', ascending=False) + print_client_cards(clients) + + elif choice == 'ИСТОРИЯ ЗАКАЗОВ': + print("\n📈 ИСТОРИЯ ЗАКАЗОВ") + print("═" * 80) + orders = get_orders_history(limit=50) + print_orders_cards(orders) + + elif choice == 'ЗАПОЛНЕНИЕ БД из файлов': + if os.path.exists(DB_PATH): + os.remove(DB_PATH) + print("🗑️ База данных удалена") + else: + print("ℹ️ Создаем новую базу") + + conn = sqlite3.connect(DB_PATH) + create_tables(conn) + conn.close() + load_data_from_files() + + elif choice == 'ВЫХОД': + print("\n👋 До свидания!") + break + + +if __name__ == "__main__": + display_menu() \ No newline at end of file diff --git a/main_flask.py b/main_flask.py new file mode 100644 index 0000000..9b65b46 --- /dev/null +++ b/main_flask.py @@ -0,0 +1,897 @@ +from flask import Flask, render_template, send_from_directory, request, redirect, url_for, flash, session +import sqlite3 +import os +from datetime import datetime, timedelta + +app = Flask(__name__) +app.secret_key = 'my_secret_key_12345' + +# ========== ФИЛЬТР ДЛЯ ФОРМАТИРОВАНИЯ ДАТ ========== +@app.template_filter('date_format') +def date_format(value): + """Преобразует дату из формата YYYY-MM-DD в ДД.ММ.ГГГГ""" + if not value: + return '-' + try: + # Если value уже строка, пробуем распарсить + if isinstance(value, str) and len(value) >= 10: + # Проверяем, что это похоже на дату в формате YYYY-MM-DD + if value[4] == '-' and value[7] == '-': + year, month, day = value[:10].split('-') + return f"{day}.{month}.{year}" + return value + except: + return value + +def get_db_connection(): + conn = sqlite3.connect('partners.db') + conn.row_factory = sqlite3.Row + return conn + +def dict_from_row(row): + """Преобразует sqlite3.Row в словарь""" + if row is None: + return None + return {key: row[key] for key in row.keys()} + +def list_from_rows(rows): + """Преобразует список sqlite3.Row в список словарей""" + return [dict_from_row(row) for row in rows] + +def calculate_discount(total_amount): + """Рассчитывает скидку на основе общей суммы заказов в рублях""" + return 0, '0%' + +def calculate_order_discount(order_amount): + """Рассчитывает скидку на текущий заказ в зависимости от его суммы""" + return 0 + +# ========== ГЛАВНАЯ СТРАНИЦА ========== + +@app.route('/') +def index(): + # Получаем параметры фильтрации из URL + search_query = request.args.get('search', '') + client_type = request.args.get('type', 'all') + min_rating = request.args.get('rating', '0') + sort_by = request.args.get('sort', 'name_asc') + + conn = get_db_connection() + + try: + # Получаем общее количество всех клиентов (без фильтров) + total_count = conn.execute('SELECT COUNT(*) as count FROM Clients').fetchone()['count'] + + # Базовый SQL запрос для клиентов + sql = ''' + SELECT c.*, ct.type_name as contractor_type_name, + COALESCE(SUM(o.total_amount), 0) as total_amount, + COALESCE(SUM(oi.quantity), 0) as total_quantity, + COUNT(DISTINCT o.id) as orders_count + FROM Clients c + LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id + LEFT JOIN Orders o ON c.id = o.client_id + LEFT JOIN OrderItems oi ON o.id = oi.order_id + WHERE 1=1 + ''' + params = [] + + # Поиск по названию или ИНН + if search_query: + sql += " AND (c.name LIKE ? OR c.inn LIKE ?)" + params.extend([f'%{search_query}%', f'%{search_query}%']) + + # Фильтр по типу клиента + if client_type != 'all': + sql += " AND ct.type_name = ?" + params.append(client_type) + + # Фильтр по минимальному рейтингу + if min_rating != '0': + sql += " AND c.rating >= ?" + params.append(float(min_rating)) + + # Группировка (обязательно после WHERE) + sql += " GROUP BY c.id" + + # Сортировка + if sort_by == 'name_asc': + sql += " ORDER BY c.name ASC" + elif sort_by == 'name_desc': + sql += " ORDER BY c.name DESC" + elif sort_by == 'rating_desc': + sql += " ORDER BY c.rating DESC" + elif sort_by == 'rating_asc': + sql += " ORDER BY c.rating ASC" + elif sort_by == 'discount_desc': + sql += " ORDER BY total_amount DESC" + elif sort_by == 'discount_asc': + sql += " ORDER BY total_amount ASC" + else: + sql += " ORDER BY c.name ASC" + + # Выполняем запрос и преобразуем в словари + rows = conn.execute(sql, params).fetchall() + clients = list_from_rows(rows) + + # Добавляем информацию о скидке на основе суммы + clients_with_discount = [] + for client in clients: + total_amount = client['total_amount'] or 0 + discount_percent, discount_text = calculate_discount(total_amount) + + client['total_sales'] = total_amount + client['discount_percent'] = discount_percent + client['discount'] = discount_percent + client['discount_text'] = discount_text + + clients_with_discount.append(client) + + conn.close() + + return render_template('index.html', + partners=clients_with_discount, + total_count=total_count) + + except Exception as e: + conn.close() + print(f"❌ ОШИБКА: {e}") + return render_template('index.html', + partners=[], + total_count=0, + error_message=f"Ошибка базы данных: {str(e)}") + +# ========== МАРШРУТЫ ДЛЯ КЛИЕНТОВ ========== + +@app.route('/add_client', methods=['GET', 'POST']) +def add_client(): + if request.method == 'POST': + try: + name = request.form['name'] + director = request.form.get('director', '') + email = request.form['email'] + phone = request.form.get('phone', '') + address = request.form.get('address', '') + inn = request.form.get('inn', '') + contractor_type_id = request.form['contractor_type_id'] + rating = request.form.get('rating', 0.0) + + conn = get_db_connection() + + # Проверяем уникальность ИНН + if inn: + existing = conn.execute('SELECT id FROM Clients WHERE inn = ?', (inn,)).fetchone() + if existing: + flash('Клиент с таким ИНН уже существует', 'error') + conn.close() + return redirect(url_for('add_client')) + + conn.execute(''' + INSERT INTO Clients (name, director, email, phone, address, inn, contractor_type_id, rating) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', (name, director, email, phone, address, inn, contractor_type_id, rating)) + + conn.commit() + conn.close() + flash('Клиент успешно добавлен!', 'success') + return redirect(url_for('index')) + + except Exception as e: + flash(f'Ошибка: {str(e)}', 'error') + return redirect(url_for('add_client')) + + # GET запрос + conn = get_db_connection() + contractor_types = conn.execute('SELECT * FROM ContractorType ORDER BY type_name').fetchall() + conn.close() + return render_template('add_client.html', contractor_types=contractor_types) + +@app.route('/edit_client/', methods=['GET', 'POST']) +def edit_client(client_id): + conn = get_db_connection() + + if request.method == 'POST': + try: + name = request.form['name'] + director = request.form.get('director', '') + email = request.form['email'] + phone = request.form.get('phone', '') + address = request.form.get('address', '') + inn = request.form.get('inn', '') + contractor_type_id = request.form['contractor_type_id'] + rating = request.form.get('rating', 0.0) + + # Проверяем уникальность ИНН + if inn: + existing = conn.execute( + 'SELECT id FROM Clients WHERE inn = ? AND id != ?', + (inn, client_id) + ).fetchone() + if existing: + flash('Клиент с таким ИНН уже существует', 'error') + conn.close() + return redirect(url_for('edit_client', client_id=client_id)) + + conn.execute(''' + UPDATE Clients + SET name = ?, director = ?, email = ?, phone = ?, + address = ?, inn = ?, contractor_type_id = ?, rating = ? + WHERE id = ? + ''', (name, director, email, phone, address, inn, contractor_type_id, rating, client_id)) + + conn.commit() + conn.close() + flash('Данные обновлены!', 'success') + return redirect(url_for('index')) + + except Exception as e: + flash(f'Ошибка: {str(e)}', 'error') + conn.close() + return redirect(url_for('edit_client', client_id=client_id)) + + # GET запрос + client_row = conn.execute(''' + SELECT c.*, ct.type_name as contractor_type_name + FROM Clients c + LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id + WHERE c.id = ? + ''', (client_id,)).fetchone() + + client_dict = dict_from_row(client_row) + + contractor_types = conn.execute('SELECT * FROM ContractorType ORDER BY type_name').fetchall() + conn.close() + + if not client_dict: + flash('Клиент не найден', 'error') + return redirect(url_for('index')) + + return render_template('edit_client.html', client=client_dict, contractor_types=contractor_types) + +# ========== МАРШРУТЫ ДЛЯ ЗАКАЗОВ ========== + +@app.route('/order_history/') +def order_history(client_id): + conn = get_db_connection() + + client_row = conn.execute(''' + SELECT c.*, ct.type_name as contractor_type_name + FROM Clients c + LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id + WHERE c.id = ? + ''', (client_id,)).fetchone() + + client_dict = dict_from_row(client_row) + + # Получаем заказы клиента + orders_rows = conn.execute(''' + SELECT + o.* + FROM Orders o + WHERE o.client_id = ? + ORDER BY o.order_date DESC, o.id DESC + ''', (client_id,)).fetchall() + + # Получаем детальную информацию о товарах в каждом заказе + orders_with_items = [] + total_quantity_all = 0 + total_amount_all = 0 + completed_count = 0 + + for order_row in orders_rows: + order_dict = dict_from_row(order_row) + + # Получаем товары для этого заказа + items_rows = conn.execute(''' + SELECT oi.*, cat.product_name, cat.article, cat.min_price, cat2.category_name + FROM OrderItems oi + JOIN Catalog cat ON oi.product_id = cat.id + LEFT JOIN Categories cat2 ON cat.category_id = cat2.id + WHERE oi.order_id = ? + ''', (order_dict['id'],)).fetchall() + + items_list = list_from_rows(items_rows) + order_dict['items'] = items_list + + # Считаем общее количество для этого заказа + order_total_qty = sum(item['quantity'] for item in items_list) if items_list else 0 + order_dict['total_items'] = order_total_qty + + total_quantity_all += order_total_qty + total_amount_all += order_dict['total_amount'] + + if order_dict['status'] == 'Выполнен': + completed_count += 1 + + orders_with_items.append(order_dict) + + # Статистика + total_stats = { + 'total_orders': len(orders_with_items), + 'total_quantity': total_quantity_all, + 'total_amount': total_amount_all, + 'completed_orders': completed_count + } + + conn.close() + + if not client_dict: + flash('Клиент не найден', 'error') + return redirect(url_for('index')) + + return render_template('order_history.html', + client=client_dict, + orders=orders_with_items, + total_stats=total_stats) + +# ========== МАРШРУТ ДЛЯ ОБНОВЛЕНИЯ СТАТУСА ЗАКАЗА ========== +@app.route('/update_order_status/', methods=['POST']) +def update_order_status(order_id): + """Обновление статуса заказа""" + try: + new_status = request.form['status'] + + conn = get_db_connection() + conn.execute('UPDATE Orders SET status = ? WHERE id = ?', (new_status, order_id)) + conn.commit() + conn.close() + + flash(f'Статус заказа обновлен на "{new_status}"', 'success') + except Exception as e: + flash(f'Ошибка при обновлении статуса: {str(e)}', 'error') + + return redirect(request.referrer or url_for('orders_list')) + +# ========== МАРШРУТ ДЛЯ ОБНОВЛЕНИЯ СТАТУСА ПРОИЗВОДСТВА ========== +@app.route('/update_production_status/', methods=['POST']) +def update_production_status(order_id): + """Обновление статуса производства""" + try: + new_status = request.form['production_status'] + conn = get_db_connection() + conn.execute('UPDATE Orders SET production_status = ? WHERE id = ?', (new_status, order_id)) + conn.commit() + conn.close() + flash(f'Статус производства обновлен на "{new_status}"', 'success') + except Exception as e: + flash(f'Ошибка при обновлении статуса производства: {str(e)}', 'error') + return redirect(request.referrer) + +# ========== МАРШРУТЫ ДЛЯ ТОВАРОВ ========== + +@app.route('/add_product', methods=['GET', 'POST']) +def add_product(): + if request.method == 'POST': + try: + product_name = request.form['product_name'] + article = request.form['article'] + category_id = request.form['category_id'] + material_id = request.form.get('material_id') or None + description = request.form.get('description', '') + min_price = float(request.form['min_price']) + quantity_in_stock = int(request.form.get('quantity_in_stock', 0)) + production_time_days = int(request.form.get('production_time_days', 1)) + + conn = get_db_connection() + + existing = conn.execute('SELECT id FROM Catalog WHERE article = ?', (article,)).fetchone() + if existing: + flash('Товар с таким артикулом уже существует', 'error') + conn.close() + return redirect(url_for('add_product')) + + conn.execute(''' + INSERT INTO Catalog + (product_name, article, category_id, material_id, description, min_price, + quantity_in_stock, production_time_days) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', (product_name, article, category_id, material_id, description, min_price, + quantity_in_stock, production_time_days)) + + conn.commit() + conn.close() + + flash('Товар успешно добавлен!', 'success') + return redirect(url_for('catalog')) + + except Exception as e: + flash(f'Ошибка: {str(e)}', 'error') + return redirect(url_for('add_product')) + + conn = get_db_connection() + categories = conn.execute('SELECT * FROM Categories ORDER BY category_name').fetchall() + materials = conn.execute('SELECT * FROM Materials ORDER BY material_name').fetchall() + conn.close() + + return render_template('add_product.html', categories=categories, materials=materials) + +@app.route('/edit_product/', methods=['GET', 'POST']) +def edit_product(product_id): + conn = get_db_connection() + + if request.method == 'POST': + try: + product_name = request.form['product_name'] + article = request.form['article'] + category_id = request.form['category_id'] + material_id = request.form.get('material_id') or None + description = request.form.get('description', '') + min_price = float(request.form['min_price']) + quantity_in_stock = int(request.form.get('quantity_in_stock', 0)) + production_time_days = int(request.form.get('production_time_days', 1)) + + existing = conn.execute( + 'SELECT id FROM Catalog WHERE article = ? AND id != ?', + (article, product_id) + ).fetchone() + if existing: + flash('Товар с таким артикулом уже существует', 'error') + conn.close() + return redirect(url_for('edit_product', product_id=product_id)) + + conn.execute(''' + UPDATE Catalog + SET product_name = ?, article = ?, category_id = ?, material_id = ?, + description = ?, min_price = ?, quantity_in_stock = ?, + production_time_days = ? + WHERE id = ? + ''', (product_name, article, category_id, material_id, description, min_price, + quantity_in_stock, production_time_days, product_id)) + + conn.commit() + conn.close() + + flash('Товар успешно обновлен!', 'success') + return redirect(url_for('catalog')) + + except Exception as e: + flash(f'Ошибка: {str(e)}', 'error') + return redirect(url_for('edit_product', product_id=product_id)) + + product_row = conn.execute(''' + SELECT cat.*, c.category_name, m.material_name + FROM Catalog cat + LEFT JOIN Categories c ON cat.category_id = c.id + LEFT JOIN Materials m ON cat.material_id = m.id + WHERE cat.id = ? + ''', (product_id,)).fetchone() + + product_dict = dict_from_row(product_row) + + categories = conn.execute('SELECT * FROM Categories ORDER BY category_name').fetchall() + materials = conn.execute('SELECT * FROM Materials ORDER BY material_name').fetchall() + conn.close() + + if not product_dict: + flash('Товар не найден', 'error') + return redirect(url_for('catalog')) + + return render_template('edit_product.html', + product=product_dict, + categories=categories, + materials=materials) + +@app.route('/delete_product/', methods=['POST']) +def delete_product(product_id): + try: + conn = get_db_connection() + + order_items = conn.execute('SELECT COUNT(*) as count FROM OrderItems WHERE product_id = ?', (product_id,)).fetchone() + if order_items['count'] > 0: + flash('Нельзя удалить товар, по которому есть заказы', 'error') + conn.close() + return redirect(url_for('catalog')) + + conn.execute('DELETE FROM Catalog WHERE id = ?', (product_id,)) + conn.commit() + conn.close() + + flash('Товар успешно удален', 'success') + except Exception as e: + flash(f'Ошибка при удалении: {str(e)}', 'error') + + return redirect(url_for('catalog')) + +@app.route('/catalog') +def catalog(): + conn = get_db_connection() + + categories = conn.execute('SELECT * FROM Categories ORDER BY category_name').fetchall() + # Получаем список всех клиентов для выпадающего списка + clients_rows = conn.execute('SELECT id, name FROM Clients ORDER BY name').fetchall() + clients = list_from_rows(clients_rows) + + search_query = request.args.get('search', '') + category_id = request.args.get('category', 'all') + sort_by = request.args.get('sort', 'name_asc') + + sql = ''' + SELECT cat.*, c.category_name, m.material_name, m.color, + m.production_time_days, m.daily_capacity + FROM Catalog cat + LEFT JOIN Categories c ON cat.category_id = c.id + LEFT JOIN Materials m ON cat.material_id = m.id + WHERE 1=1 + ''' + params = [] + + if search_query: + sql += " AND (cat.product_name LIKE ? OR cat.article LIKE ?)" + params.extend([f'%{search_query}%', f'%{search_query}%']) + + if category_id != 'all': + sql += " AND cat.category_id = ?" + params.append(category_id) + + if sort_by == 'name_asc': + sql += " ORDER BY cat.product_name ASC" + elif sort_by == 'name_desc': + sql += " ORDER BY cat.product_name DESC" + elif sort_by == 'price_asc': + sql += " ORDER BY cat.min_price ASC" + elif sort_by == 'price_desc': + sql += " ORDER BY cat.min_price DESC" + elif sort_by == 'stock_desc': + sql += " ORDER BY cat.quantity_in_stock DESC" + else: + sql += " ORDER BY cat.product_name ASC" + + products_rows = conn.execute(sql, params).fetchall() + products = list_from_rows(products_rows) + + cart = session.get('cart', {}) + cart_count = sum(item['quantity'] for item in cart.values()) if cart else 0 + + conn.close() + + return render_template('catalog.html', + products=products, + categories=categories, + clients=clients, + cart_count=cart_count) + +@app.route('/product/') +def product_detail(product_id): + conn = get_db_connection() + + product_row = conn.execute(''' + SELECT cat.*, c.category_name, m.material_name, + m.price_per_unit, m.unit, m.in_stock as material_stock, + m.daily_capacity as material_capacity, + m.production_time_days as material_production_time, + m.cost_price as material_cost_price + FROM Catalog cat + LEFT JOIN Categories c ON cat.category_id = c.id + LEFT JOIN Materials m ON cat.material_id = m.id + WHERE cat.id = ? + ''', (product_id,)).fetchone() + + product_dict = dict_from_row(product_row) + + material = None + if product_dict and product_dict.get('material_id'): + material_row = conn.execute('SELECT * FROM Materials WHERE id = ?', (product_dict['material_id'],)).fetchone() + material = dict_from_row(material_row) + + orders_stats_row = conn.execute(''' + SELECT + COUNT(DISTINCT oi.order_id) as total_orders, + SUM(oi.quantity) as total_sold, + AVG(oi.quantity) as avg_order_size + FROM OrderItems oi + WHERE oi.product_id = ? + ''', (product_id,)).fetchone() + + orders_stats_dict = dict_from_row(orders_stats_row) + + conn.close() + + if not product_dict: + flash('Товар не найден', 'error') + return redirect(url_for('catalog')) + + return render_template('product_detail.html', + product=product_dict, + material=material, + orders_stats=orders_stats_dict) + +# ========== МАРШРУТЫ ДЛЯ КОРЗИНЫ ========== + +@app.route('/cart') +def view_cart(): + cart = session.get('cart', {}) + cart_items = [] + total_amount = 0 + + conn = get_db_connection() + for key, item_data in cart.items(): + product_id = item_data['product_id'] + quantity = item_data['quantity'] + client_id = item_data['client_id'] + + # Получаем информацию о товаре + product_row = conn.execute(''' + SELECT cat.*, c.category_name, m.material_name, cat.quantity_in_stock + FROM Catalog cat + LEFT JOIN Categories c ON cat.category_id = c.id + LEFT JOIN Materials m ON cat.material_id = m.id + WHERE cat.id = ? + ''', (product_id,)).fetchone() + + # Получаем информацию о партнере + client_row = conn.execute('SELECT name FROM Clients WHERE id = ?', (client_id,)).fetchone() + client_name = client_row['name'] if client_row else 'Неизвестный' + + if product_row: + product = dict_from_row(product_row) + cart_items.append({ + 'id': key, + 'product_id': product_id, + 'product_name': product['product_name'], + 'article': product['article'], + 'category_name': product['category_name'], + 'material_name': product['material_name'], + 'client_id': client_id, + 'client_name': client_name, + 'price': product['min_price'], + 'quantity': quantity, + 'in_stock': product['quantity_in_stock'] or 0 + }) + total_amount += product['min_price'] * quantity + + conn.close() + + return render_template('cart_new.html', cart_items=cart_items, total_amount=total_amount) + +@app.route('/add_to_cart/', methods=['POST']) +def add_to_cart(product_id): + quantity = int(request.form.get('quantity', 1)) + client_id = request.form.get('client_id') + + if not client_id: + flash('Выберите партнера', 'error') + return redirect(url_for('catalog')) + + cart = session.get('cart', {}) + + # Проверяем, что все товары в корзине от одного партнера + if cart: + existing_client = next(iter(cart.values()))['client_id'] + if existing_client != int(client_id): + flash('Нельзя добавлять товары разных партнеров в одну корзину', 'error') + return redirect(url_for('catalog')) + + # Создаем уникальный ключ для комбинации товар + партнер + key = f"{product_id}_{client_id}" + + if key in cart: + cart[key]['quantity'] += quantity + else: + cart[key] = { + 'product_id': product_id, + 'client_id': int(client_id), + 'quantity': quantity + } + + session['cart'] = cart + flash('Товар добавлен в корзину', 'success') + return redirect(url_for('catalog')) + +@app.route('/update_cart/', methods=['POST']) +def update_cart(item_id): + quantity = int(request.form.get('quantity', 1)) + + cart = session.get('cart', {}) + + if quantity > 0 and item_id in cart: + cart[item_id]['quantity'] = quantity + elif quantity <= 0 and item_id in cart: + cart.pop(item_id, None) + + session['cart'] = cart + + return ('', 204) + +@app.route('/remove_from_cart/', methods=['POST']) +def remove_from_cart(item_id): + cart = session.get('cart', {}) + cart.pop(item_id, None) + session['cart'] = cart + + return redirect(url_for('view_cart')) + +# ========== МАРШРУТ ДЛЯ ОФОРМЛЕНИЯ ЗАКАЗА ========== + +@app.route('/checkout', methods=['POST']) +def checkout(): + cart = session.get('cart', {}) + if not cart: + flash('Корзина пуста', 'error') + return redirect(url_for('catalog')) + + conn = get_db_connection() + + # Проверяем, что все товары от одного партнера + client_ids = set(item_data['client_id'] for item_data in cart.values()) + + if len(client_ids) > 1: + flash('Ошибка: в корзине товары от разных партнеров', 'error') + conn.close() + return redirect(url_for('view_cart')) + + client_id = list(client_ids)[0] if client_ids else None + + # Собираем все товары + items = [] + subtotal = 0 + + for key, item_data in cart.items(): + product_id = item_data['product_id'] + quantity = item_data['quantity'] + + product_row = conn.execute('SELECT * FROM Catalog WHERE id = ?', (product_id,)).fetchone() + if not product_row: + flash(f'Товар с ID {product_id} не найден', 'error') + conn.close() + return redirect(url_for('view_cart')) + + product = dict_from_row(product_row) + + if product['quantity_in_stock'] < quantity: + flash(f'Недостаточно товара "{product["product_name"]}" на складе', 'error') + conn.close() + return redirect(url_for('view_cart')) + + items.append({ + 'product': product, + 'quantity': quantity + }) + subtotal += product['min_price'] * quantity + + try: + # Рассчитываем наценку 15% + 10% = 25% + assembly_percent = 15 + delivery_percent = 10 + assembly_cost = subtotal * assembly_percent / 100 + delivery_cost = subtotal * delivery_percent / 100 + total_amount = subtotal + assembly_cost + delivery_cost + + # Срок изготовления (максимальный из всех товаров) + production_days = max(item['product']['production_time_days'] or 1 for item in items) + today = datetime.now().strftime('%Y-%m-%d') + end_date = (datetime.now() + timedelta(days=production_days)).strftime('%Y-%m-%d') + + # Создаем ОДИН заказ + cursor = conn.execute(''' + INSERT INTO Orders ( + client_id, order_date, total_amount, status, + production_days, production_start_date, production_end_date, production_status, + assembly_percent, delivery_percent, assembly_cost, delivery_cost, base_amount + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + client_id, today, total_amount, 'Новый', + production_days, today, end_date, 'Ожидает', + assembly_percent, delivery_percent, assembly_cost, delivery_cost, subtotal + )) + + order_id = cursor.lastrowid + + # Добавляем все товары в OrderItems + for item in items: + product = item['product'] + quantity = item['quantity'] + + conn.execute(''' + INSERT INTO OrderItems (order_id, product_id, quantity, price_at_moment) + VALUES (?, ?, ?, ?) + ''', (order_id, product['id'], quantity, product['min_price'])) + + conn.execute(''' + UPDATE Catalog + SET quantity_in_stock = quantity_in_stock - ? + WHERE id = ? + ''', (quantity, product['id'])) + + conn.commit() + session.pop('cart', None) + flash(f'✅ Заказ успешно оформлен! Сумма: {total_amount:.2f} руб.', 'success') + + except Exception as e: + conn.rollback() + flash(f'Ошибка при создании заказа: {str(e)}', 'error') + + conn.close() + return redirect(url_for('orders_list')) + +# ========== МАРШРУТ ДЛЯ СПИСКА ВСЕХ ЗАКАЗОВ ========== + +@app.route('/orders_list') +def orders_list(): + """Список всех заказов""" + conn = get_db_connection() + + clients_rows = conn.execute('SELECT id, name FROM Clients ORDER BY name').fetchall() + clients_list = list_from_rows(clients_rows) + + # Получаем заказы с основной информацией + orders_rows = conn.execute(''' + SELECT + o.*, + c.name as client_name, + c.inn as client_inn, + COUNT(oi.id) as items_count, + COALESCE(SUM(oi.quantity), 0) as total_items + FROM Orders o + LEFT JOIN Clients c ON o.client_id = c.id + LEFT JOIN OrderItems oi ON o.id = oi.order_id + GROUP BY o.id + ORDER BY o.order_date DESC, o.id DESC + LIMIT 100 + ''').fetchall() + + # Для каждого заказа получаем список товаров + orders_with_items = [] + for order_row in orders_rows: + order_dict = dict_from_row(order_row) + + # Получаем товары для этого заказа + items_rows = conn.execute(''' + SELECT oi.*, cat.product_name, cat.article, cat.min_price, cat.category_id, + cat2.category_name + FROM OrderItems oi + JOIN Catalog cat ON oi.product_id = cat.id + LEFT JOIN Categories cat2 ON cat.category_id = cat2.id + WHERE oi.order_id = ? + ''', (order_dict['id'],)).fetchall() + + order_dict['items'] = list_from_rows(items_rows) + orders_with_items.append(order_dict) + + # Считаем общую статистику + total_stats_row = conn.execute(''' + SELECT + COUNT(DISTINCT o.id) as total_orders, + COALESCE(SUM(oi.quantity), 0) as total_quantity, + COALESCE(SUM(o.total_amount), 0) as total_amount + FROM Orders o + LEFT JOIN OrderItems oi ON o.id = oi.order_id + ''').fetchone() + + total_stats = dict_from_row(total_stats_row) + + conn.close() + + return render_template('orders_list.html', + orders=orders_with_items, + total_stats=total_stats, + clients=clients_list) + +# ========== МАРШРУТ ДЛЯ МАТЕРИАЛОВ ========== + +@app.route('/materials') +def materials(): + conn = get_db_connection() + + materials_rows = conn.execute(''' + SELECT * FROM Materials + ORDER BY material_name + ''').fetchall() + + materials_list = list_from_rows(materials_rows) + conn.close() + + return render_template('materials.html', materials=materials_list) + +# ========== СЛУЖЕБНЫЕ МАРШРУТЫ ========== + +@app.route('/favicon.ico') +def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + +@app.route('/static/') +def static_files(filename): + return send_from_directory(app.root_path + '/static', filename) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/partners.db b/partners.db new file mode 100644 index 0000000..0f810b9 Binary files /dev/null and b/partners.db differ diff --git a/static/OfficeStyle.png b/static/OfficeStyle.png new file mode 100644 index 0000000..170e5cf Binary files /dev/null and b/static/OfficeStyle.png differ diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..24fb2a2 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,419 @@ +/* Подключаем шрифт Franklin Gothic */ +@import url('https://fonts.googleapis.com/css2?family=Franklin+Gothic:wght@400;500;600;700&display=swap'); + +body { + font-family: 'Franklin Gothic', 'Arial Narrow', Arial, sans-serif; + background-color: #F0F8FF; /* Алиса синий */ + margin: 0; + padding: 0; +} + +.main-container { + background-color: #ffffff; + min-height: 100vh; + padding: 20px; +} + +.header { + background: linear-gradient(135deg, #4682B4 0%, #B0C4DE 100%); /* Стальной синий к светло-стальному */ + padding: 25px; + border-radius: 12px; + margin-bottom: 35px; + color: white; + box-shadow: 0 6px 20px rgba(70, 130, 180, 0.3); +} + +.logo-container { + display: flex; + align-items: center; + gap: 25px; +} + +.logo { + height: 70px; + width: auto; + object-fit: contain; + filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.2)); +} + +.header-title { + font-size: 2.5em; + font-weight: 700; + margin: 0; + text-shadow: 2px 2px 6px rgba(0,0,0,0.3); + letter-spacing: 0.5px; +} + +.page-title { + color: #2c3e50; + font-weight: 700; + border-bottom: 3px solid #4682B4; + padding-bottom: 10px; + margin-bottom: 25px; +} + +.partner-card { + border: 3px solid #B0C4DE; /* Светло-стальной */ + border-radius: 15px; + padding: 25px; + margin-bottom: 25px; + background: #ffffff; + box-shadow: 0 6px 20px rgba(176, 196, 222, 0.25); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.partner-card:hover { + transform: translateY(-5px); + box-shadow: 0 12px 25px rgba(176, 196, 222, 0.35); +} + +.partner-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 3px solid #e9ecef; + padding-bottom: 18px; + margin-bottom: 20px; +} + +.partner-name-line { + display: flex; + align-items: center; + gap: 15px; + flex-wrap: wrap; +} + +.partner-type { + font-size: 1.2em; + font-weight: 600; + color: #4682B4; /* Стальной синий */ + background: linear-gradient(135deg, #F0F8FF 0%, #E6F2FF 100%); /* Градиент алиса синий */ + padding: 8px 18px; + border-radius: 25px; + border: 2px solid #B0C4DE; +} + +.partner-name { + font-size: 1.6em; + font-weight: 700; + color: #2c3e50; + margin: 0; +} + +.divider { + color: #B0C4DE; /* Светло-стальной */ + font-weight: bold; + font-size: 1.5em; +} + +.discount-info { + display: flex; + align-items: center; + gap: 20px; +} + +.discount-badge { + font-size: 1.2em; + padding: 10px 20px; + border-radius: 30px; + background: linear-gradient(135deg, #4682B4 0%, #5A8EC6 100%); + color: white; + font-weight: 600; + box-shadow: 0 3px 10px rgba(70, 130, 180, 0.3); +} + +.sales-amount { + font-size: 1.1em; + color: #4682B4; /* Стальной синий */ + font-weight: 600; + background-color: #F0F8FF; /* Алиса синий */ + padding: 8px 15px; + border-radius: 8px; +} + +.partner-info-row { + margin-bottom: 0; + padding: 8px 0; + line-height: 1.5; + border-bottom: 1px dashed #E6F2FF; +} + +.partner-info-row:last-child { + border-bottom: none; +} + +.partner-label { + font-weight: 700; + color: #4682B4; /* Стальной синий */ + min-width: 140px; + display: inline-block; + font-size: 1.05em; +} + +.partner-value { + font-size: 1.05em; + color: #2c3e50; + font-weight: 500; +} + +.partner-buttons-container { + display: flex; + gap: 15px; + justify-content: flex-start; + margin-top: 25px; + padding-top: 20px; + border-top: 3px solid #E6F2FF; +} + +.btn-action { + padding: 12px 25px; + font-size: 1em; + display: flex; + align-items: center; + gap: 10px; + min-width: 200px; + justify-content: center; + border: none; + border-radius: 10px; + font-weight: 700; + transition: all 0.3s ease; + cursor: pointer; + letter-spacing: 0.5px; +} + +.btn-edit { + background: linear-gradient(135deg, #4682B4 0%, #5A8EC6 100%); + color: white; +} + +.btn-add { + background: linear-gradient(135deg, #6c757d 0%, #868e96 100%); + color: white; +} + +.btn-history { + background: linear-gradient(135deg, #495057 0%, #6c757d 100%); + color: white; +} + +.btn-action:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(0,0,0,0.25); + color: white; +} + +.btn-new-partner { + background: linear-gradient(135deg, #4682B4 0%, #5A8EC6 100%); + color: white; + border: none; + padding: 14px 28px; + font-weight: 700; + border-radius: 10px; + font-size: 1.1em; + letter-spacing: 0.5px; +} + +.btn-new-partner:hover { + background: linear-gradient(135deg, #5A8EC6 0%, #6B9BD4 100%); + color: white; + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(70, 130, 180, 0.3); +} + +.rating-badge { + font-size: 1em; + padding: 8px 16px; + border-radius: 20px; + background: linear-gradient(135deg, #FFD700 0%, #FFC400 100%); + color: #2c3e50; + font-weight: 700; +} + +.alert { + border-radius: 12px; + border: none; + background: linear-gradient(135deg, #F0F8FF 0%, #E6F2FF 100%); + border-left: 6px solid #4682B4; +} + +.volume-info { + background: linear-gradient(135deg, #F0F8FF 0%, #E6F2FF 100%); + padding: 12px 20px; + border-radius: 10px; + margin-top: 15px; + border: 2px solid #B0C4DE; +} + +.volume-label { + font-weight: 700; + color: #4682B4; /* Стальной синий */ + font-size: 1.1em; +} + +.volume-value { + color: #2c3e50; + font-weight: 700; + font-size: 1.2em; +} + +/* Стили для страницы истории продаж мебели */ +.table-responsive { + border-radius: 12px; + overflow: hidden; + border: 3px solid #B0C4DE; + box-shadow: 0 6px 20px rgba(176, 196, 222, 0.3); +} + +.table { + margin-bottom: 0; + border-collapse: separate; + border-spacing: 0; +} + +.table th { + background: linear-gradient(135deg, #4682B4 0%, #5A8EC6 100%); + color: white; + font-weight: 700; + padding: 16px; + text-align: center; + font-size: 1.1em; + border: none; +} + +.table td { + padding: 14px; + text-align: center; + vertical-align: middle; + border-bottom: 2px solid #E6F2FF; + font-size: 1.05em; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(176, 196, 222, 0.08); +} + +.table-striped tbody tr:hover { + background-color: rgba(70, 130, 180, 0.15); + transition: background-color 0.3s ease; +} + +.table tfoot { + background: linear-gradient(135deg, #F0F8FF 0%, #E6F2FF 100%); + font-weight: 700; + font-size: 1.1em; +} + +.table tfoot td { + border-top: 3px solid #4682B4; + padding: 18px; +} + +.text-end { + text-align: right !important; +} + +/* Стили для иконок в кнопках */ +.btn-action i { + font-size: 1.2em; +} + +/* Стили для блока с расчетом стоимости */ +.calculation-block { + background: linear-gradient(135deg, #F0F8FF 0%, #E6F2FF 100%); + border: 3px solid #B0C4DE; + border-radius: 15px; + padding: 25px; + margin-bottom: 30px; + box-shadow: 0 6px 20px rgba(176, 196, 222, 0.25); +} + +.calculation-title { + color: #4682B4; + font-weight: 700; + border-bottom: 3px solid #4682B4; + padding-bottom: 15px; + margin-bottom: 25px; +} + +.calculation-result { + background-color: white; + border: 2px solid #B0C4DE; + border-radius: 10px; + padding: 20px; + margin-top: 20px; +} + +.result-item { + padding: 10px 0; + border-bottom: 1px dashed #E6F2FF; +} + +.result-item:last-child { + border-bottom: none; + font-weight: 700; + font-size: 1.2em; + color: #4682B4; +} + +/* Адаптивность для таблицы */ +@media (max-width: 768px) { + .table-responsive { + font-size: 0.9em; + } + + .table th, .table td { + padding: 10px 6px; + } + + .btn-action { + min-width: 150px; + padding: 10px 15px; + font-size: 0.9em; + } + + .partner-card { + padding: 20px; + } + + .header-title { + font-size: 1.8em; + } +} + +/* Стили для фильтров и сортировки */ +.filter-section { + background: linear-gradient(135deg, #F0F8FF 0%, #E6F2FF 100%); + border: 2px solid #B0C4DE; + border-radius: 12px; + padding: 20px; + margin-bottom: 25px; +} + +.filter-title { + color: #4682B4; + font-weight: 700; + margin-bottom: 15px; +} + +.form-control, .form-select { + border: 2px solid #B0C4DE; + border-radius: 8px; + padding: 10px 15px; + font-size: 1em; + transition: all 0.3s ease; +} + +.form-control:focus, .form-select:focus { + border-color: #4682B4; + box-shadow: 0 0 0 0.25rem rgba(70, 130, 180, 0.25); +} + +/* Анимации */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.partner-card, .table-responsive, .calculation-block { + animation: fadeIn 0.5s ease-out; +} \ No newline at end of file diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..170e5cf Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..f5b0daa --- /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/templates/about.html b/templates/about.html new file mode 100644 index 0000000..77fdb9f --- /dev/null +++ b/templates/about.html @@ -0,0 +1,18 @@ + + + + + About Flask + + + {% extends "template.html" %} + {% block content %} + +

About Flask

+

Flask is a micro web framework written in Python.

+

Applications that use the Flask framework include Pinterest, + LinkedIn, and the community web page for Flask itself.

+ + {% endblock %} + + \ No newline at end of file diff --git a/templates/add_client.html b/templates/add_client.html new file mode 100644 index 0000000..1470391 --- /dev/null +++ b/templates/add_client.html @@ -0,0 +1,378 @@ +{% extends "base.html" %} + +{% block title %}Добавление клиента{% endblock %} + +{% block content %} +
+
+
+
+
+

+ Добавление нового клиента +

+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ +
+
Основная информация
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
От 0.0 до 5.0
+
+
+ + +
+
Контактная информация
+ +
+ + +
+ +
+ + +
+ +
+ + +
Только цифры, 10 или 12 цифр
+
+ +
+ + +
+
+
+ +
+
+
+ + Назад к списку + + +
+
+
+
+
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/add_product.html b/templates/add_product.html new file mode 100644 index 0000000..b083f31 --- /dev/null +++ b/templates/add_product.html @@ -0,0 +1,223 @@ +{% extends "base.html" %} + +{% block title %}Добавление товара{% endblock %} + +{% 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..0884976 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,620 @@ + + + + + + {% block title %}OfficeStyle - Офисная мебель{% endblock %} + + + + + + + + + + + + + + + + + + + + +
+
+ {% block header %} + {% include 'components/header.html' %} + {% endblock %} + + + {% if request.endpoint in ['index', 'orders_list', 'catalog'] %} + + {% endif %} + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} + + {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %} + {% endblock %} +
+
+ + + + + + + + + + + + + + {% block scripts %} + {% endblock %} + + \ No newline at end of file diff --git a/templates/cart_new.html b/templates/cart_new.html new file mode 100644 index 0000000..971c558 --- /dev/null +++ b/templates/cart_new.html @@ -0,0 +1,434 @@ +{% extends "base.html" %} + +{% block title %}Моя корзина{% endblock %} + +{% block content %} +
+
+

Моя корзина

+ + Продолжить покупки + +
+ + {% if cart_items %} + +
+ + + + + + + + + + + + + + + {% for item in cart_items %} + {% set item_total = item.price * item.quantity %} + + + + + + + + + + + {% endfor %} + +
ТоварАртикулКатегорияМатериалЦенаКоличествоСуммаДействия
{{ item.product_name }}{{ item.article }}{{ item.category_name }}{{ item.material_name or '-' }}{{ item.price|int }} руб. +
+ + + + +
+
{{ item_total|int }} руб. + +
+
+ + +
+
+
+
+
Итого по заказу
+
+
+ {% set ns = namespace(subtotal=0) %} + {% for item in cart_items %} + {% set ns.subtotal = ns.subtotal + (item.price * item.quantity) %} + {% endfor %} + + {% set assembly_percent = 15 %} + {% set delivery_percent = 10 %} + {% set assembly_amount = ns.subtotal * assembly_percent // 100 %} + {% set delivery_amount = ns.subtotal * delivery_percent // 100 %} + {% set total = ns.subtotal + assembly_amount + delivery_amount %} + +
+
Сумма без услуг:
+
{{ ns.subtotal|int }} руб.
+
+
+
Сборка ({{ assembly_percent }}%):
+
+{{ assembly_amount|int }} руб.
+
+
+
Доставка ({{ delivery_percent }}%):
+
+{{ delivery_amount|int }} руб.
+
+
+
+
ИТОГО:
+
{{ total|int }} руб.
+
+
+
+
+
+ + +
+
+ +
+
+ + {% else %} + +
+ +

Корзина пуста

+

Добавьте товары в корзину, чтобы оформить заказ

+ + Перейти в каталог + +
+ {% endif %} +
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/catalog.html b/templates/catalog.html new file mode 100644 index 0000000..ab4ba6d --- /dev/null +++ b/templates/catalog.html @@ -0,0 +1,564 @@ +{% extends "base.html" %} + +{% block title %}Каталог товаров{% endblock %} + +{% block content %} + + + + +
+

Поиск и сортировка

+
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + + {% if products %} + {% for product in products %} +
+
+
+
+
{{ product.product_name }}
+ Артикул: {{ product.article }} +
+ + + +
+
+
+
+
+
+ Категория: + {{ product.category_name }} +
+ +
+ Материал: + + {% if product.material_name %} + {{ product.material_name }} + {% else %} + Не указан + {% endif %} + +
+ +
+ Время изготовления: + {{ product.production_time_days or 1 }} дн. +
+ + {% if product.description %} +
+ Описание: + {{ product.description }} +
+ {% endif %} +
+
+
+ В наличии: + + {% if product.quantity_in_stock and product.quantity_in_stock > 0 %} + {{ product.quantity_in_stock }} ед. + {% else %} + Нет в наличии + {% endif %} + +
+ +
+ Цена: + {{ product.min_price|int }} руб. +
+
+
+ + + +
+
+ {% endfor %} + + +
+
+

+ + Найдено товаров: {{ products|length }} + {% if request.args.get('search') %} + по запросу "{{ request.args.get('search') }}" + {% endif %} + {% if request.args.get('category') and request.args.get('category') != 'all' %} + {% for category in categories %} + {% if category.id == request.args.get('category')|int %} + , категория: {{ category.category_name }} + {% endif %} + {% endfor %} + {% endif %} +

+
+
+ + {% else %} +
+ + {% if request.args.get('search') or request.args.get('category') != 'all' %} + Ничего не найдено по вашему запросу. +
Сбросить все фильтры + {% else %} + В каталоге нет товаров. Добавьте первый товар. + {% endif %} +
+ {% endif %} + + + + + +{% 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..77c8b09 --- /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..849d8fc --- /dev/null +++ b/templates/components/partner_card.html @@ -0,0 +1,244 @@ +
+
+ +
+
+ + {% if partner.contractor_type_name %} + {{ partner.contractor_type_name }} + {% else %} + Тип не указан + {% endif %} + + | + {{ partner.name }} +
+ +
+ + {{ 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_client.html b/templates/edit_client.html new file mode 100644 index 0000000..a342280 --- /dev/null +++ b/templates/edit_client.html @@ -0,0 +1,396 @@ +{% extends "base.html" %} + +{% block title %}Редактирование клиента{% endblock %} + +{% block content %} +
+
+
+
+
+

+ Редактирование клиента +

+
+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ +
+
Основная информация
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
От 0.0 до 5.0
+
+
+ + +
+
Контактная информация
+ +
+ + +
+ +
+ + +
+ +
+ + +
10 или 12 цифр
+
+ +
+ + +
+
+
+ +
+
+
+ + Назад к списку + +
+ + История заказов + + +
+
+
+
+
+
+
+
+
+
+ + + +{% block scripts %} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/edit_product.html b/templates/edit_product.html new file mode 100644 index 0000000..ccbeca2 --- /dev/null +++ b/templates/edit_product.html @@ -0,0 +1,279 @@ +{% extends "base.html" %} + +{% block title %}Редактирование товара{% endblock %} + +{% block content %} +
+
+
+
+
+

+ Редактирование товара +

+
+
+ + {% if client %} +
+ Редактирование товара партнера: + {{ client.name }} + +
+ {% endif %} + + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ + + Только буквы +
+ +
+ + + Не изменяется +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+ + + Сколько дней нужно на производство +
+ +
+ + +
+ +
+ + +
+
+ +
+
+
+ + Назад + + +
+
+
+
+
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..085f57c --- /dev/null +++ b/templates/home.html @@ -0,0 +1,16 @@ + + + + + Flask Tutorial + + + {% extends "template.html" %} + {% block content %} + +

My First Try Using Flask

+

Flask is Fun

+ + {% endblock %} + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..083953a --- /dev/null +++ b/templates/index.html @@ -0,0 +1,238 @@ +{% extends "base.html" %} + +{% block title %}Партнеры офисной мебели{% endblock %} + +{% block content %} + +
+

Партнеры офисной мебели

+ + Новый партнер + +
+ + +
+

Поиск и сортировка

+
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + + {% if partners %} +
+ {% for partner in partners %} + {% include 'components/partner_card.html' %} + {% endfor %} +
+ + +
+
+

+ + Найдено партнеров: {{ partners|length }} + {% if request.args.get('search') %} + по запросу "{{ request.args.get('search') }}" + {% endif %} + {% if request.args.get('type') and request.args.get('type') != 'all' %} + , тип: {{ request.args.get('type') }} + {% endif %} + {% if request.args.get('rating') and request.args.get('rating') != '0' %} + , рейтинг ≥ {{ request.args.get('rating') }} + {% endif %} +

+
+
+ {% else %} +
+ + {% if request.args.get('search') or request.args.get('type') != 'all' or request.args.get('rating') != '0' %} + Ничего не найдено по вашему запросу. +
Сбросить все фильтры + {% else %} + Нет данных о партнере. Добавьте первого партнера. + {% endif %} +
+ {% endif %} + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/materials.html b/templates/materials.html new file mode 100644 index 0000000..f959b22 --- /dev/null +++ b/templates/materials.html @@ -0,0 +1,266 @@ +{% extends "base.html" %} + +{% block title %}Материалы{% endblock %} + +{% block content %} + +
+

Материалы

+ + Всего: {{ materials|length }} + +
+ + + {% if materials %} +
+ {% for material in materials %} +
+
+
+
+ {{ material.material_name }} +
+
+
+ {% if material.description %} +

{{ material.description }}

+ {% endif %} + +
+ {% if material.category %} +
+ Категория: + {{ material.category }} +
+ {% endif %} + + {% if material.color %} +
+ Цвет: + {{ material.color }} +
+ {% endif %} + + {% if material.density %} +
+ Плотность: + {{ material.density }} кг/м³ +
+ {% endif %} + + {% if material.thickness %} +
+ Толщина: + {{ material.thickness }} мм +
+ {% endif %} + + {% if material.supplier %} +
+ Поставщик: + {{ material.supplier }} +
+ {% endif %} + + {% if material.country %} +
+ Страна: + {{ material.country }} +
+ {% endif %} + + {% if material.wear_class %} +
+ Класс износа: + {{ material.wear_class }} +
+ {% endif %} +
+ +
+ {% if material.price_per_unit %} +
+ Цена: + {{ material.price_per_unit|int }} руб./{{ material.unit or 'шт' }} +
+ {% endif %} + +
+ В наличии: + + {{ material.in_stock or 0 }} {{ material.unit or 'шт' }} + +
+
+ + +
+
+
+ {% endfor %} +
+ {% else %} +
+ +

Нет материалов

+

В базе данных пока нет материалов. Добавьте материалы через импорт данных.

+
+ {% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/order_history.html b/templates/order_history.html new file mode 100644 index 0000000..87e99d1 --- /dev/null +++ b/templates/order_history.html @@ -0,0 +1,465 @@ +{% extends "base.html" %} + +{% block title %}История заказов - {{ client.name }}{% endblock %} + +{% block content %} +
+ +
+
+ + Назад + +

История заказов - {{ client.name }}

+
+
+ + +
+
+
+ + {% if client.contractor_type_name %} + {{ client.contractor_type_name }} + {% else %} + Тип не указан + {% endif %} + + | + {{ client.name }} +
+
+ +
+
+
+
+ Директор: + {{ client.director or 'Не указан' }} +
+
+ Телефон: + {{ client.phone or 'Не указан' }} +
+
+
+
+ Email: + {{ client.email or 'Не указан' }} +
+
+ ИНН: + {{ client.inn or 'Не указан' }} +
+
+
+
+
+ + +
+

Общая статистика

+
+
+
+
{{ total_stats.total_orders or 0 }}
+
Всего заказов
+
+
+
+
+
{{ total_stats.total_quantity or 0 }}
+
Общее количество
+
+
+
+
+
{{ "{:,.2f}".format(total_stats.total_amount or 0) }} руб.
+
Общая сумма
+
+
+
+
+
{{ total_stats.completed_orders or 0 }}
+
Выполненных
+
+
+
+
+ + +
+

Детализация заказов

+ + {% if orders %} +
+ {% for order in orders %} +
+

+ +

+
+
+ +
+
+

Дата заказа: {{ order.order_date[:10] }}

+

Срок изготовления: {{ order.production_days or 1 }} дн.

+
+
+

Дата готовности: {{ order.production_end_date[:10] if order.production_end_date else '-' }}

+
+
+ + +
Состав заказа:
+
+ + + + + + + + + + + + {% for item in order['items'] %} + {% set item_sum = item['price_at_moment'] * item['quantity'] %} + + + + + + + + {% endfor %} + +
ТоварАртикулКоличествоЦена за ед.Сумма
{{ item['product_name'] }}{{ item['article'] }}{{ "{:,}".format(item['quantity']) }} ед.{{ "{:,.2f}".format(item['price_at_moment']) }} руб.{{ "{:,.2f}".format(item_sum) }} руб.
+
+ + +
+
+
+
+ Базовая сумма товаров: + {{ "{:,.2f}".format(order.base_amount) }} руб. +
+
+ Сборка ({{ order.assembly_percent }}%): + +{{ "{:,.2f}".format(order.assembly_cost) }} руб. +
+
+ Доставка ({{ order.delivery_percent }}%): + +{{ "{:,.2f}".format(order.delivery_cost) }} руб. +
+
+ ИТОГОВАЯ СУММА ЗАКАЗА: + {{ "{:,.2f}".format(order.total_amount) }} руб. +
+
+
+
+ + +
+
+
+ + +
+
+
+
+
+
+ {% endfor %} +
+ {% else %} +
+ Нет данных о заказах +
+ {% endif %} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/orders_list.html b/templates/orders_list.html new file mode 100644 index 0000000..160687a --- /dev/null +++ b/templates/orders_list.html @@ -0,0 +1,316 @@ +{% extends "base.html" %} + +{% block title %}Список заказов{% endblock %} + +{% block content %} + +
+

Список заказов

+
+ + +
+
+
+
+
Всего заказов
+

{{ total_stats.total_orders or 0 }}

+
+
+
+
+
+
+
Всего единиц
+

{{ "{:,}".format(total_stats.total_quantity or 0) }}

+
+
+
+
+ +
+
+
Общая сумма
+

{{ "{:,.2f}".format(total_stats.total_amount or 0) }} руб.

+
+
+
+
+ + + {% if orders %} +
+ + + + + + + + + + + + + + + + {% for order in orders %} + {% set markup = order.assembly_cost + order.delivery_cost %} + + + + + + + + + + + + {% endfor %} + +
IDДатаКлиентИННСостав заказаВсего единицНаценка (25%)ИтогоСтатус
{{ order.id }}{{ order.order_date[:10] if order.order_date else '-' }} + {% if order.client_id %} + + {{ order.client_name or 'Неизвестный клиент' }} + + {% else %} + {{ order.client_name or 'Общий заказ' }} + {% endif %} + {{ order.client_inn or '-' }} + {% if order['items'] and order['items']|length > 0 %} + +
+
+ + + + + + + + + + + + {% for item in order['items'] %} + {% set item_sum = item.price_at_moment * item.quantity %} + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + + +
ТоварАртикулКол-воЦена за ед.Сумма
{{ item.product_name }}{{ item.article }}{{ "{:,}".format(item.quantity) }} ед.{{ "{:,.2f}".format(item.price_at_moment) }} руб.{{ "{:,.2f}".format(item_sum) }} руб.
Базовая сумма:{{ "{:,.2f}".format(order.base_amount) }} руб.
Сборка ({{ order.assembly_percent }}%):+{{ "{:,.2f}".format(order.assembly_cost) }} руб.
Доставка ({{ order.delivery_percent }}%):+{{ "{:,.2f}".format(order.delivery_cost) }} руб.
ИТОГО:{{ "{:,.2f}".format(order.total_amount) }} руб.
+
+
+ {% else %} + Нет товаров + {% endif %} +
{{ "{:,}".format(order.total_items or 0) }} ед.+{{ "{:,.2f}".format(markup) }} руб.{{ "{:,.2f}".format(order.total_amount) }} руб. + + {{ order.status }} + +
+
+ {% else %} +
+ + Нет данных о заказах. Вернуться на главную. +
+ {% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/product_detail.html b/templates/product_detail.html new file mode 100644 index 0000000..981d90b --- /dev/null +++ b/templates/product_detail.html @@ -0,0 +1,427 @@ +{% extends "base.html" %} + +{% block title %}{{ product.product_name }} - детальная информация{% endblock %} + +{% block content %} +
+ + + +
+ +
+
+
+
Основная информация
+
+
+ + + + + + + + + + + + + + {% if product.description %} + + + + + {% endif %} + + + + + + + + +
Артикул:{{ product.article }}
Категория:{{ product.category_name }}
Материал: + {% if product.material_name %} + {{ product.material_name }} + {% else %} + Не указан + {% endif %} +
Описание:{{ product.description }}
Базовая цена:{{ product.min_price|int }} руб.
В наличии: + {% if product.quantity_in_stock and product.quantity_in_stock > 0 %} + {{ product.quantity_in_stock }} шт. + {% else %} + Нет в наличии + {% endif %} +
+
+
+ + + {% if material %} +
+
+
Информация о материале
+
+
+ + + + + + {% if material.color %} + + + + + {% endif %} + {% if material.supplier %} + + + + + {% endif %} + + + + + + + + +
Материал:{{ material.material_name }}
Цвет:{{ material.color }}
Поставщик:{{ material.supplier }}
Цена за ед.:{{ material.price_per_unit|int }} руб./{{ material.unit }}
На складе: + {% if material.in_stock and material.in_stock > 0 %} + {{ material.in_stock }} {{ material.unit }} + {% else %} + Нет + {% endif %} +
+
+
+ {% endif %} +
+ + +
+ +
+
+
Производственные характеристики
+
+
+ + + + + + + + + + + + + + + + + +
Время изготовления: + {% if product.production_time_days %} + {{ product.production_time_days }} дн. + {% else %} + {{ material.production_time_days if material else 1 }} дн. (по умолчанию) + {% endif %} +
Производительность: + {% if product.daily_capacity %} + {{ product.daily_capacity }} шт/день + {% else %} + {{ material.daily_capacity if material else 10 }} шт/день (по умолчанию) + {% endif %} +
Себестоимость: + {% if product.cost_price %} + {{ product.cost_price|int }} руб. + {% elif material and material.price_per_unit %} + {{ (material.price_per_unit * 1.3)|int }} руб. (расчетная) + {% else %} + Не указана + {% endif %} +
Прибыль с единицы: + {% set cost = product.cost_price or (material.price_per_unit * 1.3 if material else 0) %} + {% if cost and cost > 0 %} + {{ (product.min_price - cost)|int }} руб. + ({{ ((product.min_price - cost) / product.min_price * 100)|round(1) }}%) + {% else %} + — + {% endif %} +
+
+
+ + +
+
+
Расчет стоимости со сборкой и доставкой
+
+
+
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + +
Базовая стоимость:0 руб.
Стоимость сборки (15%):0 руб.
Стоимость доставки (10%):0 руб.
ИТОГОВАЯ СТОИМОСТЬ:0 руб.
+
+
+
+ + +
+
+
Статистика продаж
+
+
+
+
+
+
{{ orders_stats.total_orders or 0 }}
+ Всего заказов +
+
+
+
+
{{ orders_stats.total_sold or 0 }}
+ Продано единиц +
+
+
+
+
{{ (orders_stats.avg_order_size or 0)|int }}
+ Средний заказ +
+
+
+
+
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file