Документация — PyQt5 + MySQL

Магазин обуви | Экзамен

Разбор функций

load_products() products_window.py
def load_products(self):
  while self.layout.count():
      item = self.layout.takeAt(0)
      if item.widget():
          item.widget().deleteLater()

  sort = self.sort_comboBox.currentText()
  if "возрастанию" in sort:
      sort_param = "ORDER BY quantity ASC"
  elif "убыванию" in sort:
      sort_param = "ORDER BY quantity DESC"
  else:
      sort_param = ""

  search_param = f"%{self.search_lineEdit.text().lower()}%"
  supplier = self.category_comboBox.currentText()

  connection = db_connect()
  try:
      with connection.cursor() as cursor:
          if supplier == "Все поставщики":
              cursor.execute(
                  f"SELECT ... WHERE lower(name) LIKE %s {sort_param}",
                  (search_param,))
          else:
              cursor.execute(
                  f"SELECT ... WHERE supplier=%s AND lower(name) LIKE %s {sort_param}",
                  (supplier, search_param))
          products = cursor.fetchall()

      for data in products:
          card = ProductCard(data)
          card.edit_request.connect(self.edit_product)
          self.layout.addWidget(card)
      self.layout.addStretch()
  finally:
      connection.close()
① Очистка layout перед перезагрузкой
takeAt(0) возвращает QLayoutItem — не виджет. Сначала берём item, потом через item.widget() достаём виджет и вызываем deleteLater(). Цикл while потому что после takeAt индексы сдвигаются.
② Сортировка через f-строку
ORDER BY нельзя передать как параметр %s — MySQL не позволяет. Поэтому собираем строку заранее и вставляем через f"...{sort_param}". Это безопасно т.к. значение задаём мы сами, не пользователь.
③ Поиск через LIKE
%текст% — найдёт где угодно в строке. .lower() приводим к нижнему регистру. В SQL используем lower(p.name) чтобы сравнение было регистронезависимым.
④ Два разных запроса
Когда фильтр "Все поставщики" — WHERE только по поиску. Когда выбран поставщик — добавляем AND supplier=%s. Нельзя один запрос с условным WHERE.
⑤ Кортеж с запятой!
(search_param,) — запятая обязательна. Без неё (search_param) — это просто скобки, не кортеж. pymysql ожидает tuple.
⑥ Создание карточек и подписка на сигналы
Для каждой строки из БД создаём карточку. connect(self.edit_product) — когда карточку нажмут, вызовется edit_product с product_id. addStretch() прижимает карточки вверх.
save() — INSERT vs UPDATE add_order_form.py
def save(self):
  status = self.status_comboBox.currentData()
  pp       = self.pp_comboBox.currentData()
  recipient= self.recipient_comboBox.currentData()
  order_date = self.order_date_dateEdit.date()
                  .toPyDate()

  if not all([status, pp, recipient]):
      QMessageBox.warning(...)
      return
  if not self.items:
      QMessageBox.warning(...)
      return

  connection = db_connect()
  try:
      with connection.cursor() as cursor:
          if self.order_id:
              cursor.execute(
                  "UPDATE orders SET ... WHERE order_number=%s",
                  (..., self.order_id))
              cursor.execute(
                  "DELETE FROM order_products WHERE order_id=%s",
                  (self.order_id,))
              order_id = self.order_id
          else:
              cursor.execute(
                  "INSERT INTO orders (...) VALUES (...)", (...))
              order_id = cursor.lastrowid

          for pid, _, qty in self.items:
              cursor.execute(
                  "INSERT INTO order_products VALUES (%s,%s,%s)",
                  (pid, order_id, qty))

      connection.commit()
      self.accept()
  except Exception as e:
      print(e)
      QMessageBox.critical(...)
  finally:
      connection.close()
① currentData() vs currentText()
currentData() — возвращает ID который мы передавали вторым аргументом в addItem(текст, id). Именно ID нужен для сохранения в БД, не текст.
② Валидация перед сохранением
not all([a, b, c]) — True если хотя бы один элемент пустой/None. return после warning прерывает функцию — до БД не доходим.
③ Ветка редактирования vs создания
self.order_id задаётся в __init__ — None при создании, число при редактировании. Этим определяем что делать: UPDATE или INSERT.
④ DELETE + INSERT вместо UPDATE order_products
При редактировании пользователь мог добавить/убрать товары. Проще удалить все старые строки и вставить новые, чем угадывать что изменилось.
⑤ cursor.lastrowid
После INSERT в orders — ID только что созданной строки. Нужен чтобы привязать order_products к этому заказу. Работает только сразу после INSERT, до commit.
⑥ Сохранение товаров заказа
self.items — список кортежей (product_id, text, qty). _ вместо имени переменной — соглашение "это значение нам не нужно". В order_products пишем одну строку на каждый товар.
load_order_data() add_order_form.py
def load_order_data(self):
  connection = db_connect()
  try:
      with connection.cursor() as cursor:
          cursor.execute(
              "SELECT ... FROM orders WHERE order_number=%s",
              (self.order_id,))
          o = cursor.fetchone()

          self.date_edit.setDate(
              QDate.fromString(str(o[0]), 'yyyy-MM-dd'))

          self.receipt_edit.setText(str(o[4]))

          for box, val in [
              (self.pp_comboBox, o[2]),
              (self.recipient_comboBox, o[3]),
              (self.status_comboBox, o[5])
          ]:
              idx = box.findData(val)
              if idx >= 0:
                  box.setCurrentIndex(idx)

          cursor.execute("""
              SELECT op.product_id,
                CONCAT_WS(' - ',p.article,p.product_name),
                op.quantity
              FROM order_products op
              JOIN products p ON op.product_id=p.product_id
              WHERE op.order_id=%s""", (self.order_id,))
          for pid, pname, qty in cursor.fetchall():
              self.items.append((pid, pname, qty))
          self.refresh_list()
  finally:
      connection.close()
① fetchone() — одна строка
Один заказ по ID — всегда одна строка. Возвращает кортеж (order_date, delivery_date, pickup_point_id, ...). Обращаемся по индексу: o[0], o[1] и т.д.
② QDate из Python date
БД возвращает datetime.date объект. str() превращает его в строку "2024-05-15". QDate.fromString(строка, формат) парсит её в QDate для виджета.
③ Цикл по комбобоксам
Вместо трёх одинаковых блоков — список пар (виджет, значение) и один цикл. Для каждого комбобокса одна и та же логика: найти индекс по ID и установить.
④ if idx >= 0, не if idx!
findData() возвращает -1 если не нашёл. Индекс 0 — первый элемент — тоже валидный. if idx: не сработает для первого элемента т.к. 0 это falsy в Python.
⑤ JOIN чтобы получить название товара
В order_products хранится только product_id. Чтобы показать название в списке — нужен JOIN с таблицей products.
⑥ Заполнение self.items
Добавляем в self.items а не напрямую в listWidget — иначе delete_item сломается (он удаляет по индексу из self.items). refresh_list() синхронизирует виджет с self.items.
try / except / finally — паттерн работы с БД все файлы
connection = None
try:
    connection = db_connect()
    with connection.cursor() as cursor:
        cursor.execute("SELECT ...")
        data = cursor.fetchall()

    connection.commit()
    self.accept()

except Exception as e:
    print(e)
    QMessageBox.critical(self, "Ошибка", "...")

finally:
    if connection:
        connection.close()
① connection = None перед try
Если db_connect() упадёт — переменная connection не будет создана. В finally if connection: проверит это и не вызовет close() на несуществующей переменной.
② db_connect() внутри try
Если соединение не установится — except поймает ошибку и покажет сообщение пользователю вместо краша.
③ with cursor as cursor:
Контекстный менеджер — автоматически закрывает курсор после блока. Все fetchall() должны быть внутри этого блока.
④ fetchall() обязательно внутри with
Если вынести за пределы with — курсор уже закрыт, данные недоступны. Сохрани результат в переменную внутри блока.
⑤ commit() после изменений
Нужен только для INSERT/UPDATE/DELETE. SELECT не требует commit(). Без commit() изменения не сохранятся в БД.
⑥ except ловит любую ошибку
Exception as e — поймает всё: ошибки БД, SQL ошибки, ошибки типов. print(e) для отладки в консоль, QMessageBox для пользователя.
⑦ finally выполняется всегда
Даже если была ошибка в except. Гарантирует что соединение закроется при любом исходе. Утечка соединений → БД перестаёт отвечать.
setup_ui_by_role() products_window.py
def setup_ui_by_role(self):
  self.add_btn.hide()
  self.delete_btn.hide()
  self.orders_btn.hide()
  self.search_edit.hide()
  self.sort_box.hide()
  self.filter_box.hide()

  if self.role_id == 2:  # Администратор
      self.add_btn.show()
      self.delete_btn.show()
      self.orders_btn.show()
      self.search_edit.show()
      self.sort_box.show()
      self.filter_box.show()

  elif self.role_id == 3:  # Менеджер
      self.orders_btn.show()
      self.search_edit.show()
      self.sort_box.show()
      self.filter_box.show()
  # role_id == None → гость → всё скрыто
① Сначала скрыть всё
Принцип: скрываем всё, потом показываем только нужное. Так не нужно думать "что скрыть для каждой роли" — по умолчанию всё скрыто.
② role_id == 2 → Администратор
Видит всё: добавление, удаление, заказы, поиск, сортировку, фильтр. role_id берётся из таблицы users при авторизации и передаётся через __init__.
③ role_id == 3 → Менеджер
Видит заказы и инструменты поиска, но не может добавлять и удалять товары. Кнопки add/delete остаются скрытыми.
auth_login() login_window.py
def auth_login(self):
  login    = self.login_edit.text()
  password = self.password_edit.text()

  connection = None
  try:
      connection = db_connect()
      with connection.cursor() as cursor:
          cursor.execute(
              'SELECT full_name, role_id FROM users'
              ' WHERE login=%s AND password=%s',
              (login, password))
          user_data = cursor.fetchone()

      if user_data:
          full_name, role_id = user_data
          from products_window import ProductsWindow
          self.pw = ProductsWindow(full_name, role_id)
          self.pw.show()
          self.close()
      else:
          QMessageBox.critical(..., 'Неверный логин')

  except Exception as e:
      print(e)
      QMessageBox.critical(..., 'Нет подключения')
  finally:
      if connection:
          connection.close()
① fetchone() — ожидаем одного пользователя
Логин уникален — результат либо одна строка, либо None. fetchone() возвращает кортеж (full_name, role_id) или None.
② if user_data
None → False → неверный логин. Кортеж → True → пользователь найден. Распаковываем: full_name, role_id = user_data.
③ Импорт внутри функции
products_window импортирует login_window (для logout). Если импортировать наверху — циклический импорт. Импорт внутри функции выполняется только при вызове.
④ self.pw = — не просто pw =
Когда auth_login завершится — локальные переменные удаляются. pw без self удалится → окно закроется. self.pw живёт пока живёт LoginWindow (даже после close она ещё в памяти пока существует ссылка).

Структура проекта

Файлы логики

# Корень проекта
main.py              # Запуск
db.py                # Подключение к БД
login_window.py      # Авторизация
products_window.py   # Главное окно
product_card.py      # Карточка товара
add_product_form.py  # Форма товара
orders_window.py     # Окно заказов
order_card.py        # Карточка заказа
add_order_form.py    # Форма заказа
UI/                  # Сгенерированные UI файлы

Схема БД

products       product_id, article, product_name,
               unit, price, supplier_id,
               manufacture_id, category_id,
               discount, quantity_in_stock,
               description, photo_path

orders         order_number, order_date,
               delivery_date, pickup_point_id,
               user_recipient_id, receipt_code,
               order_status_id

order_products order_products_id, product_id,
               order_id, quantity

order_statuses status_id, status_name
pickup_points  pickup_point_id, postal_code,
               city, street, house
suppliers      supplier_id, supplier_name
manufacture    manufacture_id, manufacture_name
category       category_id, category
users          user_id, full_name, login,
               password, role_id

Роли

role_idРольДоступ
2АдминистраторВсё: добавление, редактирование, удаление, заказы
3МенеджерПросмотр, поиск, фильтр, заказы (без добавления товаров)
NoneГостьТолько просмотр

db.py

import pymysql

def db_connect():
    connection = pymysql.connect(
        host='localhost',
        port=3306,
        user='root',
        passwd='root',
        database='demka_1'
    )
    return connection

main.py

import sys
import traceback
from PyQt5.QtWidgets import QApplication
from login_window import LoginWindow

def except_hook(exc_type, exc_value, exc_traceback):
    # PyQt5 глотает ошибки — это перехватывает их и выводит в консоль
    error_text = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
    print(error_text)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    sys.excepthook = except_hook
    window = LoginWindow()
    window.show()
    sys.exit(app.exec_())

login_window.py

from PyQt5.QtWidgets import QWidget, QMessageBox
from UI.login_ui import Ui_Dialog
from db import db_connect

class LoginWindow(QWidget, Ui_Dialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.login_pushButton.clicked.connect(self.auth_login)

    def auth_login(self):
        login = self.login_lineEdit.text()
        password = self.password_lineEdit.text()
        connection = None
        try:
            connection = db_connect()
            with connection.cursor() as cursor:
                cursor.execute(
                    'SELECT full_name, role_id FROM users WHERE login=%s AND password=%s',
                    (login, password)
                )
                user_data = cursor.fetchone()

            if user_data:
                full_name, role_id = user_data
                from products_window import ProductsWindow
                self.products_window = ProductsWindow(full_name=full_name, role_id=role_id)
                self.products_window.show()   # self. — иначе окно закроется сразу
                self.close()
            else:
                QMessageBox.critical(self, 'Ошибка', 'Неверный логин или пароль')

        except Exception as e:
            print(e)
            QMessageBox.critical(self, 'Ошибка', 'Нет подключения к БД')
        finally:
            if connection:
                connection.close()
connection = None перед try — если db_connect() упадёт, в finally connection не будет определён и тоже упадёт
Импорт ProductsWindow внутри функции — защита от циклического импорта (products_window импортирует login_window)

products_window.py

from PyQt5.QtWidgets import QMainWindow
from UI.main_ui import Ui_MainWindow
from db import db_connect
from product_card import ProductCard

class ProductsWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, full_name="Гость", role_id=None):
        super().__init__()
        self.setupUi(self)
        self.role_id = role_id
        self.selected_card = None
        self.selected_product_id = None
        self.user_fio.setText(full_name)

        self.setup_ui_by_role()
        self.load_filter()    # 1. заполнить комбобоксы
        self.load_sort()
        self.load_products()   # 2. загрузить данные

        # 3. подключить сигналы ПОСЛЕ заполнения — иначе сработают раньше времени
        self.category_comboBox.currentTextChanged.connect(self.load_products)
        self.sort_comboBox.currentTextChanged.connect(self.load_products)
        self.search_lineEdit.textChanged.connect(self.load_products)
        self.add_product_pushButton.clicked.connect(self.add_product)
        self.delete_product_pushButton.clicked.connect(self.delete_product)
        self.logout_pushButton.clicked.connect(self.logout)
        self.orders_pushButton.clicked.connect(self.open_orders)

    def load_products(self):
        self.selected_card = None
        self.selected_product_id = None

        # Очистка layout
        while self.products_list_verticalLayout.count():
            item = self.products_list_verticalLayout.takeAt(0)
            if item.widget():
                item.widget().deleteLater()

        supplier_filter = self.category_comboBox.currentText()
        search = self.search_lineEdit.text().lower()
        search_param = f"%{search}%"

        sort = self.sort_comboBox.currentText()
        if "возрастанию" in sort:
            sort_param = "ORDER BY p.quantity_in_stock ASC"
        elif "убыванию" in sort:
            sort_param = "ORDER BY p.quantity_in_stock DESC"
        else:
            sort_param = ""

        base_query = """SELECT p.product_id, p.article, p.product_name, p.unit, p.price,
            s.supplier_name, m.manufacture_name, c.category, p.discount,
            p.quantity_in_stock, p.description, p.photo_path
            FROM products p
            JOIN suppliers s ON p.supplier_id = s.supplier_id
            JOIN manufacture m ON p.manufacture_id = m.manufacture_id
            JOIN category c ON p.category_id = c.category_id"""

        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                if supplier_filter == "Все поставщики" or not supplier_filter:
                    cursor.execute(f"{base_query} WHERE lower(p.product_name) LIKE %s {sort_param}", (search_param,))
                else:
                    cursor.execute(f"{base_query} WHERE s.supplier_name=%s AND lower(p.product_name) LIKE %s {sort_param}",
                                   (supplier_filter, search_param))
                products = cursor.fetchall()

            for data in products:
                card = ProductCard(data)
                card.edit_request.connect(self.edit_product)
                card.select_request.connect(self.select_product)
                self.products_list_verticalLayout.addWidget(card)
            self.products_list_verticalLayout.addStretch()

        finally:
            connection.close()

    def setup_ui_by_role(self):
        # Сначала всё скрыть
        self.search_lineEdit.hide()
        self.category_comboBox.hide()
        self.sort_comboBox.hide()
        self.add_product_pushButton.hide()
        self.delete_product_pushButton.hide()
        self.orders_pushButton.hide()

        if self.role_id == 2:  # Администратор
            self.search_lineEdit.show()
            self.category_comboBox.show()
            self.sort_comboBox.show()
            self.add_product_pushButton.show()
            self.delete_product_pushButton.show()
            self.orders_pushButton.show()
        elif self.role_id == 3:  # Менеджер
            self.search_lineEdit.show()
            self.category_comboBox.show()
            self.sort_comboBox.show()
            self.orders_pushButton.show()

    def load_filter(self):
        self.category_comboBox.clear()
        self.category_comboBox.addItem("Все поставщики")
        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                cursor.execute("SELECT supplier_id, supplier_name FROM suppliers")
                for sid, sname in cursor.fetchall():
                    self.category_comboBox.addItem(sname, sid)
        finally:
            connection.close()

    def load_sort(self):
        self.sort_comboBox.addItem("Без сортировки")
        self.sort_comboBox.addItem("Количество по возрастанию")
        self.sort_comboBox.addItem("Количество по убыванию")

    def select_product(self, card):
        if self.selected_card:
            self.selected_card.set_selected(False)
        self.selected_card = card
        self.selected_product_id = card.product_id
        card.set_selected(True)

    def add_product(self):
        from add_product_form import AddProductForm
        form = AddProductForm(parent=self)
        if form.exec_():
            self.load_products()

    def edit_product(self, product_id):
        if self.role_id != 2:
            return
        from add_product_form import AddProductForm
        form = AddProductForm(product_id=product_id, parent=self)
        if form.exec_():
            self.load_products()

    def delete_product(self):
        if not self.selected_product_id:
            return
        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                cursor.execute("SELECT COUNT(*) FROM order_products WHERE product_id=%s", (self.selected_product_id,))
                if cursor.fetchone()[0] > 0:
                    QMessageBox.critical(self, "Ошибка", "Товар участвует в заказах")
                    return
                cursor.execute("DELETE FROM products WHERE product_id=%s", (self.selected_product_id,))
                connection.commit()
        finally:
            connection.close()
        self.selected_product_id = None
        self.selected_card = None
        self.load_products()

    def open_orders(self):
        from orders_window import OrdersWindow
        from PyQt5.QtCore import Qt
        self.orders_window = OrdersWindow(role_id=self.role_id)
        self.orders_window.setWindowModality(Qt.ApplicationModal)
        self.orders_window.show()

    def logout(self):
        from login_window import LoginWindow
        self.login_window = LoginWindow()
        self.login_window.show()
        self.close()

product_card.py

from PyQt5 import QtGui
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QFrame
from UI.product_form_ui import Ui_Frame

class ProductCard(QFrame, Ui_Frame):
    edit_request   = pyqtSignal(int)   # передаёт product_id при двойном клике
    select_request = pyqtSignal(object) # передаёт саму карточку для выделения

    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        (self.product_id, article, product_name, unit, price,
         supplier_name, manufacture_name, category, discount,
         quantity_in_stock, description, photo_path) = data

        self.setup_labels(article, product_name, price, discount,
                          quantity_in_stock, supplier_name)
        self.set_photo(photo_path)

    def setup_labels(self, article, product_name, price, discount,
                       quantity_in_stock, supplier_name):
        self.article_label.setText(article)
        self.name_label.setText(product_name)
        self.price_label.setText(f"{price} руб.")
        self.discount_label.setText(f"{discount}%")
        self.stock_label.setText(f"{quantity_in_stock} шт.")
        self.supplier_label.setText(supplier_name)

    def set_photo(self, photo_path):
        if photo_path:
            self.photo_label.setPixmap(QtGui.QPixmap(photo_path))
        else:
            self.photo_label.setPixmap(QtGui.QPixmap("import/picture.png"))

    def set_selected(self, selected: bool):
        if selected:
            self.setStyleSheet("QFrame { border: 2px solid #4fc3f7; }")
        else:
            self.setStyleSheet("")

    def mousePressEvent(self, event):
        self.select_request.emit(self)           # выделение
        self.edit_request.emit(self.product_id)   # редактирование
        super().mousePressEvent(event)

add_product_form.py

from PyQt5.QtWidgets import QDialog, QMessageBox, QFileDialog
from UI.add_product_ui import Ui_Dialog
from db import db_connect

class AddProductForm(QDialog, Ui_Dialog):
    def __init__(self, parent=None, product_id=None):
        super().__init__(parent)
        self.setupUi(self)
        self.product_id = product_id

        self.load_comboboxes()
        if self.product_id:
            self.load_data()

        self.save_pushButton.clicked.connect(self.save)
        self.cancel_pushButton.clicked.connect(self.reject)
        self.photo_pushButton.clicked.connect(self.select_photo)

    def load_comboboxes(self):
        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                cursor.execute("SELECT category_id, category FROM category")
                for cid, cname in cursor.fetchall():
                    self.category_comboBox.addItem(cname, cid)

                cursor.execute("SELECT manufacture_id, manufacture_name FROM manufacture")
                for mid, mname in cursor.fetchall():
                    self.manufacture_comboBox.addItem(mname, mid)

                cursor.execute("SELECT supplier_id, supplier_name FROM suppliers")
                for sid, sname in cursor.fetchall():
                    self.supplier_comboBox.addItem(sname, sid)
        finally:
            connection.close()

    def load_data(self):
        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                cursor.execute("SELECT * FROM products WHERE product_id=%s", (self.product_id,))
                p = cursor.fetchone()

            self.article_lineEdit.setText(p[1])
            self.name_lineEdit.setText(p[2])
            self.price_lineEdit.setText(str(p[4]))
            self.discount_spinBox.setValue(float(p[8]))
            self.stock_spinBox.setValue(int(p[9]))
            self.description_textEdit.setPlainText(p[10] or "")
            self.photo_lineEdit.setText(p[11] or "")

            # Установка комбобоксов по ID
            for box, val in [
                (self.supplier_comboBox, p[5]),
                (self.manufacture_comboBox, p[6]),
                (self.category_comboBox, p[7])
            ]:
                idx = box.findData(val)
                if idx >= 0:
                    box.setCurrentIndex(idx)
        finally:
            connection.close()

    def select_photo(self):
        path, _ = QFileDialog.getOpenFileName(self, "Выберите фото", "", "Images (*.jpg *.png)")
        if path:
            self.photo_lineEdit.setText(path)

    def save(self):
        article  = self.article_lineEdit.text()
        name     = self.name_lineEdit.text()
        price    = float(self.price_lineEdit.text() or 0)
        discount = float(self.discount_spinBox.value())
        stock    = int(self.stock_spinBox.value())
        desc     = self.description_textEdit.toPlainText()
        photo    = self.photo_lineEdit.text()
        category = self.category_comboBox.currentData()
        manufacture = self.manufacture_comboBox.currentData()
        supplier = self.supplier_comboBox.currentData()

        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                if self.product_id:
                    cursor.execute("""UPDATE products SET article=%s, product_name=%s,
                        unit='шт.', price=%s, supplier_id=%s, manufacture_id=%s,
                        category_id=%s, discount=%s, quantity_in_stock=%s,
                        description=%s, photo_path=%s WHERE product_id=%s""",
                        (article, name, price, supplier, manufacture, category,
                         discount, stock, desc, photo, self.product_id))
                else:
                    cursor.execute("""INSERT INTO products (article, product_name, unit, price,
                        supplier_id, manufacture_id, category_id, discount,
                        quantity_in_stock, description, photo_path)
                        VALUES (%s,%s,'шт.',%s,%s,%s,%s,%s,%s,%s,%s)""",
                        (article, name, price, supplier, manufacture, category,
                         discount, stock, desc, photo))
            connection.commit()
            QMessageBox.information(self, "Успех", "Сохранено")
            self.accept()
        except Exception as e:
            print(e)
            QMessageBox.critical(self, "Ошибка", "Не удалось сохранить")
        finally:
            connection.close()

orders_window.py

from PyQt5.QtWidgets import QMainWindow
from UI.orders_form_ui import Ui_MainWindow
from db import db_connect
from order_card import OrderCard

class OrdersWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, role_id=None):
        super().__init__()
        self.setupUi(self)
        self.role_id = role_id
        self.load_orders()
        self.add_order_pushButton.clicked.connect(self.add_order)

    def load_orders(self):
        while self.orders_verticalLayout.count():
            item = self.orders_verticalLayout.takeAt(0)
            if item.widget():
                item.widget().deleteLater()

        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                cursor.execute("""SELECT o.order_number, o.order_date, o.delivery_date,
                    os.status_name,
                    CONCAT_WS(', ', TRIM(pp.postal_code), TRIM(pp.city),
                              TRIM(pp.street), TRIM(pp.house)) as address
                    FROM orders o
                    JOIN order_statuses os ON o.order_status_id = os.status_id
                    JOIN pickup_points pp ON o.pickup_point_id = pp.pickup_point_id""")
                orders = cursor.fetchall()

        finally:
            connection.close()

        for data in orders:
            card = OrderCard(data)
            card.card_request.connect(self.edit_order)
            self.orders_verticalLayout.addWidget(card)
        self.orders_verticalLayout.addStretch()

    def add_order(self):
        from add_order_form import AddOrderForm
        form = AddOrderForm(parent=self)
        if form.exec_():
            self.load_orders()

    def edit_order(self, order_id):
        from add_order_form import AddOrderForm
        form = AddOrderForm(parent=self, order_id=order_id)
        if form.exec_():
            self.load_orders()

order_card.py

from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QFrame
from UI.order_card_ui import Ui_Frame

class OrderCard(QFrame, Ui_Frame):
    card_request = pyqtSignal(int)

    def __init__(self, data, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        (self.order_id, order_date, delivery_date, status, address) = data
        self.number_label.setText(f"Заказ №{self.order_id}")
        self.status_label.setText(status)
        self.address_label.setText(address)
        self.order_date_label.setText(str(order_date))
        self.delivery_date_label.setText(str(delivery_date))

    def mousePressEvent(self, event):
        self.card_request.emit(self.order_id)
        super().mousePressEvent(event)

add_order_form.py

from PyQt5.QtCore import QDate
from PyQt5.QtWidgets import QDialog, QMessageBox
from UI.add_order_ui import Ui_Dialog
from db import db_connect

class AddOrderForm(QDialog, Ui_Dialog):
    def __init__(self, parent=None, order_id=None):
        super().__init__(parent)
        self.setupUi(self)
        self.order_id = order_id
        self.items = []   # список (product_id, text, qty)

        self.load_comboboxes()
        if self.order_id:
            self.load_order_data()

        self.add_product_pushButton.clicked.connect(self.add_item)
        self.delete_product_pushButton.clicked.connect(self.delete_item)
        self.save_pushButton.clicked.connect(self.save)
        self.cancel_pushButton.clicked.connect(self.reject)

    def load_comboboxes(self):
        self.status_comboBox.addItem("Выберите статус", None)
        self.recipient_comboBox.addItem("Выберите получателя", None)
        self.pp_comboBox.addItem("Выберите адрес", None)
        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                cursor.execute("SELECT status_id, status_name FROM order_statuses")
                for sid, sname in cursor.fetchall():
                    self.status_comboBox.addItem(sname, sid)

                cursor.execute("SELECT product_id, article, product_name FROM products")
                for pid, art, pname in cursor.fetchall():
                    self.product_comboBox.addItem(f"{art} - {pname}", pid)

                cursor.execute("SELECT user_id, full_name FROM users")
                for uid, fname in cursor.fetchall():
                    self.recipient_comboBox.addItem(fname, uid)

                cursor.execute("""SELECT pickup_point_id,
                    CONCAT_WS(', ', TRIM(postal_code), TRIM(city), TRIM(street), TRIM(house))
                    FROM pickup_points""")
                for ppid, addr in cursor.fetchall():
                    self.pp_comboBox.addItem(addr, ppid)
        finally:
            connection.close()

    def load_order_data(self):
        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                cursor.execute("""SELECT order_date, delivery_date, pickup_point_id,
                    user_recipient_id, receipt_code, order_status_id
                    FROM orders WHERE order_number=%s""", (self.order_id,))
                o = cursor.fetchone()

                self.order_date_dateEdit.setDate(QDate.fromString(str(o[0]), 'yyyy-MM-dd'))
                self.delivery_date_dateEdit.setDate(QDate.fromString(str(o[1]), 'yyyy-MM-dd'))
                self.receipt_lineEdit.setText(str(o[4]))

                for box, val in [
                    (self.pp_comboBox, o[2]),
                    (self.recipient_comboBox, o[3]),
                    (self.status_comboBox, o[5])
                ]:
                    idx = box.findData(val)
                    if idx >= 0:
                        box.setCurrentIndex(idx)

                # Загрузка товаров заказа
                cursor.execute("""SELECT op.product_id,
                    CONCAT_WS(' - ', p.article, p.product_name), op.quantity
                    FROM order_products op
                    JOIN products p ON op.product_id = p.product_id
                    WHERE op.order_id=%s""", (self.order_id,))
                for pid, pname, qty in cursor.fetchall():
                    self.items.append((pid, pname, qty))
                self.refresh_list()
        finally:
            connection.close()

    def add_item(self):
        product_id = self.product_comboBox.currentData()
        if not product_id:
            return
        text = self.product_comboBox.currentText()
        qty = self.qty_spinBox.value()
        self.items.append((product_id, text, qty))
        self.refresh_list()

    def delete_item(self):
        row = self.products_listWidget.currentRow()
        if row < 0:
            return
        self.items.pop(row)
        self.refresh_list()

    def refresh_list(self):
        self.products_listWidget.clear()
        for _, name, qty in self.items:
            self.products_listWidget.addItem(f"{name} x {qty} шт.")

    def save(self):
        status    = self.status_comboBox.currentData()
        pp        = self.pp_comboBox.currentData()
        recipient = self.recipient_comboBox.currentData()
        receipt   = self.receipt_lineEdit.text()
        order_date    = self.order_date_dateEdit.date().toPyDate()
        delivery_date = self.delivery_date_dateEdit.date().toPyDate()

        if not all([status, pp, recipient]):
            QMessageBox.warning(self, "Ошибка", "Заполните все поля")
            return
        if not self.items:
            QMessageBox.warning(self, "Ошибка", "Добавьте товары")
            return

        connection = db_connect()
        try:
            with connection.cursor() as cursor:
                if self.order_id:
                    cursor.execute("""UPDATE orders SET order_date=%s, delivery_date=%s,
                        pickup_point_id=%s, user_recipient_id=%s, receipt_code=%s,
                        order_status_id=%s WHERE order_number=%s""",
                        (order_date, delivery_date, pp, recipient, receipt, status, self.order_id))
                    cursor.execute("DELETE FROM order_products WHERE order_id=%s", (self.order_id,))
                    order_id = self.order_id
                else:
                    cursor.execute("""INSERT INTO orders (order_date, delivery_date,
                        pickup_point_id, user_recipient_id, receipt_code, order_status_id)
                        VALUES (%s,%s,%s,%s,%s,%s)""",
                        (order_date, delivery_date, pp, recipient, receipt, status))
                    order_id = cursor.lastrowid  # ID только что созданного заказа

                for pid, _, qty in self.items:
                    cursor.execute(
                        "INSERT INTO order_products (product_id, order_id, quantity) VALUES (%s,%s,%s)",
                        (pid, order_id, qty))

            connection.commit()
            QMessageBox.information(self, "Успех", "Сохранено")
            self.accept()
        except Exception as e:
            print(e)
            QMessageBox.critical(self, "Ошибка", "Не удалось сохранить")
        finally:
            connection.close()

Ключевые паттерны

self. для окон — обязательно

# Окно закроется сразу:
window = ProductsWindow()
window.show()

# Правильно:
self.products_window = ProductsWindow()
self.products_window.show()

exec_() vs show()

QDialog   → exec_()   # блокирует, возвращает 0/1
QMainWindow → show()  # не блокирует

result = form.exec_()
if result:   # 1 = accept(), 0 = reject()
    self.load_products()

Цепочка сигналов

# 1. Объявление в классе карточки:
edit_request = pyqtSignal(int)

# 2. Отправка сигнала:
def mousePressEvent(self, event):
    self.edit_request.emit(self.product_id)
    super().mousePressEvent(event)

# 3. Подписка при создании карточки:
card.edit_request.connect(self.edit_product)

# 4. Обработчик принимает product_id:
def edit_product(self, product_id):
    ...

findData для комбобоксов

# addItem(текст, данные) — данные = ID из БД
comboBox.addItem("Название", supplier_id)

# Установить по ID при загрузке:
idx = comboBox.findData(supplier_id)
if idx >= 0:   # не if idx: — 0 тоже валидный индекс!
    comboBox.setCurrentIndex(idx)

# Получить выбранный ID:
supplier_id = comboBox.currentData()

QDate для DateEdit

# Из БД в виджет:
dateEdit.setDate(
    QDate.fromString(str(date_from_db), 'yyyy-MM-dd')
)

# Из виджета в Python:
py_date = dateEdit.date().toPyDate()

cursor.lastrowid

# ID строки только что вставленной через INSERT
cursor.execute("INSERT INTO orders ...")
order_id = cursor.lastrowid

# Нужен чтобы привязать order_products к заказу:
cursor.execute(
    "INSERT INTO order_products ... VALUES (%s,%s,%s)",
    (product_id, order_id, qty)
)

Очистка layout

while self.layout.count():
    item = self.layout.takeAt(0)
    if item.widget():
        item.widget().deleteLater()
# item — QLayoutItem, не виджет!
# item.widget() — достать виджет из него

Порядок в __init__

# ВАЖНО: connect ПОСЛЕ load_filter
# иначе addItem → сигнал → load_products раньше времени
self.load_filter()
self.load_sort()
self.load_products()
self.comboBox.currentTextChanged.connect(self.load_products)

Блокировка окна

from PyQt5.QtCore import Qt
self.orders_window = OrdersWindow()
self.orders_window.setWindowModality(
    Qt.ApplicationModal  # блокирует все окна
)
self.orders_window.show()

QFileDialog

path, _ = QFileDialog.getOpenFileName(
    self,
    "Выберите фото",   # заголовок
    "",                # начальная директория
    "Images (*.jpg *.png)"  # фильтр
)
if path:
    self.photo_lineEdit.setText(path)

SQL справочник

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

-- SELECT
SELECT поля FROM таблица WHERE условие

-- INSERT
INSERT INTO таблица (поля)
VALUES (%s, %s, %s)

-- UPDATE (без скобок после SET!)
UPDATE таблица SET поле=%s, поле2=%s
WHERE id=%s

-- DELETE
DELETE FROM таблица WHERE id=%s

LIKE и поиск

-- % = любые символы
WHERE name LIKE '%бот%'   -- содержит
WHERE name LIKE 'бот%'    -- начинается

-- Через параметры (нельзя f-строку с %s!)
search_param = f"%{text.lower()}%"
cursor.execute(
    "WHERE lower(name) LIKE %s",
    (search_param,)   # запятая обязательна!
)

ORDER BY через f-строку

# ORDER BY нельзя передать как %s параметр
# Только через f-строку:
sort = "ORDER BY quantity_in_stock ASC"
cursor.execute(f"SELECT ... {sort}")

CONCAT_WS для адреса

CONCAT_WS(', ',
    TRIM(postal_code),
    TRIM(city),
    TRIM(street),
    TRIM(house)
) as address
-- TRIM убирает пробелы
-- CONCAT_WS пропускает NULL поля

AND / OR приоритет

-- AND выполняется раньше OR (как * перед +)
-- Без скобок — НЕВЕРНО:
WHERE supplier='Kari' AND name LIKE '%x%'
    OR desc LIKE '%x%'
-- Читается: (supplier AND name) OR desc

-- Со скобками — ВЕРНО:
WHERE supplier='Kari'
    AND (name LIKE '%x%' OR desc LIKE '%x%')

JOIN для заказов

SELECT op.product_id,
    CONCAT_WS(' - ', p.article, p.product_name),
    op.quantity
FROM order_products op
JOIN products p ON op.product_id = p.product_id
WHERE op.order_id = %s

Частые ошибки

ОШИБКА

Кортеж с одним элементом

# Это не кортеж — просто скобки:
(product_id)

# Кортеж — нужна запятая:
(product_id,)
ОШИБКА

if idx: для индексов

# Неверно — 0 это falsy:
if idx:
    box.setCurrentIndex(idx)

# Верно:
if idx >= 0:
    box.setCurrentIndex(idx)
ОШИБКА

fetchall() вне with

# Неверно — курсор закрыт:
with cursor as c:
    c.execute(...)
data = c.fetchall()  # ошибка!

# Верно — внутри with:
with cursor as c:
    c.execute(...)
    data = c.fetchall()
ОШИБКА

== вместо = при присвоении

# Ничего не делает — это сравнение:
supplier_filter == ""

# Присвоение:
supplier_filter = ""
ОШИБКА

not Null

# Это не Python:
if photo_path not Null:

# Правильно:
if photo_path:
if photo_path is not None:
ОШИБКА

setPixmap принимает QPixmap

# Неверно — строка не QPixmap:
label.setPixmap(photo_path)

# Верно:
from PyQt5 import QtGui
label.setPixmap(QtGui.QPixmap(photo_path))
ОШИБКА

accept() в кнопке отмены

# Неверно — вызовет load_products:
def cancel(self):
    self.accept()

# Верно:
def cancel(self):
    self.reject()
ОШИБКА

Django импорты

# PyCharm добавляет автоматически — удаляй:
from django.db import connection
from itertools import product
from re import search
ОШИБКА

Путь к картинке

# Qt Designer пишет относительно .ui файла:
"../import/Icon.jpg"  # неверно при запуске

# Верно — относительно рабочей директории:
"import/Icon.jpg"

MySQL Workbench — лайфхаки

Горячие клавиши

КлавишиДействие
Ctrl + EnterВыполнить текущий запрос (где курсор)
Ctrl + Shift + EnterВыполнить всё в редакторе
Ctrl + /Закомментировать строку
Ctrl + ZОтменить
Ctrl + LУдалить строку

Быстрый просмотр таблицы

В панели слева → правой кнопкой на таблицу → Select Rows — покажет первые 1000 строк без написания запроса.


Там же → Table Inspector → вкладка Columns — все поля таблицы с типами.

Импорт CSV в таблицу

1. Правой кнопкой на таблицу
2. Table Data Import Wizard
3. Выбрать CSV файл
4. Проверить маппинг колонок
5. Next → Next → Finish
CSV должен быть в UTF-8, разделитель — запятая или точка с запятой

Выполнить только часть запроса

Выдели нужный текст мышкой → Ctrl+Enter — выполнится только выделенное.


Удобно когда в файле несколько запросов и нужно запустить один.

Быстрый INSERT из таблицы

-- Правой кнопкой на таблицу →
-- Send to SQL Editor →
-- Insert Statement
-- Получишь готовый шаблон:
INSERT INTO `table` (`col1`, `col2`)
VALUES ('val1', 'val2');

Если забыл структуру таблицы

-- Показать все поля таблицы:
DESCRIBE products;

-- Показать CREATE TABLE:
SHOW CREATE TABLE products;

-- Все таблицы в БД:
SHOW TABLES;

Сброс AUTO_INCREMENT

-- Если удалил все строки и хочешь
-- чтоб ID снова с 1:
TRUNCATE TABLE order_products;
-- TRUNCATE удаляет всё и сбрасывает счётчик
-- DELETE FROM — только удаляет, ID продолжается

Отключить проверку FK при заполнении

-- Если мешают foreign key при вставке:
SET FOREIGN_KEY_CHECKS = 0;

INSERT INTO ...

SET FOREIGN_KEY_CHECKS = 1;
-- Не забудь включить обратно!

Excel / LibreOffice — подготовка данных

ВПР (VLOOKUP) — главная функция

=ВПР(что_ищем; где_ищем; номер_столбца; 0)

Пример: по названию поставщика найти его ID
=ВПР(A2; $F$2:$G$10; 2; 0)
  A2         — название поставщика в текущей строке
  $F$2:$G$10 — таблица ID|Название ($ = фиксация)
  2          — взять второй столбец (ID)
  0          — точное совпадение
В LibreOffice разделитель ; (точка с запятой), в Excel может быть , (запятая)

Типичная задача: заменить текст на ID

Дано: таблица товаров со столбцом "Поставщик" (текст)
Нужно: столбец supplier_id (число)

1. На отдельном листе сделай справочник:
   ID | Поставщик
   1  | Kari
   2  | Обувь для вас

2. В столбце supplier_id пиши:
=ВПР(C2; Лист2.$A$1:$B$10; 1; 0)
   C2 = ячейка с названием поставщика

3. Протяни формулу вниз на все строки

Зафиксировать диапазон — $

Без $ — диапазон сдвигается при протягивании:
=ВПР(A2; F2:G10; 2; 0)  ← неверно

С $ — диапазон фиксирован:
=ВПР(A2; $F$2:$G$10; 2; 0)  ← верно

Быстро поставить $: выдели диапазон в формуле → F4

Скопировать значения без формул

После ВПР в столбце формулы, а нам нужны числа:

1. Выдели столбец с ВПР
2. Ctrl+C
3. Правой кнопкой → Специальная вставка
4. Выбрать "Значения" (только числа/текст)
5. OK

Теперь можно удалить справочник — значения останутся

Экспорт в CSV для импорта в БД

1. Файл → Сохранить как → CSV
2. Разделитель: точка с запятой или запятая
3. Кодировка: UTF-8

В Workbench:
Table Data Import Wizard → выбрать CSV
Проверить что колонки совпадают!

ЕСЛИ + ВПР — обработка ошибок

ВПР вернёт #Н/Д если не нашёл — это сломает импорт

Защита от ошибки:
=ЕСЛИОШИБКА(ВПР(A2; $F$2:$G$5; 2; 0); "")

Или в английском Excel:
=IFERROR(VLOOKUP(A2; $F$2:$G$5; 2; 0); "")

Быстрые операции с данными

Протянуть формулу на весь столбец:
Кликни на ячейку с формулой →
двойной клик на квадратик в правом нижнем углу

Найти и заменить (чистка данных):
Ctrl+H → заменить лишние пробелы, символы

Удалить дубликаты:
Данные → Удалить дубликаты

Генерация INSERT запросов в Excel

Можно собрать SQL прямо в ячейке:
="INSERT INTO products VALUES ("
 &A2& ", '"&B2&"', '"&C2&"')"

Протяни вниз → скопируй столбец →
вставь в Workbench → выполни

Удобно для быстрого заполнения таблиц

Порядок заполнения БД из Excel

ВАЖНО: заполнять в правильном порядке — сначала справочники, потом зависимые таблицы

1. suppliers      ← нет зависимостей
2. manufacture    ← нет зависимостей
3. category       ← нет зависимостей
4. users          ← нет зависимостей
5. pickup_points  ← нет зависимостей
6. order_statuses ← нет зависимостей
7. products       ← зависит от suppliers, manufacture, category
8. orders         ← зависит от users, pickup_points, order_statuses
9. order_products ← зависит от orders, products

Если нарушить порядок — FOREIGN KEY ошибка при INSERT