Initial Commit
This commit is contained in:
commit
18d456bf2e
20 changed files with 1442 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
12
.idea/master.iml
generated
Normal file
12
.idea/master.iml
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/master.iml" filepath="$PROJECT_DIR$/.idea/master.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
BIN
assets/Велосипед Giant ATX.jpg
Normal file
BIN
assets/Велосипед Giant ATX.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
BIN
assets/Велосипед Trek X200.jpg
Normal file
BIN
assets/Велосипед Trek X200.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
BIN
assets/Лыжи SnowFast 300.jpeg
Normal file
BIN
assets/Лыжи SnowFast 300.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
BIN
assets/Ролики SpeedRun.jpg
Normal file
BIN
assets/Ролики SpeedRun.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
BIN
assets/Самокат Urban Pro.jpg
Normal file
BIN
assets/Самокат Urban Pro.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/Сноуборд Arctic Pro.jpg
Normal file
BIN
assets/Сноуборд Arctic Pro.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
57
composer.py
Normal file
57
composer.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
from src.windows import LoginWindow, MainWindow
|
||||||
|
from src.objects import User
|
||||||
|
from src.db import DB_CONFIG
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal
|
||||||
|
from PyQt6.QtSql import QSqlDatabase
|
||||||
|
|
||||||
|
|
||||||
|
class Composer(QObject):
|
||||||
|
"""Управляет навигацией между окнами и жизненным циклом приложения"""
|
||||||
|
render_request = pyqtSignal(User)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._current = None
|
||||||
|
self._app = QApplication([])
|
||||||
|
self._init_db()
|
||||||
|
self.render_request.connect(self._render)
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
"""Инициализация Qt SQL соединения (используется QSqlTableModel)"""
|
||||||
|
self._db = QSqlDatabase.addDatabase("QPSQL")
|
||||||
|
self._db.setDatabaseName(DB_CONFIG['dbname'])
|
||||||
|
self._db.setPort(DB_CONFIG['port'])
|
||||||
|
self._db.setHostName(DB_CONFIG['host'])
|
||||||
|
self._db.setUserName(DB_CONFIG['user'])
|
||||||
|
self._db.setPassword(DB_CONFIG['password'])
|
||||||
|
|
||||||
|
if not self._db.open():
|
||||||
|
raise Exception(
|
||||||
|
f"Не удалось подключиться к БД: {self._db.lastError().text()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pyqtSlot(User)
|
||||||
|
def _render(self, user: User):
|
||||||
|
"""Маршрутизация: все роли идут в MainWindow, права применяются внутри"""
|
||||||
|
self._main_fabric(user)
|
||||||
|
|
||||||
|
def _login_fabric(self):
|
||||||
|
self.wlogin = LoginWindow(self, self._db)
|
||||||
|
self._switch_window(self.wlogin)
|
||||||
|
|
||||||
|
def _main_fabric(self, user: User):
|
||||||
|
self.wmain = MainWindow(self, self._db, user)
|
||||||
|
self._switch_window(self.wmain)
|
||||||
|
|
||||||
|
def _switch_window(self, new_window):
|
||||||
|
if self._current:
|
||||||
|
self._current.close()
|
||||||
|
new_window.show()
|
||||||
|
self._current = new_window
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
import sys
|
||||||
|
self._login_fabric()
|
||||||
|
sys.exit(self._app.exec())
|
||||||
8
main.py
Normal file
8
main.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from composer import Composer
|
||||||
|
|
||||||
|
def main():
|
||||||
|
composer = Composer()
|
||||||
|
composer.run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
139
postgresql.sql
Normal file
139
postgresql.sql
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
create extension pgcrypto;
|
||||||
|
|
||||||
|
create domain email as varchar(600)
|
||||||
|
check (
|
||||||
|
value ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
|
||||||
|
);
|
||||||
|
|
||||||
|
create domain phone as varchar(50)
|
||||||
|
check (
|
||||||
|
value ~ '^\+[0-9]{10,15}$'
|
||||||
|
);
|
||||||
|
|
||||||
|
create type rent_status as enum ('Новая', 'Подтверждена', 'Выдана', 'Завершена', 'Отменена');
|
||||||
|
|
||||||
|
create table cmd_type(
|
||||||
|
id serial primary key,
|
||||||
|
type varchar(300)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table manufacturer(
|
||||||
|
id serial primary key,
|
||||||
|
name varchar(300)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table pu_point(
|
||||||
|
id serial primary key,
|
||||||
|
name varchar(300),
|
||||||
|
address varchar(600)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table commodity(
|
||||||
|
id serial primary key,
|
||||||
|
inv_number char(4),
|
||||||
|
supply_name varchar,
|
||||||
|
supply_type int references cmd_type(id),
|
||||||
|
rent decimal(10,2),
|
||||||
|
manufacturer int references manufacturer(id),
|
||||||
|
pick_up_point int references pu_point(id),
|
||||||
|
img_path text
|
||||||
|
);
|
||||||
|
|
||||||
|
create table client(
|
||||||
|
id serial primary key,
|
||||||
|
name varchar(600),
|
||||||
|
phone phone,
|
||||||
|
email email
|
||||||
|
);
|
||||||
|
|
||||||
|
create table employee(
|
||||||
|
id serial primary key,
|
||||||
|
name varchar(600)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table rent(
|
||||||
|
id serial primary key,
|
||||||
|
commodity int references commodity(id),
|
||||||
|
client int references client(id),
|
||||||
|
r_start date,
|
||||||
|
r_end date,
|
||||||
|
status rent_status,
|
||||||
|
employee int references employee(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table roles(
|
||||||
|
id serial primary key,
|
||||||
|
name varchar(300),
|
||||||
|
level int
|
||||||
|
);
|
||||||
|
|
||||||
|
create table users(
|
||||||
|
id serial primary key,
|
||||||
|
name varchar(300),
|
||||||
|
role int references roles(id),
|
||||||
|
hash text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table permission_map(
|
||||||
|
id serial primary key,
|
||||||
|
perm varchar(300),
|
||||||
|
req_level int
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into cmd_type(type) values
|
||||||
|
('Велосипед'), ('Самокат'), ('Лыжи'), ('Ролики'), ('Сноуборд');
|
||||||
|
|
||||||
|
insert into manufacturer(name) values
|
||||||
|
('Trek'), ('UrbanRide'), ('SnowFast'),
|
||||||
|
('SpeedRun'), ('Giant'), ('Arctic');
|
||||||
|
|
||||||
|
insert into pu_point(name, address) values
|
||||||
|
('Центральный', 'ул. Мира 15'),
|
||||||
|
('Северный', 'пр. Победы 10'),
|
||||||
|
('Южный', 'ул. Ленина 8');
|
||||||
|
|
||||||
|
insert into commodity(inv_number, supply_name,
|
||||||
|
supply_type, rent, manufacturer,
|
||||||
|
pick_up_point, img_path) values
|
||||||
|
('1001', 'Trek X200', 1, 900.00, 1, 1, 'assets/Велосипед TrekX200.jpg'),
|
||||||
|
('1002', 'Urban Pro', 2, 400.00, 2, 2, 'assets/Самокат Urban Pro.jpg'),
|
||||||
|
('1003', 'SnowFast 300', 1, 1200.00, 3, 1, 'assets/Лыжи SnowFast 300.jpg'),
|
||||||
|
('1004', 'SpeedRun', 1, 700.00, 4, 3, 'assets/Ролики SpeedRun.jpg'),
|
||||||
|
('1005', 'Giant ATX', 1, 850.00, 5, 2, 'assets/Велосипед Giant ATX.jpg'),
|
||||||
|
('1006', 'Arctic Pro', 1, 1500.00, 6, 3, 'assets/Сноуборд Arctic Pro.jpg');
|
||||||
|
|
||||||
|
insert into client(name, phone, email) values
|
||||||
|
('Иванов И.И.', '+79997776655', 'ivan@mail.ru'),
|
||||||
|
('Сидоров С.С.', '+79887776655', 'sid@mail.ru'),
|
||||||
|
('Кузнецов К.К.', '+7776655443', 'kuz@mail.ru'),
|
||||||
|
('Смирнова А.А.', '+79665554433', 'smirnova@mail.ru'),
|
||||||
|
('Васильев Д.Д.', '+79554443322', 'vasiliev@mail.ru');
|
||||||
|
|
||||||
|
insert into employee(name) values
|
||||||
|
('Петров П.П.'), ('Орлов А.А.');
|
||||||
|
|
||||||
|
insert into rent(commodity, client, r_start, r_end, status, employee) values
|
||||||
|
(1, 1, '2024-04-12', '2024-04-15', 'Новая', 1),
|
||||||
|
(2, 2, '2024-04-10', '2024-04-11', 'Подтверждена', 2),
|
||||||
|
(3, 1, '2024-04-20', '2024-04-25', 'Выдана', 1),
|
||||||
|
(4, 3, '2024-04-15', '2024-04-17', 'Завершена', 2),
|
||||||
|
(5, 4, '2024-04-18', '2024-04-20', 'Отменена', 1),
|
||||||
|
(6, 5, '2024-04-01', '2024-04-05', 'Новая', 2);
|
||||||
|
|
||||||
|
insert into roles(name, level) values
|
||||||
|
('admin', 100), ('employee', 50), ('client', 25), ('guest', 0);
|
||||||
|
|
||||||
|
insert into users(name, role, hash) values
|
||||||
|
('admin', 1, crypt('admin123', gen_salt('bf'))),
|
||||||
|
('employee', 2, crypt('employee123', gen_salt('bf'))),
|
||||||
|
('client1', 3, crypt('client123', gen_salt('bf'))),
|
||||||
|
('client2', 3, crypt('client321', gen_salt('bf')));
|
||||||
|
|
||||||
|
insert into permission_map(perm, req_level) values
|
||||||
|
('read.equipment', 0),
|
||||||
|
('read.requests', 50),
|
||||||
|
('create.equipment', 100),
|
||||||
|
('create.requests', 25),
|
||||||
|
('update.equipment', 100),
|
||||||
|
('update.request.status', 50),
|
||||||
|
('delete.equipment', 100);
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
311
src/db.py
Normal file
311
src/db.py
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
import psycopg2 as pg
|
||||||
|
from .objects import User, Role
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 5432,
|
||||||
|
"dbname": "example3",
|
||||||
|
"user": "postgres",
|
||||||
|
"password": "1q2w3e4r%TGB###"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
"""Получить подключение к базе данных"""
|
||||||
|
return pg.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def do_request(autocommit=False):
|
||||||
|
"""
|
||||||
|
Декоратор для выполнения SQL запросов.
|
||||||
|
Автоматически управляет соединением и курсором.
|
||||||
|
"""
|
||||||
|
def upper_wrapper(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
kwargs['cursor'] = cursor
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
if autocommit:
|
||||||
|
conn.commit()
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: Database request failed: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return wrapper
|
||||||
|
return upper_wrapper
|
||||||
|
|
||||||
|
def can(user: User, permission: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверить, имеет ли пользователь право на действие.
|
||||||
|
Сверяется с таблицей permission_map в БД.
|
||||||
|
"""
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT req_level FROM permission_map WHERE perm = %s",
|
||||||
|
(permission,)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return False
|
||||||
|
return user.role.level >= row[0]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: permission check failed: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
@do_request()
|
||||||
|
def auth(login: str, password: str, *, cursor) -> User | None:
|
||||||
|
"""Аутентификация пользователя через bcrypt (pgcrypto)"""
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT u.id, u.name, r.name, r.level
|
||||||
|
FROM users u
|
||||||
|
JOIN roles r ON r.id = u.role
|
||||||
|
WHERE u.name = %s
|
||||||
|
AND u.hash = crypt(%s, u.hash)
|
||||||
|
""",
|
||||||
|
(login, password)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
role = Role(name=row[2], level=row[3])
|
||||||
|
return User(id=row[0], name=row[1], role=role)
|
||||||
|
|
||||||
|
@do_request()
|
||||||
|
def get_equipment(search: str = "", *, cursor) -> list[tuple]:
|
||||||
|
"""
|
||||||
|
Получить список снаряжения.
|
||||||
|
Возвращает: id, inv_number, supply_name, type, rent,
|
||||||
|
manufacturer, pickup_point, img_path, is_available
|
||||||
|
is_available = True если нет активной аренды (Новая/Подтверждена/Выдана)
|
||||||
|
"""
|
||||||
|
like = f"%{search.lower()}%"
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
c.id,
|
||||||
|
c.inv_number,
|
||||||
|
c.supply_name,
|
||||||
|
ct.type,
|
||||||
|
c.rent,
|
||||||
|
m.name AS manufacturer,
|
||||||
|
pp.name AS pick_up_point,
|
||||||
|
c.img_path,
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM rent r
|
||||||
|
WHERE r.commodity = c.id
|
||||||
|
AND r.status IN ('Новая','Подтверждена','Выдана')
|
||||||
|
) AS is_available
|
||||||
|
FROM commodity c
|
||||||
|
LEFT JOIN cmd_type ct ON ct.id = c.supply_type
|
||||||
|
LEFT JOIN manufacturer m ON m.id = c.manufacturer
|
||||||
|
LEFT JOIN pu_point pp ON pp.id = c.pick_up_point
|
||||||
|
WHERE LOWER(c.supply_name) LIKE %s
|
||||||
|
OR LOWER(ct.type) LIKE %s
|
||||||
|
ORDER BY c.id
|
||||||
|
""",
|
||||||
|
(like, like)
|
||||||
|
)
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
@do_request()
|
||||||
|
def get_equipment_by_id(eq_id: int, *, cursor) -> tuple | None:
|
||||||
|
"""Получить одну запись снаряжения по ID"""
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT c.id, c.inv_number, c.supply_name,
|
||||||
|
c.supply_type, c.rent, c.manufacturer,
|
||||||
|
c.pick_up_point, c.img_path
|
||||||
|
FROM commodity c
|
||||||
|
WHERE c.id = %s
|
||||||
|
""",
|
||||||
|
(eq_id,)
|
||||||
|
)
|
||||||
|
return cursor.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
@do_request(autocommit=True)
|
||||||
|
def insert_equipment(inv_number, supply_name, supply_type,
|
||||||
|
rent, manufacturer, pick_up_point,
|
||||||
|
img_path="", *, cursor) -> bool:
|
||||||
|
"""Добавить новое снаряжение"""
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO commodity
|
||||||
|
(inv_number, supply_name, supply_type, rent,
|
||||||
|
manufacturer, pick_up_point, img_path)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""",
|
||||||
|
(inv_number, supply_name, supply_type, rent,
|
||||||
|
manufacturer, pick_up_point, img_path)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@do_request(autocommit=True)
|
||||||
|
def update_equipment(eq_id, inv_number, supply_name, supply_type,
|
||||||
|
rent, manufacturer, pick_up_point,
|
||||||
|
img_path="", *, cursor) -> bool:
|
||||||
|
"""Обновить снаряжение"""
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE commodity
|
||||||
|
SET inv_number = %s,
|
||||||
|
supply_name = %s,
|
||||||
|
supply_type = %s,
|
||||||
|
rent = %s,
|
||||||
|
manufacturer = %s,
|
||||||
|
pick_up_point= %s,
|
||||||
|
img_path = %s
|
||||||
|
WHERE id = %s
|
||||||
|
""",
|
||||||
|
(inv_number, supply_name, supply_type, rent,
|
||||||
|
manufacturer, pick_up_point, img_path, eq_id)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@do_request(autocommit=True)
|
||||||
|
def delete_equipment(eq_id: int, *, cursor) -> bool:
|
||||||
|
"""Удалить снаряжение"""
|
||||||
|
cursor.execute("DELETE FROM commodity WHERE id = %s", (eq_id,))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@do_request()
|
||||||
|
def get_cmd_types(*, cursor) -> list[tuple]:
|
||||||
|
"""Получить все типы снаряжения"""
|
||||||
|
cursor.execute("SELECT id, type FROM cmd_type ORDER BY id")
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
@do_request()
|
||||||
|
def get_manufacturers(*, cursor) -> list[tuple]:
|
||||||
|
"""Получить всех производителей"""
|
||||||
|
cursor.execute("SELECT id, name FROM manufacturer ORDER BY id")
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
@do_request()
|
||||||
|
def get_pickup_points(*, cursor) -> list[tuple]:
|
||||||
|
"""Получить все пункты выдачи"""
|
||||||
|
cursor.execute("SELECT id, name FROM pu_point ORDER BY id")
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
@do_request()
|
||||||
|
def get_clients(*, cursor) -> list[tuple]:
|
||||||
|
"""Получить всех клиентов (id, name)"""
|
||||||
|
cursor.execute("SELECT id, name FROM client ORDER BY name")
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
@do_request()
|
||||||
|
def get_all_rents(status_filter: str | None = None, *, cursor) -> list[tuple]:
|
||||||
|
"""
|
||||||
|
Получить все заявки на аренду.
|
||||||
|
Возвращает: id, commodity_name, client_name, r_start, r_end, status, employee_name
|
||||||
|
"""
|
||||||
|
if status_filter and status_filter != "Все":
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT r.id,
|
||||||
|
c.supply_name,
|
||||||
|
cl.name,
|
||||||
|
r.r_start,
|
||||||
|
r.r_end,
|
||||||
|
r.status,
|
||||||
|
e.name
|
||||||
|
FROM rent r
|
||||||
|
JOIN commodity c ON c.id = r.commodity
|
||||||
|
JOIN client cl ON cl.id = r.client
|
||||||
|
LEFT JOIN employee e ON e.id = r.employee
|
||||||
|
WHERE r.status = %s
|
||||||
|
ORDER BY r.id DESC
|
||||||
|
""",
|
||||||
|
(status_filter,)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT r.id,
|
||||||
|
c.supply_name,
|
||||||
|
cl.name,
|
||||||
|
r.r_start,
|
||||||
|
r.r_end,
|
||||||
|
r.status,
|
||||||
|
e.name
|
||||||
|
FROM rent r
|
||||||
|
JOIN commodity c ON c.id = r.commodity
|
||||||
|
JOIN client cl ON cl.id = r.client
|
||||||
|
LEFT JOIN employee e ON e.id = r.employee
|
||||||
|
ORDER BY r.id DESC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
@do_request()
|
||||||
|
def get_user_rents(user_id: int, *, cursor) -> list[tuple]:
|
||||||
|
"""
|
||||||
|
Получить заявки конкретного клиента по его user_id.
|
||||||
|
Связь: users.name == client.name (упрощённая схема без FK).
|
||||||
|
"""
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT r.id,
|
||||||
|
c.supply_name,
|
||||||
|
cl.name,
|
||||||
|
r.r_start,
|
||||||
|
r.r_end,
|
||||||
|
r.status,
|
||||||
|
e.name
|
||||||
|
FROM rent r
|
||||||
|
JOIN commodity c ON c.id = r.commodity
|
||||||
|
JOIN client cl ON cl.id = r.client
|
||||||
|
LEFT JOIN employee e ON e.id = r.employee
|
||||||
|
JOIN users u ON u.name = cl.name
|
||||||
|
WHERE u.id = %s
|
||||||
|
ORDER BY r.id DESC
|
||||||
|
""",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
@do_request(autocommit=True)
|
||||||
|
def create_rent(commodity_id: int, client_id: int,
|
||||||
|
r_start: str, r_end: str,
|
||||||
|
employee_id: int | None = None, *, cursor) -> bool:
|
||||||
|
"""Создать новую заявку на аренду"""
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO rent (commodity, client, r_start, r_end, status, employee)
|
||||||
|
VALUES (%s, %s, %s, %s, 'Новая', %s)
|
||||||
|
""",
|
||||||
|
(commodity_id, client_id, r_start, r_end, employee_id)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@do_request(autocommit=True)
|
||||||
|
def update_rent_status(rent_id: int, status: str, *, cursor) -> bool:
|
||||||
|
"""Обновить статус заявки"""
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE rent SET status = %s WHERE id = %s",
|
||||||
|
(status, rent_id)
|
||||||
|
)
|
||||||
|
return True
|
||||||
20
src/objects.py
Normal file
20
src/objects.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from enum import Enum, auto
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
class SignalCode(Enum):
|
||||||
|
"""Коды сигналов для обработки ошибок"""
|
||||||
|
SIGFALSE = auto() # Неверные данные
|
||||||
|
SIGERR = auto() # Ошибка валидации
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Role:
|
||||||
|
"""Модель роли пользователя"""
|
||||||
|
name: str
|
||||||
|
level: int
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class User:
|
||||||
|
"""Модель пользователя"""
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
role: Role
|
||||||
76
src/utils.py
Normal file
76
src/utils.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget,
|
||||||
|
QTableView,
|
||||||
|
QHBoxLayout,
|
||||||
|
QVBoxLayout,
|
||||||
|
QDateEdit,
|
||||||
|
QPushButton,
|
||||||
|
QLabel,
|
||||||
|
QHeaderView,
|
||||||
|
QAbstractItemView,
|
||||||
|
)
|
||||||
|
from PyQt6.QtSql import QSqlTableModel
|
||||||
|
|
||||||
|
|
||||||
|
class TabWidgetCustom(QWidget):
|
||||||
|
"""
|
||||||
|
Универсальный виджет для CRUD операций с таблицей БД.
|
||||||
|
Отображает данные через QSqlTableModel с тулбаром действий.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, table_name: str, db, show_date_filter=False):
|
||||||
|
super().__init__()
|
||||||
|
self._name = table_name
|
||||||
|
self._db = db
|
||||||
|
self._show_date_filter = show_date_filter
|
||||||
|
self._setup()
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
self.root = QVBoxLayout(self)
|
||||||
|
|
||||||
|
if self._show_date_filter:
|
||||||
|
self.header = QHBoxLayout()
|
||||||
|
self.from_date = QDateEdit()
|
||||||
|
self.from_date.setCalendarPopup(True)
|
||||||
|
self.to_date = QDateEdit()
|
||||||
|
self.to_date.setCalendarPopup(True)
|
||||||
|
self.button_filter = QPushButton("Filter")
|
||||||
|
self.button_all = QPushButton("Show All")
|
||||||
|
self.header.addWidget(QLabel("From:"))
|
||||||
|
self.header.addWidget(self.from_date)
|
||||||
|
self.header.addWidget(QLabel("To:"))
|
||||||
|
self.header.addWidget(self.to_date)
|
||||||
|
self.header.addWidget(self.button_filter)
|
||||||
|
self.header.addWidget(self.button_all)
|
||||||
|
self.root.addLayout(self.header)
|
||||||
|
|
||||||
|
self.view = QTableView()
|
||||||
|
self.view.setSelectionBehavior(
|
||||||
|
QAbstractItemView.SelectionBehavior.SelectRows
|
||||||
|
)
|
||||||
|
self.view.horizontalHeader().setSectionResizeMode(
|
||||||
|
QHeaderView.ResizeMode.Stretch
|
||||||
|
)
|
||||||
|
self.view.setAlternatingRowColors(True)
|
||||||
|
|
||||||
|
self.btoolbar = QHBoxLayout()
|
||||||
|
self.button_add = QPushButton("+ Добавить")
|
||||||
|
self.button_del = QPushButton("- Удалить")
|
||||||
|
self.button_ok = QPushButton("✓ Применить")
|
||||||
|
self.button_deny = QPushButton("✗ Отменить")
|
||||||
|
self.button_csv = QPushButton("Экспорт CSV")
|
||||||
|
|
||||||
|
for btn in (self.button_add, self.button_del,
|
||||||
|
self.button_ok, self.button_deny, self.button_csv):
|
||||||
|
self.btoolbar.addWidget(btn)
|
||||||
|
|
||||||
|
self.root.addWidget(self.view)
|
||||||
|
self.root.addLayout(self.btoolbar)
|
||||||
|
self._setup_db()
|
||||||
|
|
||||||
|
def _setup_db(self):
|
||||||
|
self.model = QSqlTableModel(db=self._db)
|
||||||
|
self.model.setTable(self._name)
|
||||||
|
self.model.setEditStrategy(QSqlTableModel.EditStrategy.OnManualSubmit)
|
||||||
|
self.model.select()
|
||||||
|
self.view.setModel(self.model)
|
||||||
795
src/windows.py
Normal file
795
src/windows.py
Normal file
|
|
@ -0,0 +1,795 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import csv
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QMainWindow, QTabWidget, QTableWidget, QTableWidgetItem,
|
||||||
|
QHeaderView, QAbstractItemView,
|
||||||
|
QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||||
|
QGroupBox, QLabel, QPushButton, QLineEdit,
|
||||||
|
QComboBox, QDateEdit, QDoubleSpinBox,
|
||||||
|
QMessageBox, QDialog, QDialogButtonBox,
|
||||||
|
QFileDialog
|
||||||
|
)
|
||||||
|
from PyQt6.QtGui import QPixmap, QColor
|
||||||
|
from PyQt6.QtCore import Qt, QDate, pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
|
from .objects import User, SignalCode
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
RENT_STATUSES = ["Новая", "Подтверждена", "Выдана", "Завершена", "Отменена"]
|
||||||
|
ASSETS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets")
|
||||||
|
|
||||||
|
|
||||||
|
def _make_table(headers: list[str]) -> QTableWidget:
|
||||||
|
"""Вспомогательная функция: создать настроенный QTableWidget"""
|
||||||
|
t = QTableWidget()
|
||||||
|
t.setColumnCount(len(headers))
|
||||||
|
t.setHorizontalHeaderLabels(headers)
|
||||||
|
t.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
|
t.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
t.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
|
t.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||||
|
t.verticalHeader().setVisible(False)
|
||||||
|
t.setAlternatingRowColors(True)
|
||||||
|
return t
|
||||||
|
|
||||||
|
class BaseWindow(QMainWindow):
|
||||||
|
"""Базовый класс для всех окон приложения"""
|
||||||
|
|
||||||
|
def __init__(self, composer, db_conn, user: User | None = None):
|
||||||
|
super().__init__()
|
||||||
|
self._composer = composer
|
||||||
|
self._db = db_conn
|
||||||
|
self._user = user
|
||||||
|
self._define_widgets()
|
||||||
|
self._tune_layouts()
|
||||||
|
self._connect_slots()
|
||||||
|
self._apply_window_settings()
|
||||||
|
|
||||||
|
def _define_widgets(self): pass
|
||||||
|
def _tune_layouts(self): pass
|
||||||
|
def _connect_slots(self): pass
|
||||||
|
def _apply_window_settings(self): pass
|
||||||
|
|
||||||
|
class LoginWindow(BaseWindow):
|
||||||
|
"""Окно авторизации с кнопкой гостевого входа"""
|
||||||
|
|
||||||
|
login_success = pyqtSignal(User)
|
||||||
|
login_forbidden = pyqtSignal(SignalCode)
|
||||||
|
|
||||||
|
def _define_widgets(self):
|
||||||
|
self.root = QWidget()
|
||||||
|
|
||||||
|
self.auth_form = QGroupBox("Вход в систему")
|
||||||
|
self.auth_form.setAlignment(
|
||||||
|
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
||||||
|
)
|
||||||
|
|
||||||
|
self.login_line = QLineEdit()
|
||||||
|
self.login_line.setFixedWidth(200)
|
||||||
|
self.login_line.setPlaceholderText("Логин")
|
||||||
|
|
||||||
|
self.password_line = QLineEdit()
|
||||||
|
self.password_line.setFixedWidth(200)
|
||||||
|
self.password_line.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
self.password_line.setPlaceholderText("Пароль")
|
||||||
|
|
||||||
|
self.auth_button = QPushButton("Войти")
|
||||||
|
self.guest_button = QPushButton("Продолжить как гость")
|
||||||
|
|
||||||
|
def _tune_layouts(self):
|
||||||
|
form = QFormLayout()
|
||||||
|
form.addRow("Логин:", self.login_line)
|
||||||
|
form.addRow("Пароль:", self.password_line)
|
||||||
|
self.auth_form.setLayout(form)
|
||||||
|
|
||||||
|
root_l = QVBoxLayout()
|
||||||
|
root_l.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
root_l.addWidget(self.auth_form)
|
||||||
|
root_l.addWidget(self.auth_button)
|
||||||
|
root_l.addWidget(self.guest_button)
|
||||||
|
self.root.setLayout(root_l)
|
||||||
|
self.setCentralWidget(self.root)
|
||||||
|
|
||||||
|
def _connect_slots(self):
|
||||||
|
self.auth_button.clicked.connect(self._on_auth)
|
||||||
|
self.guest_button.clicked.connect(self._on_guest)
|
||||||
|
self.login_success.connect(self._on_login_success)
|
||||||
|
self.login_forbidden.connect(self._on_login_forbidden)
|
||||||
|
self.password_line.returnPressed.connect(self._on_auth)
|
||||||
|
|
||||||
|
def _on_auth(self):
|
||||||
|
login = self.login_line.text().strip()
|
||||||
|
password = self.password_line.text()
|
||||||
|
|
||||||
|
if not login or not password:
|
||||||
|
self.login_forbidden.emit(SignalCode.SIGERR)
|
||||||
|
return
|
||||||
|
|
||||||
|
user = db.auth(login, password)
|
||||||
|
if not user:
|
||||||
|
self.login_forbidden.emit(SignalCode.SIGFALSE)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.login_success.emit(user)
|
||||||
|
|
||||||
|
def _on_guest(self):
|
||||||
|
# Гость — пользователь с нулевым уровнем без записи в БД
|
||||||
|
from .objects import Role
|
||||||
|
guest_role = Role(name="guest", level=0)
|
||||||
|
guest_user = User(id=0, name="Гость", role=guest_role)
|
||||||
|
self.login_success.emit(guest_user)
|
||||||
|
|
||||||
|
@pyqtSlot(User)
|
||||||
|
def _on_login_success(self, user: User):
|
||||||
|
self._composer.render_request.emit(user)
|
||||||
|
|
||||||
|
@pyqtSlot(SignalCode)
|
||||||
|
def _on_login_forbidden(self, code: SignalCode):
|
||||||
|
match code:
|
||||||
|
case SignalCode.SIGFALSE:
|
||||||
|
QMessageBox.warning(self, "Ошибка", "Неверный логин или пароль")
|
||||||
|
case SignalCode.SIGERR:
|
||||||
|
QMessageBox.critical(self, "Ошибка ввода",
|
||||||
|
"Введите логин и пароль")
|
||||||
|
|
||||||
|
def _apply_window_settings(self):
|
||||||
|
self.setWindowTitle("Вход — Аренда спортивного инвентаря")
|
||||||
|
self.setFixedSize(320, 200)
|
||||||
|
|
||||||
|
class MainWindow(BaseWindow):
|
||||||
|
"""
|
||||||
|
Единое окно приложения.
|
||||||
|
Содержит два таба: «Снаряжение» и «Заявки».
|
||||||
|
Доступность элементов управления определяется уровнем прав пользователя.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _define_widgets(self):
|
||||||
|
self.root = QTabWidget()
|
||||||
|
|
||||||
|
self.eq_widget = QWidget()
|
||||||
|
|
||||||
|
# Поиск
|
||||||
|
self.eq_search_input = QLineEdit()
|
||||||
|
self.eq_search_input.setPlaceholderText("Поиск по названию или типу…")
|
||||||
|
self.eq_search_btn = QPushButton("Найти")
|
||||||
|
self.eq_show_all_btn = QPushButton("Показать все")
|
||||||
|
|
||||||
|
# Таблица
|
||||||
|
self.eq_table = _make_table([
|
||||||
|
"ID", "Инв. №", "Наименование", "Тип",
|
||||||
|
"Аренда/сут", "Производитель", "Пункт выдачи", "Доступно"
|
||||||
|
])
|
||||||
|
|
||||||
|
# Превью фото
|
||||||
|
self.eq_img_label = QLabel("Выберите снаряжение")
|
||||||
|
self.eq_img_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.eq_img_label.setFixedHeight(160)
|
||||||
|
self.eq_img_label.setStyleSheet(
|
||||||
|
"border: 1px solid #ccc; background: #f5f5f5;"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кнопки CRUD — только для admin (level=100)
|
||||||
|
self.eq_add_btn = QPushButton("➕ Добавить")
|
||||||
|
self.eq_edit_btn = QPushButton("✏️ Редактировать")
|
||||||
|
self.eq_del_btn = QPushButton("🗑️ Удалить")
|
||||||
|
self.eq_csv_btn = QPushButton("Экспорт CSV")
|
||||||
|
|
||||||
|
# ── Таб «Заявки» ──────────────────────────────────────
|
||||||
|
self.rent_widget = QWidget()
|
||||||
|
|
||||||
|
self.rent_status_filter = QComboBox()
|
||||||
|
self.rent_status_filter.addItem("Все")
|
||||||
|
self.rent_status_filter.addItems(RENT_STATUSES)
|
||||||
|
|
||||||
|
self.rent_filter_btn = QPushButton("Применить фильтр")
|
||||||
|
self.rent_refresh_btn = QPushButton("Обновить")
|
||||||
|
|
||||||
|
self.rent_table = _make_table([
|
||||||
|
"ID", "Снаряжение", "Клиент",
|
||||||
|
"Начало", "Конец", "Статус", "Сотрудник"
|
||||||
|
])
|
||||||
|
|
||||||
|
# Кнопки управления заявками
|
||||||
|
self.rent_create_btn = QPushButton("➕ Новая заявка")
|
||||||
|
self.rent_approve_btn = QPushButton("✓ Подтвердить")
|
||||||
|
self.rent_issue_btn = QPushButton("📦 Выдать")
|
||||||
|
self.rent_complete_btn = QPushButton("✅ Завершить")
|
||||||
|
self.rent_cancel_btn = QPushButton("✗ Отменить")
|
||||||
|
|
||||||
|
self.rent_approve_btn.setStyleSheet("background:#90EE90;")
|
||||||
|
self.rent_issue_btn.setStyleSheet("background:#87CEEB;")
|
||||||
|
self.rent_complete_btn.setStyleSheet("background:#98FB98;")
|
||||||
|
self.rent_cancel_btn.setStyleSheet("background:#FFB6C6;")
|
||||||
|
|
||||||
|
def _tune_layouts(self):
|
||||||
|
# ── Снаряжение ────────────────────────────────────────
|
||||||
|
eq_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
search_row = QHBoxLayout()
|
||||||
|
search_row.addWidget(self.eq_search_input)
|
||||||
|
search_row.addWidget(self.eq_search_btn)
|
||||||
|
search_row.addWidget(self.eq_show_all_btn)
|
||||||
|
eq_layout.addLayout(search_row)
|
||||||
|
eq_layout.addWidget(self.eq_table)
|
||||||
|
eq_layout.addWidget(self.eq_img_label)
|
||||||
|
|
||||||
|
eq_btn_row = QHBoxLayout()
|
||||||
|
eq_btn_row.addWidget(self.eq_add_btn)
|
||||||
|
eq_btn_row.addWidget(self.eq_edit_btn)
|
||||||
|
eq_btn_row.addWidget(self.eq_del_btn)
|
||||||
|
eq_btn_row.addStretch()
|
||||||
|
eq_btn_row.addWidget(self.eq_csv_btn)
|
||||||
|
eq_layout.addLayout(eq_btn_row)
|
||||||
|
|
||||||
|
self.eq_widget.setLayout(eq_layout)
|
||||||
|
|
||||||
|
# ── Заявки ────────────────────────────────────────────
|
||||||
|
rent_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
filter_row = QHBoxLayout()
|
||||||
|
filter_row.addWidget(QLabel("Статус:"))
|
||||||
|
filter_row.addWidget(self.rent_status_filter)
|
||||||
|
filter_row.addWidget(self.rent_filter_btn)
|
||||||
|
filter_row.addWidget(self.rent_refresh_btn)
|
||||||
|
filter_row.addStretch()
|
||||||
|
rent_layout.addLayout(filter_row)
|
||||||
|
rent_layout.addWidget(self.rent_table)
|
||||||
|
|
||||||
|
rent_btn_row = QHBoxLayout()
|
||||||
|
rent_btn_row.addWidget(self.rent_create_btn)
|
||||||
|
rent_btn_row.addWidget(self.rent_approve_btn)
|
||||||
|
rent_btn_row.addWidget(self.rent_issue_btn)
|
||||||
|
rent_btn_row.addWidget(self.rent_complete_btn)
|
||||||
|
rent_btn_row.addWidget(self.rent_cancel_btn)
|
||||||
|
rent_btn_row.addStretch()
|
||||||
|
rent_layout.addLayout(rent_btn_row)
|
||||||
|
|
||||||
|
self.rent_widget.setLayout(rent_layout)
|
||||||
|
|
||||||
|
# ── Корень ────────────────────────────────────────────
|
||||||
|
self.root.addTab(self.eq_widget, "🏄 Снаряжение")
|
||||||
|
self.root.addTab(self.rent_widget, "📋 Заявки")
|
||||||
|
self.setCentralWidget(self.root)
|
||||||
|
|
||||||
|
def _connect_slots(self):
|
||||||
|
# Снаряжение
|
||||||
|
self.eq_search_btn.clicked.connect(self._load_equipment)
|
||||||
|
self.eq_show_all_btn.clicked.connect(self._reset_eq_search)
|
||||||
|
self.eq_search_input.returnPressed.connect(self._load_equipment)
|
||||||
|
self.eq_table.selectionModel().selectionChanged.connect(
|
||||||
|
self._on_eq_selection
|
||||||
|
)
|
||||||
|
self.eq_add_btn.clicked.connect(self._eq_add)
|
||||||
|
self.eq_edit_btn.clicked.connect(self._eq_edit)
|
||||||
|
self.eq_del_btn.clicked.connect(self._eq_delete)
|
||||||
|
self.eq_csv_btn.clicked.connect(self._eq_csv_export)
|
||||||
|
|
||||||
|
# Заявки
|
||||||
|
self.rent_filter_btn.clicked.connect(self._load_rents)
|
||||||
|
self.rent_refresh_btn.clicked.connect(self._load_rents)
|
||||||
|
self.rent_create_btn.clicked.connect(self._rent_create)
|
||||||
|
self.rent_approve_btn.clicked.connect(
|
||||||
|
lambda: self._rent_set_status("Подтверждена")
|
||||||
|
)
|
||||||
|
self.rent_issue_btn.clicked.connect(
|
||||||
|
lambda: self._rent_set_status("Выдана")
|
||||||
|
)
|
||||||
|
self.rent_complete_btn.clicked.connect(
|
||||||
|
lambda: self._rent_set_status("Завершена")
|
||||||
|
)
|
||||||
|
self.rent_cancel_btn.clicked.connect(
|
||||||
|
lambda: self._rent_set_status("Отменена")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _apply_window_settings(self):
|
||||||
|
role_label = self._user.role.name if self._user else "—"
|
||||||
|
self.setWindowTitle(
|
||||||
|
f"Аренда спортивного инвентаря | "
|
||||||
|
f"{self._user.name} [{role_label}]"
|
||||||
|
)
|
||||||
|
self.setMinimumSize(1100, 700)
|
||||||
|
self._apply_permissions()
|
||||||
|
self._load_equipment()
|
||||||
|
self._load_rents()
|
||||||
|
|
||||||
|
def _apply_permissions(self):
|
||||||
|
"""Включить/выключить элементы управления согласно правам пользователя"""
|
||||||
|
user = self._user
|
||||||
|
|
||||||
|
# Снаряжение: CRUD только для admin
|
||||||
|
can_edit_eq = db.can(user, "create.equipment")
|
||||||
|
self.eq_add_btn.setVisible(can_edit_eq)
|
||||||
|
self.eq_edit_btn.setVisible(can_edit_eq)
|
||||||
|
self.eq_del_btn.setVisible(can_edit_eq)
|
||||||
|
|
||||||
|
# Заявки: таб виден всем кроме гостя (level 0)
|
||||||
|
can_see_rents = user.role.level > 0
|
||||||
|
self.root.setTabVisible(1, can_see_rents)
|
||||||
|
|
||||||
|
# Создать заявку: client (25) и выше
|
||||||
|
can_create_rent = db.can(user, "create.requests")
|
||||||
|
self.rent_create_btn.setVisible(can_create_rent)
|
||||||
|
|
||||||
|
# Менять статус: employee (50) и выше
|
||||||
|
can_change_status = db.can(user, "update.request.status")
|
||||||
|
for btn in (self.rent_approve_btn, self.rent_issue_btn,
|
||||||
|
self.rent_complete_btn, self.rent_cancel_btn):
|
||||||
|
btn.setVisible(can_change_status)
|
||||||
|
|
||||||
|
# ── Снаряжение ────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _load_equipment(self):
|
||||||
|
search = self.eq_search_input.text().strip()
|
||||||
|
rows = db.get_equipment(search)
|
||||||
|
self.eq_table.setRowCount(0)
|
||||||
|
|
||||||
|
for row_data in (rows or []):
|
||||||
|
r = self.eq_table.rowCount()
|
||||||
|
self.eq_table.insertRow(r)
|
||||||
|
|
||||||
|
# id, inv_number, supply_name, type, rent,
|
||||||
|
# manufacturer, pick_up_point, img_path, is_available
|
||||||
|
display = [
|
||||||
|
str(row_data[0]), # ID
|
||||||
|
str(row_data[1]), # inv_number
|
||||||
|
str(row_data[2]), # supply_name
|
||||||
|
str(row_data[3] or ""), # type
|
||||||
|
f"{row_data[4]:.2f} ₽", # rent
|
||||||
|
str(row_data[5] or ""), # manufacturer
|
||||||
|
str(row_data[6] or ""), # pick_up_point
|
||||||
|
"Да" if row_data[8] else "Нет", # is_available
|
||||||
|
]
|
||||||
|
for col, val in enumerate(display):
|
||||||
|
item = QTableWidgetItem(val)
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole, row_data)
|
||||||
|
if not row_data[8]: # недоступно — серый фон
|
||||||
|
item.setBackground(QColor("#d0d0d0"))
|
||||||
|
self.eq_table.setItem(r, col, item)
|
||||||
|
|
||||||
|
def _reset_eq_search(self):
|
||||||
|
self.eq_search_input.clear()
|
||||||
|
self._load_equipment()
|
||||||
|
|
||||||
|
def _on_eq_selection(self):
|
||||||
|
row = self.eq_table.currentRow()
|
||||||
|
if row < 0:
|
||||||
|
return
|
||||||
|
item = self.eq_table.item(row, 0)
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
row_data = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
if row_data:
|
||||||
|
self._show_eq_image(row_data[7]) # img_path
|
||||||
|
|
||||||
|
def _show_eq_image(self, img_path: str):
|
||||||
|
if not img_path:
|
||||||
|
self.eq_img_label.setText("Нет изображения")
|
||||||
|
return
|
||||||
|
full = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(__file__)),
|
||||||
|
img_path
|
||||||
|
)
|
||||||
|
if os.path.exists(full):
|
||||||
|
px = QPixmap(full).scaled(
|
||||||
|
self.eq_img_label.width(),
|
||||||
|
self.eq_img_label.height(),
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation,
|
||||||
|
)
|
||||||
|
self.eq_img_label.setPixmap(px)
|
||||||
|
else:
|
||||||
|
self.eq_img_label.setText(f"Файл не найден:\n{img_path}")
|
||||||
|
|
||||||
|
def _selected_eq_data(self) -> tuple | None:
|
||||||
|
row = self.eq_table.currentRow()
|
||||||
|
if row < 0:
|
||||||
|
return None
|
||||||
|
item = self.eq_table.item(row, 0)
|
||||||
|
return item.data(Qt.ItemDataRole.UserRole) if item else None
|
||||||
|
|
||||||
|
def _eq_add(self):
|
||||||
|
dlg = EquipmentDialog(parent=self)
|
||||||
|
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
self._load_equipment()
|
||||||
|
|
||||||
|
def _eq_edit(self):
|
||||||
|
row_data = self._selected_eq_data()
|
||||||
|
if not row_data:
|
||||||
|
QMessageBox.warning(self, "Выбор", "Выберите снаряжение для редактирования")
|
||||||
|
return
|
||||||
|
dlg = EquipmentDialog(eq_id=row_data[0], parent=self)
|
||||||
|
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
self._load_equipment()
|
||||||
|
|
||||||
|
def _eq_delete(self):
|
||||||
|
row_data = self._selected_eq_data()
|
||||||
|
if not row_data:
|
||||||
|
QMessageBox.warning(self, "Выбор", "Выберите снаряжение для удаления")
|
||||||
|
return
|
||||||
|
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self, "Подтверждение",
|
||||||
|
f"Удалить «{row_data[2]}»?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
result = db.delete_equipment(row_data[0])
|
||||||
|
if result:
|
||||||
|
self._load_equipment()
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Ошибка",
|
||||||
|
"Не удалось удалить запись.")
|
||||||
|
|
||||||
|
def _eq_csv_export(self):
|
||||||
|
headers = [
|
||||||
|
self.eq_table.horizontalHeaderItem(i).text()
|
||||||
|
for i in range(self.eq_table.columnCount())
|
||||||
|
]
|
||||||
|
filename = "equipment_export.csv"
|
||||||
|
try:
|
||||||
|
with open(filename, "w", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerow(headers)
|
||||||
|
for r in range(self.eq_table.rowCount()):
|
||||||
|
writer.writerow([
|
||||||
|
self.eq_table.item(r, c).text()
|
||||||
|
if self.eq_table.item(r, c) else ""
|
||||||
|
for c in range(self.eq_table.columnCount())
|
||||||
|
])
|
||||||
|
QMessageBox.information(self, "Экспорт",
|
||||||
|
f"Данные сохранены в {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Ошибка экспорта", str(e))
|
||||||
|
|
||||||
|
# ── Заявки ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _load_rents(self):
|
||||||
|
status = self.rent_status_filter.currentText()
|
||||||
|
|
||||||
|
# Клиент видит только свои заявки
|
||||||
|
if self._user.role.level < 50:
|
||||||
|
rows = db.get_user_rents(self._user.id)
|
||||||
|
else:
|
||||||
|
rows = db.get_all_rents(
|
||||||
|
status_filter=status if status != "Все" else None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.rent_table.setRowCount(0)
|
||||||
|
for row_data in (rows or []):
|
||||||
|
r = self.rent_table.rowCount()
|
||||||
|
self.rent_table.insertRow(r)
|
||||||
|
for col, val in enumerate(row_data):
|
||||||
|
item = QTableWidgetItem(str(val) if val is not None else "")
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole, row_data[0]) # ID
|
||||||
|
self.rent_table.setItem(r, col, item)
|
||||||
|
|
||||||
|
def _selected_rent_id(self) -> int | None:
|
||||||
|
row = self.rent_table.currentRow()
|
||||||
|
if row < 0:
|
||||||
|
QMessageBox.warning(self, "Выбор", "Выберите заявку")
|
||||||
|
return None
|
||||||
|
item = self.rent_table.item(row, 0)
|
||||||
|
return int(item.text()) if item else None
|
||||||
|
|
||||||
|
def _rent_create(self):
|
||||||
|
dlg = RentDialog(user=self._user, parent=self)
|
||||||
|
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
self._load_rents()
|
||||||
|
|
||||||
|
def _rent_set_status(self, status: str):
|
||||||
|
rent_id = self._selected_rent_id()
|
||||||
|
if rent_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self, "Подтверждение",
|
||||||
|
f"Установить статус «{status}» для заявки #{rent_id}?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
result = db.update_rent_status(rent_id, status)
|
||||||
|
if result:
|
||||||
|
self._load_rents()
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Ошибка",
|
||||||
|
"Не удалось обновить статус.")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# EquipmentDialog – добавление / редактирование снаряжения
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class EquipmentDialog(QDialog):
|
||||||
|
def __init__(self, eq_id: int | None = None, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._eq_id = eq_id
|
||||||
|
self._img_path = ""
|
||||||
|
self.setWindowTitle(
|
||||||
|
"Редактировать снаряжение" if eq_id else "Добавить снаряжение"
|
||||||
|
)
|
||||||
|
self.setMinimumWidth(460)
|
||||||
|
self._build_ui()
|
||||||
|
self._load_lookups()
|
||||||
|
if eq_id:
|
||||||
|
self._load_data(eq_id)
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
form = QFormLayout()
|
||||||
|
|
||||||
|
self._id_field = QLineEdit()
|
||||||
|
self._id_field.setReadOnly(True)
|
||||||
|
self._id_field.setPlaceholderText("Автоматически")
|
||||||
|
form.addRow("ID:", self._id_field)
|
||||||
|
|
||||||
|
self._inv = QLineEdit()
|
||||||
|
self._inv.setMaxLength(4)
|
||||||
|
form.addRow("Инв. номер *:", self._inv)
|
||||||
|
|
||||||
|
self._name = QLineEdit()
|
||||||
|
form.addRow("Наименование *:", self._name)
|
||||||
|
|
||||||
|
self._type_cb = QComboBox()
|
||||||
|
form.addRow("Тип *:", self._type_cb)
|
||||||
|
|
||||||
|
self._rent = QDoubleSpinBox()
|
||||||
|
self._rent.setRange(0, 99999)
|
||||||
|
self._rent.setDecimals(2)
|
||||||
|
self._rent.setSuffix(" ₽")
|
||||||
|
form.addRow("Аренда/сут *:", self._rent)
|
||||||
|
|
||||||
|
self._manuf_cb = QComboBox()
|
||||||
|
form.addRow("Производитель *:", self._manuf_cb)
|
||||||
|
|
||||||
|
self._point_cb = QComboBox()
|
||||||
|
form.addRow("Пункт выдачи *:", self._point_cb)
|
||||||
|
|
||||||
|
# Фото
|
||||||
|
img_row = QHBoxLayout()
|
||||||
|
self._img_preview = QLabel("Нет фото")
|
||||||
|
self._img_preview.setFixedSize(100, 100)
|
||||||
|
self._img_preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self._img_preview.setStyleSheet(
|
||||||
|
"border:1px solid #aaa; background:#eee;"
|
||||||
|
)
|
||||||
|
btn_pick = QPushButton("Выбрать фото…")
|
||||||
|
btn_pick.clicked.connect(self._pick_image)
|
||||||
|
img_row.addWidget(self._img_preview)
|
||||||
|
img_row.addWidget(btn_pick)
|
||||||
|
form.addRow("Фото:", img_row)
|
||||||
|
|
||||||
|
layout.addLayout(form)
|
||||||
|
|
||||||
|
btns = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Save |
|
||||||
|
QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
btns.accepted.connect(self._save)
|
||||||
|
btns.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(btns)
|
||||||
|
|
||||||
|
def _load_lookups(self):
|
||||||
|
for tid, tname in (db.get_cmd_types() or []):
|
||||||
|
self._type_cb.addItem(tname, tid)
|
||||||
|
for mid, mname in (db.get_manufacturers() or []):
|
||||||
|
self._manuf_cb.addItem(mname, mid)
|
||||||
|
for pid, pname in (db.get_pickup_points() or []):
|
||||||
|
self._point_cb.addItem(pname, pid)
|
||||||
|
|
||||||
|
def _load_data(self, eq_id: int):
|
||||||
|
row = db.get_equipment_by_id(eq_id)
|
||||||
|
if not row:
|
||||||
|
return
|
||||||
|
# id, inv_number, supply_name, supply_type, rent,
|
||||||
|
# manufacturer, pick_up_point, img_path
|
||||||
|
self._id_field.setText(str(row[0]))
|
||||||
|
self._inv.setText(str(row[1]))
|
||||||
|
self._name.setText(str(row[2]))
|
||||||
|
self._set_combo(self._type_cb, row[3])
|
||||||
|
self._rent.setValue(float(row[4]))
|
||||||
|
self._set_combo(self._manuf_cb, row[5])
|
||||||
|
self._set_combo(self._point_cb, row[6])
|
||||||
|
self._img_path = row[7] or ""
|
||||||
|
self._refresh_preview()
|
||||||
|
|
||||||
|
def _set_combo(self, cb: QComboBox, value):
|
||||||
|
for i in range(cb.count()):
|
||||||
|
if cb.itemData(i) == value:
|
||||||
|
cb.setCurrentIndex(i)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _pick_image(self):
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self, "Выбрать изображение", "",
|
||||||
|
"Изображения (*.png *.jpg *.jpeg *.bmp)"
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
self._img_path = os.path.relpath(path)
|
||||||
|
self._refresh_preview()
|
||||||
|
|
||||||
|
def _refresh_preview(self):
|
||||||
|
full = os.path.abspath(self._img_path) if self._img_path else ""
|
||||||
|
if self._img_path and os.path.exists(full):
|
||||||
|
px = QPixmap(full).scaled(
|
||||||
|
100, 100, Qt.AspectRatioMode.KeepAspectRatio
|
||||||
|
)
|
||||||
|
self._img_preview.setPixmap(px)
|
||||||
|
else:
|
||||||
|
self._img_preview.setText("Нет фото")
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
inv = self._inv.text().strip()
|
||||||
|
name = self._name.text().strip()
|
||||||
|
ttype = self._type_cb.currentData()
|
||||||
|
rent = self._rent.value()
|
||||||
|
manuf = self._manuf_cb.currentData()
|
||||||
|
point = self._point_cb.currentData()
|
||||||
|
img = self._img_path
|
||||||
|
|
||||||
|
if not inv or not name:
|
||||||
|
QMessageBox.warning(self, "Ошибка",
|
||||||
|
"Инв. номер и наименование обязательны.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._eq_id:
|
||||||
|
result = db.update_equipment(
|
||||||
|
self._eq_id, inv, name, ttype, rent, manuf, point, img
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = db.insert_equipment(
|
||||||
|
inv, name, ttype, rent, manuf, point, img
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.accept()
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Ошибка БД",
|
||||||
|
"Не удалось сохранить запись.")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# RentDialog – создание заявки на аренду
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class RentDialog(QDialog):
|
||||||
|
def __init__(self, user: User, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._user = user
|
||||||
|
self.setWindowTitle("Новая заявка на аренду")
|
||||||
|
self.setMinimumWidth(400)
|
||||||
|
self._build_ui()
|
||||||
|
self._load_data()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
box = QGroupBox("Параметры аренды")
|
||||||
|
form = QFormLayout(box)
|
||||||
|
|
||||||
|
self._eq_cb = QComboBox()
|
||||||
|
form.addRow("Снаряжение *:", self._eq_cb)
|
||||||
|
|
||||||
|
# Для сотрудника и выше — выбор клиента
|
||||||
|
# Для клиента — только своё имя (нередактируемо)
|
||||||
|
self._client_cb = QComboBox()
|
||||||
|
form.addRow("Клиент *:", self._client_cb)
|
||||||
|
|
||||||
|
self._date_from = QDateEdit(QDate.currentDate())
|
||||||
|
self._date_from.setCalendarPopup(True)
|
||||||
|
self._date_from.setDisplayFormat("dd.MM.yyyy")
|
||||||
|
form.addRow("Начало аренды *:", self._date_from)
|
||||||
|
|
||||||
|
self._date_to = QDateEdit(QDate.currentDate().addDays(1))
|
||||||
|
self._date_to.setCalendarPopup(True)
|
||||||
|
self._date_to.setDisplayFormat("dd.MM.yyyy")
|
||||||
|
form.addRow("Конец аренды *:", self._date_to)
|
||||||
|
|
||||||
|
layout.addWidget(box)
|
||||||
|
|
||||||
|
btns = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Save |
|
||||||
|
QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
btns.accepted.connect(self._save)
|
||||||
|
btns.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(btns)
|
||||||
|
|
||||||
|
def _load_data(self):
|
||||||
|
# Только доступное снаряжение
|
||||||
|
equipment = db.get_equipment()
|
||||||
|
for row in (equipment or []):
|
||||||
|
if row[8]: # is_available
|
||||||
|
self._eq_cb.addItem(f"[{row[1]}] {row[2]}", row[0])
|
||||||
|
|
||||||
|
# Список клиентов
|
||||||
|
clients = db.get_clients()
|
||||||
|
if self._user.role.level >= 50:
|
||||||
|
# Сотрудник видит всех клиентов
|
||||||
|
for cid, cname in (clients or []):
|
||||||
|
self._client_cb.addItem(cname, cid)
|
||||||
|
else:
|
||||||
|
# Клиент видит только себя — ищем совпадение по имени в users
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
import psycopg2
|
||||||
|
from .db import DB_CONFIG
|
||||||
|
conn = psycopg2.connect(**DB_CONFIG)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT cl.id, cl.name FROM client cl
|
||||||
|
JOIN users u ON u.name = cl.name
|
||||||
|
WHERE u.id = %s
|
||||||
|
""",
|
||||||
|
(self._user.id,)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
self._client_cb.addItem(row[1], row[0])
|
||||||
|
self._client_cb.setEnabled(False)
|
||||||
|
else:
|
||||||
|
self._client_cb.addItem(self._user.name, None)
|
||||||
|
self._client_cb.setEnabled(False)
|
||||||
|
except Exception:
|
||||||
|
self._client_cb.addItem(self._user.name, None)
|
||||||
|
self._client_cb.setEnabled(False)
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
eq_id = self._eq_cb.currentData()
|
||||||
|
client_id = self._client_cb.currentData()
|
||||||
|
|
||||||
|
qd_from = self._date_from.date()
|
||||||
|
qd_to = self._date_to.date()
|
||||||
|
date_from = f"{qd_from.year()}-{qd_from.month():02d}-{qd_from.day():02d}"
|
||||||
|
date_to = f"{qd_to.year()}-{qd_to.month():02d}-{qd_to.day():02d}"
|
||||||
|
|
||||||
|
if not eq_id:
|
||||||
|
QMessageBox.warning(self, "Ошибка", "Нет доступного снаряжения.")
|
||||||
|
return
|
||||||
|
if not client_id:
|
||||||
|
QMessageBox.warning(self, "Ошибка",
|
||||||
|
"Не удалось определить клиента.")
|
||||||
|
return
|
||||||
|
if self._date_to.date() <= self._date_from.date():
|
||||||
|
QMessageBox.warning(self, "Ошибка",
|
||||||
|
"Дата окончания должна быть позже даты начала.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Привязываем сотрудника если пользователь — сотрудник
|
||||||
|
employee_id = None
|
||||||
|
if self._user.role.level >= 50 and self._user.id > 0:
|
||||||
|
try:
|
||||||
|
import psycopg2
|
||||||
|
from .db import DB_CONFIG
|
||||||
|
conn = psycopg2.connect(**DB_CONFIG)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id FROM employee WHERE name = "
|
||||||
|
"(SELECT name FROM users WHERE id = %s)",
|
||||||
|
(self._user.id,)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
employee_id = row[0]
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
result = db.create_rent(
|
||||||
|
commodity_id=eq_id,
|
||||||
|
client_id=client_id,
|
||||||
|
r_start=date_from,
|
||||||
|
r_end=date_to,
|
||||||
|
employee_id=employee_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.accept()
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Ошибка БД",
|
||||||
|
"Не удалось создать заявку.")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue