This commit is contained in:
andrei
2026-05-18 12:00:21 +07:00
commit 60486d5ed8
11 changed files with 1262 additions and 0 deletions
Binary file not shown.
+26
View File
@@ -0,0 +1,26 @@
-- ============================================================
-- 00_create_user.sql
-- Создание пользователя для семестровой работы
-- Выполнять под SYS / SYSTEM / SYSDBA
-- ============================================================
DROP USER student CASCADE;
CREATE USER student IDENTIFIED BY 123456
DEFAULT TABLESPACE USERS
QUOTA UNLIMITED ON USERS;
GRANT CREATE SESSION,
CREATE TABLE,
CREATE SEQUENCE,
CREATE TRIGGER,
CREATE PROCEDURE,
CREATE VIEW,
CREATE TYPE,
CREATE SYNONYM
TO student;
-- Для работы JSON_OBJECT (Oracle 12c+)
GRANT EXECUTE ON SYS.DBMS_SQL TO student;
COMMIT;
+62
View File
@@ -0,0 +1,62 @@
-- 01_create_tables.sql
-- Информационная система магазина автозапчастей
DROP TABLE purchase_orders PURGE;
DROP TABLE products PURGE;
DROP TABLE suppliers PURGE;
-- 1 Поставщики (suppliers)
CREATE TABLE suppliers (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR2(255) NOT NULL,
category VARCHAR2(100),
contact_info VARCHAR2(255),
has_guarantee CHAR(1) CHECK (has_guarantee IN ('Y', 'N')),
contract_id VARCHAR2(50)
);
-- 2 Товары (products)
CREATE TABLE products (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR2(255) NOT NULL,
article VARCHAR2(100) UNIQUE NOT NULL
);
-- 3 Заказы поставщикам (purchase_orders)
CREATE TABLE purchase_orders (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
supplier_id NUMBER NOT NULL,
order_date DATE NOT NULL,
CONSTRAINT fk_po_suppliers FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
);
-- Данные ----------------------------------------------------------------------------------------------------
INSERT INTO suppliers (id, name, category, contact_info, has_guarantee, contract_id) VALUES
(1, 'Bosch Russia', 'Proizvoditel', 'info@bosch.ru', 'Y', 'CT-BOSCH-01');
INSERT INTO suppliers (id, name, category, contact_info, has_guarantee, contract_id) VALUES
(2, 'Mann-Filter Rus', 'Proizvoditel', 'support@mann-filter.ru', 'Y', 'CT-MANN-02');
INSERT INTO suppliers (id, name, category, contact_info, has_guarantee, contract_id) VALUES
(3, 'Kontrakt Avto', 'Diler', '+7(495)123-45-67', 'Y', 'CT-KAVTO-03');
INSERT INTO suppliers (id, name, category, contact_info, has_guarantee, contract_id) VALUES
(4, 'Masterskaya Petrova', 'Nebolshoe proizvodstvo', 'petrov_garage@mail.ru', 'N', NULL);
INSERT INTO suppliers (id, name, category, contact_info, has_guarantee, contract_id) VALUES
(5, 'Zapchasti s Rynka', 'Melkiy magazin', 'rynochniy@yandex.ru', 'N', NULL);
INSERT INTO products (id, name, article) VALUES (1, 'Tormoznye kolodki perednie', 'BOSCH-1987936037');
INSERT INTO products (id, name, article) VALUES (2, 'Maslyanyy filtr', 'MANN-W6018');
INSERT INTO products (id, name, article) VALUES (3, 'Vozdushnyy filtr', 'BOSCH-1987432234');
INSERT INTO products (id, name, article) VALUES (4, 'Svecha zazhiganiya', 'DENSO-IU27');
INSERT INTO products (id, name, article) VALUES (5, 'Remen GRM', 'GATES-T420');
INSERT INTO purchase_orders (id, supplier_id, order_date) VALUES (1, 1, DATE '2025-12-01');
INSERT INTO purchase_orders (id, supplier_id, order_date) VALUES (2, 2, DATE '2025-12-02');
INSERT INTO purchase_orders (id, supplier_id, order_date) VALUES (3, 3, DATE '2025-12-03');
INSERT INTO purchase_orders (id, supplier_id, order_date) VALUES (4, 1, DATE '2025-12-05');
INSERT INTO purchase_orders (id, supplier_id, order_date) VALUES (5, 4, DATE '2025-12-10');
-- Сдвигаем последовательности IDENTITY, чтобы не конфликтовали с ручными ID
ALTER TABLE suppliers MODIFY id GENERATED BY DEFAULT AS IDENTITY START WITH 6;
ALTER TABLE products MODIFY id GENERATED BY DEFAULT AS IDENTITY START WITH 6;
ALTER TABLE purchase_orders MODIFY id GENERATED BY DEFAULT AS IDENTITY START WITH 6;
COMMIT;
+17
View File
@@ -0,0 +1,17 @@
-- 02_create_log_table.sql
-- Структура данных для логирования операций
DROP TABLE operation_log PURGE;
CREATE TABLE operation_log (
log_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
table_name VARCHAR2(50) NOT NULL,
operation_type VARCHAR2(10) NOT NULL CHECK (operation_type IN ('INSERT', 'UPDATE', 'DELETE')),
operation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
record_pk VARCHAR2(100) NOT NULL,
old_data CLOB,
new_data CLOB,
is_undone CHAR(1) DEFAULT 'N' CHECK (is_undone IN ('Y', 'N'))
);
COMMIT;
+123
View File
@@ -0,0 +1,123 @@
-- 03_pkg_crud.sql
-- Пакет CRUD
CREATE OR REPLACE PACKAGE pkg_entity_crud AS
-- suppliers
PROCEDURE add_supplier(p_name IN VARCHAR2, p_category IN VARCHAR2, p_contact_info IN VARCHAR2,
p_has_guarantee IN CHAR, p_contract_id IN VARCHAR2, p_new_id OUT NUMBER);
PROCEDURE upd_supplier(p_id IN NUMBER, p_name IN VARCHAR2, p_category IN VARCHAR2, p_contact_info IN VARCHAR2,
p_has_guarantee IN CHAR, p_contract_id IN VARCHAR2);
PROCEDURE del_supplier(p_id IN NUMBER);
-- products
PROCEDURE add_product(p_name IN VARCHAR2, p_article IN VARCHAR2, p_new_id OUT NUMBER);
PROCEDURE upd_product(p_id IN NUMBER, p_name IN VARCHAR2, p_article IN VARCHAR2);
PROCEDURE del_product(p_id IN NUMBER);
-- purchase_orders
PROCEDURE add_purchase_order(p_supplier_id IN NUMBER, p_order_date IN DATE, p_new_id OUT NUMBER);
PROCEDURE upd_purchase_order(p_id IN NUMBER, p_supplier_id IN NUMBER, p_order_date IN DATE);
PROCEDURE del_purchase_order(p_id IN NUMBER);
END pkg_entity_crud;
/
-- body ===================================================================================================================
CREATE OR REPLACE PACKAGE BODY pkg_entity_crud AS
-- Приватные хелперы --------------------------------------------------------------------------
PROCEDURE assert_not_null(p_val IN VARCHAR2, p_name IN VARCHAR2) IS
BEGIN
IF p_val IS NULL THEN
RAISE_APPLICATION_ERROR(-20001, p_name || ' is required');
END IF;
END;
PROCEDURE assert_found(p_msg IN VARCHAR2) IS
BEGIN
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20003, p_msg);
END IF;
END;
PROCEDURE assert_supplier_exists(p_supplier_id IN NUMBER) IS
PRAGMA AUTONOMOUS_TRANSACTION;
v_cnt NUMBER;
BEGIN
IF p_supplier_id IS NULL THEN RETURN; END IF;
SELECT COUNT(*) INTO v_cnt FROM suppliers WHERE id = p_supplier_id;
IF v_cnt = 0 THEN
RAISE_APPLICATION_ERROR(-20004, 'Supplier does not exist');
END IF;
END;
-- SUPPLIERS -------------------------------------------------------------------------------------------------------
PROCEDURE add_supplier(p_name IN VARCHAR2, p_category IN VARCHAR2, p_contact_info IN VARCHAR2,
p_has_guarantee IN CHAR, p_contract_id IN VARCHAR2, p_new_id OUT NUMBER) IS
BEGIN
assert_not_null(p_name, 'Name');
INSERT INTO suppliers (name, category, contact_info, has_guarantee, contract_id)
VALUES (p_name, p_category, p_contact_info, p_has_guarantee, p_contract_id)
RETURNING id INTO p_new_id;
END;
PROCEDURE upd_supplier(p_id IN NUMBER, p_name IN VARCHAR2, p_category IN VARCHAR2, p_contact_info IN VARCHAR2,
p_has_guarantee IN CHAR, p_contract_id IN VARCHAR2) IS
BEGIN
UPDATE suppliers
SET name = p_name, category = p_category, contact_info = p_contact_info,
has_guarantee = p_has_guarantee, contract_id = p_contract_id
WHERE id = p_id;
assert_found('Supplier not found');
END;
PROCEDURE del_supplier(p_id IN NUMBER) IS
BEGIN
DELETE FROM suppliers WHERE id = p_id;
assert_found('Supplier not found');
END;
-- PRODUCTS -----------------------------------------------------------------------------------------------------------------
PROCEDURE add_product(p_name IN VARCHAR2, p_article IN VARCHAR2, p_new_id OUT NUMBER) IS
BEGIN
assert_not_null(p_name, 'Name');
assert_not_null(p_article, 'Article');
INSERT INTO products (name, article) VALUES (p_name, p_article) RETURNING id INTO p_new_id;
END;
PROCEDURE upd_product(p_id IN NUMBER, p_name IN VARCHAR2, p_article IN VARCHAR2) IS
BEGIN
UPDATE products SET name = p_name, article = p_article WHERE id = p_id;
assert_found('Product not found');
END;
PROCEDURE del_product(p_id IN NUMBER) IS
BEGIN
DELETE FROM products WHERE id = p_id;
assert_found('Product not found');
END;
-- PURCHASE_ORDERS -------------------------------------------------------------------------------------------------------------
PROCEDURE add_purchase_order(p_supplier_id IN NUMBER, p_order_date IN DATE, p_new_id OUT NUMBER) IS
BEGIN
assert_not_null(TO_CHAR(p_supplier_id), 'Supplier');
assert_not_null(TO_CHAR(p_order_date), 'Order date');
assert_supplier_exists(p_supplier_id);
INSERT INTO purchase_orders (supplier_id, order_date)
VALUES (p_supplier_id, p_order_date) RETURNING id INTO p_new_id;
END;
PROCEDURE upd_purchase_order(p_id IN NUMBER, p_supplier_id IN NUMBER, p_order_date IN DATE) IS
BEGIN
assert_supplier_exists(p_supplier_id);
UPDATE purchase_orders SET supplier_id = p_supplier_id, order_date = p_order_date WHERE id = p_id;
assert_found('Purchase order not found');
END;
PROCEDURE del_purchase_order(p_id IN NUMBER) IS
BEGIN
DELETE FROM purchase_orders WHERE id = p_id;
assert_found('Purchase order not found');
END;
END pkg_entity_crud;
/
+77
View File
@@ -0,0 +1,77 @@
-- 04_triggers.sql
-- Триггеры
CREATE OR REPLACE TRIGGER trg_log_suppliers
AFTER INSERT OR UPDATE OR DELETE ON suppliers
FOR EACH ROW
DECLARE
v_old CLOB; v_new CLOB; v_op VARCHAR2(10); v_pk NUMBER;
BEGIN
IF SYS_CONTEXT('USERENV', 'CLIENT_INFO') = 'SKIP_LOGGING' THEN RETURN; END IF;
IF INSERTING THEN
v_op := 'INSERT'; v_pk := :NEW.id;
v_new := JSON_OBJECT('id' VALUE :NEW.id,'name' VALUE :NEW.name,'category' VALUE :NEW.category,'contact_info' VALUE :NEW.contact_info,'has_guarantee' VALUE :NEW.has_guarantee,'contract_id' VALUE :NEW.contract_id);
ELSIF UPDATING THEN
v_op := 'UPDATE'; v_pk := :OLD.id;
v_old := JSON_OBJECT('id' VALUE :OLD.id,'name' VALUE :OLD.name,'category' VALUE :OLD.category,'contact_info' VALUE :OLD.contact_info,'has_guarantee' VALUE :OLD.has_guarantee,'contract_id' VALUE :OLD.contract_id);
v_new := JSON_OBJECT('id' VALUE :NEW.id,'name' VALUE :NEW.name,'category' VALUE :NEW.category,'contact_info' VALUE :NEW.contact_info,'has_guarantee' VALUE :NEW.has_guarantee,'contract_id' VALUE :NEW.contract_id);
ELSE
v_op := 'DELETE'; v_pk := :OLD.id;
v_old := JSON_OBJECT('id' VALUE :OLD.id,'name' VALUE :OLD.name,'category' VALUE :OLD.category,'contact_info' VALUE :OLD.contact_info,'has_guarantee' VALUE :OLD.has_guarantee,'contract_id' VALUE :OLD.contract_id);
END IF;
INSERT INTO operation_log (table_name, operation_type, record_pk, old_data, new_data)
VALUES ('SUPPLIERS', v_op, TO_CHAR(v_pk), v_old, v_new);
END;
/
CREATE OR REPLACE TRIGGER trg_log_products
AFTER INSERT OR UPDATE OR DELETE ON products
FOR EACH ROW
DECLARE
v_old CLOB; v_new CLOB; v_op VARCHAR2(10); v_pk NUMBER;
BEGIN
IF SYS_CONTEXT('USERENV', 'CLIENT_INFO') = 'SKIP_LOGGING' THEN RETURN; END IF;
IF INSERTING THEN
v_op := 'INSERT'; v_pk := :NEW.id;
v_new := JSON_OBJECT('id' VALUE :NEW.id,'name' VALUE :NEW.name,'article' VALUE :NEW.article);
ELSIF UPDATING THEN
v_op := 'UPDATE'; v_pk := :OLD.id;
v_old := JSON_OBJECT('id' VALUE :OLD.id,'name' VALUE :OLD.name,'article' VALUE :OLD.article);
v_new := JSON_OBJECT('id' VALUE :NEW.id,'name' VALUE :NEW.name,'article' VALUE :NEW.article);
ELSE
v_op := 'DELETE'; v_pk := :OLD.id;
v_old := JSON_OBJECT('id' VALUE :OLD.id,'name' VALUE :OLD.name,'article' VALUE :OLD.article);
END IF;
INSERT INTO operation_log (table_name, operation_type, record_pk, old_data, new_data)
VALUES ('PRODUCTS', v_op, TO_CHAR(v_pk), v_old, v_new);
END;
/
CREATE OR REPLACE TRIGGER trg_log_purchase_orders
AFTER INSERT OR UPDATE OR DELETE ON purchase_orders
FOR EACH ROW
DECLARE
v_old CLOB; v_new CLOB; v_op VARCHAR2(10); v_pk NUMBER;
BEGIN
IF SYS_CONTEXT('USERENV', 'CLIENT_INFO') = 'SKIP_LOGGING' THEN RETURN; END IF;
IF INSERTING THEN
v_op := 'INSERT'; v_pk := :NEW.id;
v_new := JSON_OBJECT('id' VALUE :NEW.id,'supplier_id' VALUE :NEW.supplier_id,'order_date' VALUE TO_CHAR(:NEW.order_date,'YYYY-MM-DD'));
ELSIF UPDATING THEN
v_op := 'UPDATE'; v_pk := :OLD.id;
v_old := JSON_OBJECT('id' VALUE :OLD.id,'supplier_id' VALUE :OLD.supplier_id,'order_date' VALUE TO_CHAR(:OLD.order_date,'YYYY-MM-DD'));
v_new := JSON_OBJECT('id' VALUE :NEW.id,'supplier_id' VALUE :NEW.supplier_id,'order_date' VALUE TO_CHAR(:NEW.order_date,'YYYY-MM-DD'));
ELSE
v_op := 'DELETE'; v_pk := :OLD.id;
v_old := JSON_OBJECT('id' VALUE :OLD.id,'supplier_id' VALUE :OLD.supplier_id,'order_date' VALUE TO_CHAR(:OLD.order_date,'YYYY-MM-DD'));
END IF;
INSERT INTO operation_log (table_name, operation_type, record_pk, old_data, new_data)
VALUES ('PURCHASE_ORDERS', v_op, TO_CHAR(v_pk), v_old, v_new);
END;
/
+140
View File
@@ -0,0 +1,140 @@
-- 05_pkg_log_manager.sql
-- Пакет: просмотр лога, отмена операции, сводный отчёт
CREATE OR REPLACE PACKAGE pkg_log_manager AS
PROCEDURE view_log(
p_start_date IN TIMESTAMP DEFAULT NULL,
p_end_date IN TIMESTAMP DEFAULT NULL,
p_operation_type IN VARCHAR2 DEFAULT NULL,
p_cursor OUT SYS_REFCURSOR
);
PROCEDURE undo_operation(p_log_id IN NUMBER);
FUNCTION get_report(
p_sort1 IN BOOLEAN DEFAULT FALSE,
p_sort2 IN BOOLEAN DEFAULT FALSE,
p_sort3 IN BOOLEAN DEFAULT FALSE
) RETURN SYS_REFCURSOR;
END pkg_log_manager;
/
CREATE OR REPLACE PACKAGE BODY pkg_log_manager AS
PROCEDURE view_log(
p_start_date IN TIMESTAMP DEFAULT NULL,
p_end_date IN TIMESTAMP DEFAULT NULL,
p_operation_type IN VARCHAR2 DEFAULT NULL,
p_cursor OUT SYS_REFCURSOR
) IS
BEGIN
OPEN p_cursor FOR
SELECT log_id, table_name, operation_type, operation_date,
record_pk, old_data, new_data, is_undone
FROM operation_log
WHERE (p_operation_type IS NULL OR operation_type = p_operation_type)
AND (p_start_date IS NULL OR operation_date >= p_start_date)
AND (p_end_date IS NULL OR operation_date <= p_end_date)
ORDER BY log_id DESC;
END view_log;
PROCEDURE undo_operation(p_log_id IN NUMBER) IS
v_rec operation_log%ROWTYPE;
BEGIN
SELECT * INTO v_rec FROM operation_log WHERE log_id = p_log_id;
-- уже отменена
IF v_rec.is_undone = 'Y' THEN
RAISE_APPLICATION_ERROR(-20051, 'Operation already undone');
END IF;
-- не писать лог
DBMS_APPLICATION_INFO.SET_CLIENT_INFO('SKIP_LOGGING');
CASE v_rec.operation_type
WHEN 'INSERT' THEN
EXECUTE IMMEDIATE 'DELETE FROM ' || v_rec.table_name || ' WHERE id = :pk'
USING TO_NUMBER(v_rec.record_pk);
WHEN 'DELETE' THEN
CASE v_rec.table_name
WHEN 'SUPPLIERS' THEN
INSERT INTO suppliers (id, name, category, contact_info, has_guarantee, contract_id)
VALUES (JSON_VALUE(v_rec.old_data,'$.id' RETURNING NUMBER),
JSON_VALUE(v_rec.old_data,'$.name' RETURNING VARCHAR2),
JSON_VALUE(v_rec.old_data,'$.category' RETURNING VARCHAR2),
JSON_VALUE(v_rec.old_data,'$.contact_info' RETURNING VARCHAR2),
JSON_VALUE(v_rec.old_data,'$.has_guarantee' RETURNING VARCHAR2),
JSON_VALUE(v_rec.old_data,'$.contract_id' RETURNING VARCHAR2));
WHEN 'PRODUCTS' THEN
INSERT INTO products (id, name, article)
VALUES (JSON_VALUE(v_rec.old_data,'$.id' RETURNING NUMBER),
JSON_VALUE(v_rec.old_data,'$.name' RETURNING VARCHAR2),
JSON_VALUE(v_rec.old_data,'$.article' RETURNING VARCHAR2));
WHEN 'PURCHASE_ORDERS' THEN
INSERT INTO purchase_orders (id, supplier_id, order_date)
VALUES (JSON_VALUE(v_rec.old_data,'$.id' RETURNING NUMBER),
JSON_VALUE(v_rec.old_data,'$.supplier_id' RETURNING NUMBER),
TO_DATE(JSON_VALUE(v_rec.old_data,'$.order_date' RETURNING VARCHAR2),'YYYY-MM-DD'));
END CASE;
WHEN 'UPDATE' THEN
CASE v_rec.table_name
WHEN 'SUPPLIERS' THEN
UPDATE suppliers SET
name = JSON_VALUE(v_rec.old_data,'$.name' RETURNING VARCHAR2),
category = JSON_VALUE(v_rec.old_data,'$.category' RETURNING VARCHAR2),
contact_info = JSON_VALUE(v_rec.old_data,'$.contact_info' RETURNING VARCHAR2),
has_guarantee = JSON_VALUE(v_rec.old_data,'$.has_guarantee' RETURNING VARCHAR2),
contract_id = JSON_VALUE(v_rec.old_data,'$.contract_id' RETURNING VARCHAR2)
WHERE id = TO_NUMBER(v_rec.record_pk);
WHEN 'PRODUCTS' THEN
UPDATE products SET
name = JSON_VALUE(v_rec.old_data,'$.name' RETURNING VARCHAR2),
article = JSON_VALUE(v_rec.old_data,'$.article' RETURNING VARCHAR2)
WHERE id = TO_NUMBER(v_rec.record_pk);
WHEN 'PURCHASE_ORDERS' THEN
UPDATE purchase_orders SET
supplier_id = JSON_VALUE(v_rec.old_data,'$.supplier_id' RETURNING NUMBER),
order_date = TO_DATE(JSON_VALUE(v_rec.old_data,'$.order_date' RETURNING VARCHAR2),'YYYY-MM-DD')
WHERE id = TO_NUMBER(v_rec.record_pk);
END CASE;
END CASE;
UPDATE operation_log SET is_undone = 'Y' WHERE log_id = p_log_id;
--писать логи
DBMS_APPLICATION_INFO.SET_CLIENT_INFO(NULL);
-- другие ошибки
EXCEPTION
WHEN OTHERS THEN
DBMS_APPLICATION_INFO.SET_CLIENT_INFO(NULL);
RAISE;
END undo_operation;
FUNCTION get_report(
p_sort1 IN BOOLEAN DEFAULT FALSE,
p_sort2 IN BOOLEAN DEFAULT FALSE,
p_sort3 IN BOOLEAN DEFAULT FALSE
) RETURN SYS_REFCURSOR IS
v_cursor SYS_REFCURSOR;
v_sql VARCHAR2(1000);
v_order VARCHAR2(200) := '';
BEGIN
v_sql := 'SELECT table_name, operation_type, COUNT(*) AS op_count
FROM operation_log
GROUP BY table_name, operation_type';
IF p_sort1 THEN v_order := v_order || 'table_name,'; END IF;
IF p_sort2 THEN v_order := v_order || 'operation_type,'; END IF;
IF p_sort3 THEN v_order := v_order || 'op_count DESC,'; END IF;
IF v_order IS NOT NULL THEN
v_sql := v_sql || ' ORDER BY ' || RTRIM(v_order, ',');
END IF;
OPEN v_cursor FOR v_sql;
RETURN v_cursor;
END get_report;
END pkg_log_manager;
/
+218
View File
@@ -0,0 +1,218 @@
-- 06_test.sql
-- Тесты
SET SERVEROUTPUT ON;
-- ------------------------------------------------------------
-- 1. Тест CRUD + автологирование (suppliers)
-- ------------------------------------------------------------
DECLARE
v_new_id suppliers.id%TYPE;
v_cursor SYS_REFCURSOR;
v_log_id operation_log.log_id%TYPE;
v_tn operation_log.table_name%TYPE;
v_ot operation_log.operation_type%TYPE;
v_pk operation_log.record_pk%TYPE;
v_und operation_log.is_undone%TYPE;
BEGIN
DBMS_OUTPUT.PUT_LINE('=== TEST 1: CRUD suppliers + logging ===');
-- INSERT
pkg_entity_crud.add_supplier('Test Supplier', 'Diler', 'test@test.com', 'Y', 'CT-TEST-99', v_new_id);
DBMS_OUTPUT.PUT_LINE('Inserted supplier id=' || v_new_id);
-- UPDATE
pkg_entity_crud.upd_supplier(v_new_id, 'Test Supplier Updated', 'Proizvoditel', 'new@test.com', 'N', NULL);
DBMS_OUTPUT.PUT_LINE('Updated supplier id=' || v_new_id);
-- DELETE
pkg_entity_crud.del_supplier(v_new_id);
DBMS_OUTPUT.PUT_LINE('Deleted supplier id=' || v_new_id);
-- Проверим лог
OPEN v_cursor FOR
SELECT log_id, table_name, operation_type, record_pk, is_undone
FROM operation_log
WHERE table_name = 'SUPPLIERS'
ORDER BY log_id DESC
FETCH FIRST 3 ROWS ONLY;
LOOP
FETCH v_cursor INTO v_log_id, v_tn, v_ot, v_pk, v_und;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('LOG: id=' || v_log_id || ' table=' || v_tn ||
' op=' || v_ot || ' pk=' || v_pk || ' undone=' || v_und);
END LOOP;
CLOSE v_cursor;
END;
/
-- ------------------------------------------------------------
-- 2. Тест CRUD + автологирование (products)
-- ------------------------------------------------------------
DECLARE
v_new_id products.id%TYPE;
v_cursor SYS_REFCURSOR;
v_log_id operation_log.log_id%TYPE;
v_tn operation_log.table_name%TYPE;
v_ot operation_log.operation_type%TYPE;
BEGIN
DBMS_OUTPUT.PUT_LINE('=== TEST 2: CRUD products + logging ===');
pkg_entity_crud.add_product('Test Product', 'TEST-ARTICLE-999', v_new_id);
DBMS_OUTPUT.PUT_LINE('Inserted product id=' || v_new_id);
pkg_entity_crud.upd_product(v_new_id, 'Test Product Updated', 'TEST-ARTICLE-999-U');
DBMS_OUTPUT.PUT_LINE('Updated product id=' || v_new_id);
pkg_entity_crud.del_product(v_new_id);
DBMS_OUTPUT.PUT_LINE('Deleted product id=' || v_new_id);
OPEN v_cursor FOR
SELECT log_id, table_name, operation_type
FROM operation_log
WHERE table_name = 'PRODUCTS'
ORDER BY log_id DESC
FETCH FIRST 3 ROWS ONLY;
LOOP
FETCH v_cursor INTO v_log_id, v_tn, v_ot;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('LOG: id=' || v_log_id || ' table=' || v_tn || ' op=' || v_ot);
END LOOP;
CLOSE v_cursor;
END;
/
-- ------------------------------------------------------------
-- 3. Тест CRUD + автологирование (purchase_orders)
-- ------------------------------------------------------------
DECLARE
v_new_id purchase_orders.id%TYPE;
v_cursor SYS_REFCURSOR;
v_log_id operation_log.log_id%TYPE;
v_tn operation_log.table_name%TYPE;
v_ot operation_log.operation_type%TYPE;
BEGIN
DBMS_OUTPUT.PUT_LINE('=== TEST 3: CRUD purchase_orders + logging ===');
pkg_entity_crud.add_purchase_order(1, DATE '2025-12-20', v_new_id);
DBMS_OUTPUT.PUT_LINE('Inserted order id=' || v_new_id);
pkg_entity_crud.upd_purchase_order(v_new_id, 2, DATE '2025-12-21');
DBMS_OUTPUT.PUT_LINE('Updated order id=' || v_new_id);
pkg_entity_crud.del_purchase_order(v_new_id);
DBMS_OUTPUT.PUT_LINE('Deleted order id=' || v_new_id);
OPEN v_cursor FOR
SELECT log_id, table_name, operation_type
FROM operation_log
WHERE table_name = 'PURCHASE_ORDERS'
ORDER BY log_id DESC
FETCH FIRST 3 ROWS ONLY;
LOOP
FETCH v_cursor INTO v_log_id, v_tn, v_ot;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('LOG: id=' || v_log_id || ' table=' || v_tn || ' op=' || v_ot);
END LOOP;
CLOSE v_cursor;
END;
/
-- ------------------------------------------------------------
-- 4. Тест просмотра лога (view_log)
-- ------------------------------------------------------------
DECLARE
v_cursor SYS_REFCURSOR;
v_log_id operation_log.log_id%TYPE;
v_tn operation_log.table_name%TYPE;
v_ot operation_log.operation_type%TYPE;
v_date operation_log.operation_date%TYPE;
v_pk operation_log.record_pk%TYPE;
v_old operation_log.old_data%TYPE;
v_new operation_log.new_data%TYPE;
v_und operation_log.is_undone%TYPE;
BEGIN
DBMS_OUTPUT.PUT_LINE('=== TEST 4: view_log (last 10 rows) ===');
pkg_log_manager.view_log(
p_start_date => NULL,
p_end_date => NULL,
p_operation_type=> NULL,
p_cursor => v_cursor
);
LOOP
FETCH v_cursor INTO v_log_id, v_tn, v_ot, v_date, v_pk, v_old, v_new, v_und;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('LOG: id=' || v_log_id || ' table=' || v_tn ||
' op=' || v_ot || ' date=' || v_date ||
' pk=' || v_pk || ' undone=' || v_und);
END LOOP;
CLOSE v_cursor;
END;
/
-- ------------------------------------------------------------
-- 5. Тест отмены операции (undo)
-- ------------------------------------------------------------
DECLARE
v_sup_id suppliers.id%TYPE;
v_log_id operation_log.log_id%TYPE;
v_cnt NUMBER;
BEGIN
DBMS_OUTPUT.PUT_LINE('=== TEST 5: undo_operation ===');
-- Создаём поставщика, запоминаем log_id INSERT
pkg_entity_crud.add_supplier('Undo Test', 'Diler', 'undo@test.com', 'Y', 'CT-UNDO-01', v_sup_id);
BEGIN
SELECT log_id INTO v_log_id
FROM operation_log
WHERE table_name = 'SUPPLIERS' AND operation_type = 'INSERT'
ORDER BY log_id DESC FETCH FIRST 1 ROW ONLY;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('ERROR: operation_log is empty — check triggers');
RAISE;
END;
DBMS_OUTPUT.PUT_LINE('Created supplier id=' || v_sup_id || ', log_id=' || v_log_id);
-- Проверяем, что он есть
SELECT COUNT(*) INTO v_cnt FROM suppliers WHERE id = v_sup_id;
DBMS_OUTPUT.PUT_LINE('Count before undo=' || v_cnt);
-- Отменяем INSERT → DELETE
pkg_log_manager.undo_operation(v_log_id);
SELECT COUNT(*) INTO v_cnt FROM suppliers WHERE id = v_sup_id;
DBMS_OUTPUT.PUT_LINE('Count after undo=' || v_cnt || ' (expected 0)');
END;
/
-- ------------------------------------------------------------
-- 6. Тест сводного отчёта (get_report)
-- ------------------------------------------------------------
DECLARE
v_cursor SYS_REFCURSOR;
v_tn operation_log.table_name%TYPE;
v_ot operation_log.operation_type%TYPE;
v_cnt NUMBER;
BEGIN
DBMS_OUTPUT.PUT_LINE('=== TEST 6: get_report (sort by table_name, then count) ===');
v_cursor := pkg_log_manager.get_report(p_sort1 => TRUE, p_sort2 => FALSE, p_sort3 => TRUE);
LOOP
FETCH v_cursor INTO v_tn, v_ot, v_cnt;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('REPORT: table=' || v_tn || ' op=' || v_ot || ' count=' || v_cnt);
END LOOP;
CLOSE v_cursor;
END;
/
COMMIT;
+599
View File
@@ -0,0 +1,599 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import json
from datetime import date
try:
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QPushButton, QLabel, QLineEdit,
QComboBox, QDialog, QFormLayout, QMessageBox, QHeaderView,
QTabWidget, QStatusBar, QGroupBox, QCheckBox, QTextEdit,
QDateEdit, QSpinBox
)
from PySide6.QtCore import Qt, QDate, QTimer
from PySide6.QtGui import QColor, QBrush, QFont
QT_AVAILABLE = True
except ImportError:
QT_AVAILABLE = False
try:
import oracledb
ORACLE_AVAILABLE = True
except ImportError:
ORACLE_AVAILABLE = False
# =====================================================================
# Работа с БД
# =====================================================================
class DB:
conn = None
@classmethod
def connect(cls, user, pwd, host, port, service, role):
dsn = f"{host}:{port}/{service}"
kwargs = {"user": user, "password": pwd, "dsn": dsn}
if role == "SYSDBA":
kwargs["mode"] = oracledb.SYSDBA
elif role == "SYSOPER":
kwargs["mode"] = oracledb.SYSOPER
cls.conn = oracledb.connect(**kwargs)
@classmethod
def query(cls, sql, params=None):
if cls.conn is None:
return []
with cls.conn.cursor() as cur:
cur.execute(sql, params or {})
return cur.fetchall()
@classmethod
def execute(cls, sql, params=None):
if cls.conn is None:
return
with cls.conn.cursor() as cur:
cur.execute(sql, params or {})
cls.conn.commit()
# =====================================================================
# Диалог подключения
# =====================================================================
class ConnectDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Подключение к Oracle")
self.setFixedSize(350, 300)
layout = QFormLayout(self)
self.user = QLineEdit("student")
self.pwd = QLineEdit()
self.pwd.setEchoMode(QLineEdit.Password)
self.host = QLineEdit("localhost")
self.port = QLineEdit("1521")
self.service = QLineEdit("XEPDB1")
self.role = QComboBox()
self.role.addItems(["NORMAL", "SYSDBA", "SYSOPER"])
layout.addRow("Пользователь:", self.user)
layout.addRow("Пароль:", self.pwd)
layout.addRow("Хост:", self.host)
layout.addRow("Порт:", self.port)
layout.addRow("Service Name:", self.service)
layout.addRow("Role:", self.role)
btn = QPushButton("Подключиться")
btn.clicked.connect(self.do_connect)
layout.addRow(btn)
def do_connect(self):
try:
DB.connect(
self.user.text(), self.pwd.text(), self.host.text(),
self.port.text(), self.service.text(), self.role.currentText()
)
self.accept()
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
# =====================================================================
# Диалог редактирования записи
# =====================================================================
class RecordDialog(QDialog):
def __init__(self, title, fields, values=None, parent=None):
super().__init__(parent)
self.setWindowTitle(title)
self.setFixedSize(400, 350)
self.layout = QFormLayout(self)
self.fields = {}
values = values or {}
for col, label in fields:
edit = QLineEdit(str(values.get(col, "")))
self.layout.addRow(label + ":", edit)
self.fields[col] = edit
btn = QPushButton("Сохранить")
btn.clicked.connect(self.accept)
self.layout.addRow(btn)
def get_data(self):
return {k: v.text() for k, v in self.fields.items()}
# =====================================================================
# Диалог деталей операции
# =====================================================================
class LogDetailDialog(QDialog):
def __init__(self, log_id, table, op_type, old_data, new_data, parent=None):
super().__init__(parent)
self.setWindowTitle(f"Детали операции #{log_id}")
self.resize(500, 400)
layout = QVBoxLayout(self)
info = QLabel(f"<b>Таблица:</b> {table} &nbsp; <b>Операция:</b> {op_type}")
layout.addWidget(info)
def make_box(title, data, color):
if not data:
return
box = QGroupBox(title)
v = QVBoxLayout(box)
te = QTextEdit()
te.setReadOnly(True)
te.setStyleSheet(f"background-color: {color};")
try:
te.setPlainText(json.dumps(json.loads(data), ensure_ascii=False, indent=2))
except Exception:
te.setPlainText(str(data))
v.addWidget(te)
layout.addWidget(box)
make_box("До операции (old_data)", old_data, "#ffe6e6")
make_box("После операции (new_data)", new_data, "#e6ffe6")
btn = QPushButton("Закрыть")
btn.clicked.connect(self.accept)
layout.addWidget(btn)
# =====================================================================
# Главное окно
# =====================================================================
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Магазин автозапчастей")
self.resize(1150, 750)
self.tabs = QTabWidget()
self.setCentralWidget(self.tabs)
self.build_table_tab("Suppliers", "suppliers",
["id", "name", "category", "contact_info", "has_guarantee", "contract_id"],
[("name", "Название"), ("category", "Категория"),
("contact_info", "Контакты"), ("has_guarantee", "Гарантия (Y/N)"),
("contract_id", "Договор")])
self.build_table_tab("Products", "products",
["id", "name", "article"],
[("name", "Название"), ("article", "Артикул")])
self.build_orders_tab()
self.build_log_tab()
self.build_report_tab()
self.status = QStatusBar()
self.setStatusBar(self.status)
self.status.showMessage("Не подключено")
QTimer.singleShot(0, self.show_connect)
# -----------------------------------------------------------------
# Универсальная вкладка таблицы
# -----------------------------------------------------------------
def build_table_tab(self, title, table, columns, fields):
w = QWidget()
v = QVBoxLayout(w)
table_widget = QTableWidget()
table_widget.setColumnCount(len(columns))
table_widget.setHorizontalHeaderLabels(columns)
table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table_widget.setSelectionBehavior(QTableWidget.SelectRows)
v.addWidget(table_widget)
setattr(self, f"tw_{table}", table_widget)
h = QHBoxLayout()
btn_refresh = QPushButton("🔄 Обновить")
btn_refresh.clicked.connect(lambda: self.load_table(table, columns))
btn_add = QPushButton(" Добавить")
btn_add.clicked.connect(lambda: self.add_record(table, fields))
btn_edit = QPushButton("✏️ Изменить")
btn_edit.clicked.connect(lambda: self.edit_record(table, columns, fields))
btn_del = QPushButton("🗑️ Удалить")
btn_del.clicked.connect(lambda: self.delete_record(table, columns))
for b in (btn_refresh, btn_add, btn_edit, btn_del):
h.addWidget(b)
v.addLayout(h)
self.tabs.addTab(w, title)
QTimer.singleShot(100, lambda: self.load_table(table, columns))
def load_table(self, table, columns):
tw = getattr(self, f"tw_{table}")
tw.setRowCount(0)
try:
rows = DB.query(f"SELECT {','.join(columns)} FROM {table}")
tw.setRowCount(len(rows))
for i, row in enumerate(rows):
for j, val in enumerate(row):
tw.setItem(i, j, QTableWidgetItem(str(val) if val is not None else ""))
self.status.showMessage(f"{table}: загружено {len(rows)} записей")
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
def add_record(self, table, fields):
dlg = RecordDialog(f"Добавить {table}", fields)
if dlg.exec() != QDialog.Accepted:
return
data = dlg.get_data()
cols = ", ".join(data.keys())
ph = ", ".join([f":{k}" for k in data.keys()])
try:
DB.execute(f"INSERT INTO {table} ({cols}) VALUES ({ph})", data)
self.load_table(table, [c for c, _ in fields] + ["id"])
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
def edit_record(self, table, columns, fields):
tw = getattr(self, f"tw_{table}")
row = tw.currentRow()
if row < 0:
QMessageBox.warning(self, "Внимание", "Выберите запись")
return
pk = tw.item(row, 0).text()
current = {col: tw.item(row, i).text() for i, col in enumerate(columns)}
dlg = RecordDialog(f"Изменить {table}", fields, current)
if dlg.exec() != QDialog.Accepted:
return
data = dlg.get_data()
sets = ", ".join([f"{k} = :{k}" for k in data.keys()])
data["id"] = pk
try:
DB.execute(f"UPDATE {table} SET {sets} WHERE id = :id", data)
self.load_table(table, columns)
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
def delete_record(self, table, columns):
tw = getattr(self, f"tw_{table}")
row = tw.currentRow()
if row < 0:
QMessageBox.warning(self, "Внимание", "Выберите запись")
return
pk = tw.item(row, 0).text()
if QMessageBox.question(self, "Подтверждение", f"Удалить запись {pk}?") == QMessageBox.Yes:
try:
DB.execute(f"DELETE FROM {table} WHERE id = :id", {"id": pk})
self.load_table(table, columns)
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
# -----------------------------------------------------------------
# Orders (отдельно из-за DateEdit и ComboBox)
# -----------------------------------------------------------------
def build_orders_tab(self):
w = QWidget()
v = QVBoxLayout(w)
self.tw_orders = QTableWidget()
self.tw_orders.setColumnCount(3)
self.tw_orders.setHorizontalHeaderLabels(["id", "supplier_id", "order_date"])
self.tw_orders.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.tw_orders.setSelectionBehavior(QTableWidget.SelectRows)
v.addWidget(self.tw_orders)
h = QHBoxLayout()
btn_refresh = QPushButton("🔄 Обновить")
btn_refresh.clicked.connect(self.load_orders)
btn_add = QPushButton(" Добавить")
btn_add.clicked.connect(self.add_order)
btn_edit = QPushButton("✏️ Изменить")
btn_edit.clicked.connect(self.edit_order)
btn_del = QPushButton("🗑️ Удалить")
btn_del.clicked.connect(self.delete_order)
for b in (btn_refresh, btn_add, btn_edit, btn_del):
h.addWidget(b)
v.addLayout(h)
self.tabs.addTab(w, "Purchase Orders")
QTimer.singleShot(150, self.load_orders)
def load_orders(self):
self.tw_orders.setRowCount(0)
try:
rows = DB.query("SELECT id, supplier_id, order_date FROM purchase_orders")
self.tw_orders.setRowCount(len(rows))
for i, (oid, sid, odate) in enumerate(rows):
self.tw_orders.setItem(i, 0, QTableWidgetItem(str(oid)))
self.tw_orders.setItem(i, 1, QTableWidgetItem(str(sid)))
self.tw_orders.setItem(i, 2, QTableWidgetItem(str(odate) if odate else ""))
self.status.showMessage(f"purchase_orders: загружено {len(rows)} записей")
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
def _supplier_combo(self):
cb = QComboBox()
self.supplier_map = {}
for sid, sname in DB.query("SELECT id, name FROM suppliers"):
txt = f"{sid}{sname}"
cb.addItem(txt)
self.supplier_map[txt] = sid
return cb
def add_order(self):
dlg = QDialog(self)
dlg.setWindowTitle("Добавить заказ")
dlg.setFixedSize(350, 200)
f = QFormLayout(dlg)
cb = self._supplier_combo()
de = QDateEdit(QDate.currentDate())
de.setCalendarPopup(True)
f.addRow("Поставщик:", cb)
f.addRow("Дата:", de)
btn = QPushButton("Сохранить")
btn.clicked.connect(dlg.accept)
f.addRow(btn)
if dlg.exec() != QDialog.Accepted:
return
try:
DB.execute("INSERT INTO purchase_orders (supplier_id, order_date) VALUES (:sid, :od)",
{"sid": self.supplier_map[cb.currentText()], "od": de.date().toPython()})
self.load_orders()
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
def edit_order(self):
row = self.tw_orders.currentRow()
if row < 0:
QMessageBox.warning(self, "Внимание", "Выберите запись")
return
pk = self.tw_orders.item(row, 0).text()
dlg = QDialog(self)
dlg.setWindowTitle("Изменить заказ")
dlg.setFixedSize(350, 200)
f = QFormLayout(dlg)
cb = self._supplier_combo()
de = QDateEdit()
de.setCalendarPopup(True)
# установить текущие значения
cur_sid = self.tw_orders.item(row, 1).text()
cur_date = self.tw_orders.item(row, 2).text()
for i in range(cb.count()):
if cb.itemText(i).startswith(cur_sid + " "):
cb.setCurrentIndex(i)
break
de.setDate(QDate.fromString(cur_date, "yyyy-MM-dd"))
f.addRow("Поставщик:", cb)
f.addRow("Дата:", de)
btn = QPushButton("Сохранить")
btn.clicked.connect(dlg.accept)
f.addRow(btn)
if dlg.exec() != QDialog.Accepted:
return
try:
DB.execute("UPDATE purchase_orders SET supplier_id = :sid, order_date = :od WHERE id = :id",
{"sid": self.supplier_map[cb.currentText()],
"od": de.date().toPython(),
"id": pk})
self.load_orders()
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
def delete_order(self):
row = self.tw_orders.currentRow()
if row < 0:
QMessageBox.warning(self, "Внимание", "Выберите запись")
return
pk = self.tw_orders.item(row, 0).text()
if QMessageBox.question(self, "Подтверждение", f"Удалить заказ {pk}?") == QMessageBox.Yes:
try:
DB.execute("DELETE FROM purchase_orders WHERE id = :id", {"id": pk})
self.load_orders()
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
# -----------------------------------------------------------------
# Журнал операций
# -----------------------------------------------------------------
def build_log_tab(self):
w = QWidget()
v = QVBoxLayout(w)
filt = QGroupBox("Фильтры")
fh = QHBoxLayout(filt)
self.log_type = QComboBox()
self.log_type.addItems(["Все", "INSERT", "UPDATE", "DELETE"])
self.log_from = QLineEdit("2025-01-01")
self.log_to = QLineEdit("2026-12-31")
fh.addWidget(QLabel("Тип:"))
fh.addWidget(self.log_type)
fh.addWidget(QLabel("С:"))
fh.addWidget(self.log_from)
fh.addWidget(QLabel("По:"))
fh.addWidget(self.log_to)
btn_load = QPushButton("Показать")
btn_load.clicked.connect(self.load_log)
btn_undo = QPushButton("↩️ Отменить выбранное")
btn_undo.clicked.connect(self.undo_selected)
fh.addWidget(btn_load)
fh.addWidget(btn_undo)
fh.addStretch()
v.addWidget(filt)
self.tw_log = QTableWidget()
self.tw_log.setColumnCount(8)
self.tw_log.setHorizontalHeaderLabels(
["log_id", "table_name", "op_type", "date", "record_pk", "old_data", "new_data", "undone"]
)
self.tw_log.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.tw_log.setSelectionBehavior(QTableWidget.SelectRows)
self.tw_log.doubleClicked.connect(self.log_double_click)
v.addWidget(self.tw_log)
# цветовые теги
self.tw_log.tag_colors = {
"INSERT": QColor("#d4edda"),
"UPDATE": QColor("#fff3cd"),
"DELETE": QColor("#f8d7da")
}
self.tabs.addTab(w, "Журнал операций")
def load_log(self):
self.tw_log.setRowCount(0)
try:
op = None if self.log_type.currentText() == "Все" else self.log_type.currentText()
sql = """SELECT log_id, table_name, operation_type, operation_date, record_pk,
old_data, new_data, is_undone
FROM operation_log
WHERE (:op IS NULL OR operation_type = :op)
AND operation_date BETWEEN TO_TIMESTAMP(:d1, 'YYYY-MM-DD')
AND TO_TIMESTAMP(:d2 || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
ORDER BY log_id DESC"""
rows = DB.query(sql, {"op": op, "d1": self.log_from.text(), "d2": self.log_to.text()})
self.tw_log.setRowCount(len(rows))
for i, (lid, tn, ot, od, pk, old_d, new_d, und) in enumerate(rows):
vals = [lid, tn, ot, str(od) if od else "", pk,
str(old_d)[:60] + "..." if old_d and len(str(old_d)) > 60 else str(old_d or ""),
str(new_d)[:60] + "..." if new_d and len(str(new_d)) > 60 else str(new_d or ""),
und]
for j, val in enumerate(vals):
item = QTableWidgetItem(str(val))
if ot in self.tw_log.tag_colors:
item.setBackground(QBrush(self.tw_log.tag_colors[ot]))
self.tw_log.setItem(i, j, item)
self.status.showMessage(f"Журнал: найдено {len(rows)} записей")
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
def undo_selected(self):
row = self.tw_log.currentRow()
if row < 0:
QMessageBox.warning(self, "Внимание", "Выберите запись")
return
lid = self.tw_log.item(row, 0).text()
if QMessageBox.question(self, "Подтверждение", f"Отменить операцию {lid}?") == QMessageBox.Yes:
try:
DB.execute("BEGIN pkg_log_manager.undo_operation(:id); END;", {"id": lid})
self.load_log()
self.refresh_all_tables()
QMessageBox.information(self, "Успех", "Операция отменена")
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
def log_double_click(self):
row = self.tw_log.currentRow()
if row < 0:
return
lid = self.tw_log.item(row, 0).text()
tn = self.tw_log.item(row, 1).text()
ot = self.tw_log.item(row, 2).text()
try:
old_d, new_d = DB.query("SELECT old_data, new_data FROM operation_log WHERE log_id = :id", {"id": lid})[0]
LogDetailDialog(int(lid), tn, ot, old_d, new_d, self).exec()
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
# -----------------------------------------------------------------
# Отчёт
# -----------------------------------------------------------------
def build_report_tab(self):
w = QWidget()
v = QVBoxLayout(w)
opts = QGroupBox("Параметры сортировки")
oh = QHBoxLayout(opts)
self.chk_r1 = QCheckBox("По таблице (флаг 1)")
self.chk_r2 = QCheckBox("По типу операции (флаг 2)")
self.chk_r3 = QCheckBox("По количеству (флаг 3)")
btn = QPushButton("Сформировать отчёт")
btn.clicked.connect(self.load_report)
oh.addWidget(self.chk_r1)
oh.addWidget(self.chk_r2)
oh.addWidget(self.chk_r3)
oh.addWidget(btn)
oh.addStretch()
v.addWidget(opts)
self.tw_rep = QTableWidget()
self.tw_rep.setColumnCount(3)
self.tw_rep.setHorizontalHeaderLabels(["Таблица", "Тип операции", "Количество"])
self.tw_rep.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
v.addWidget(self.tw_rep)
self.tabs.addTab(w, "Отчёт")
def load_report(self):
self.tw_rep.setRowCount(0)
try:
parts = []
if self.chk_r1.isChecked():
parts.append("table_name")
if self.chk_r2.isChecked():
parts.append("operation_type")
if self.chk_r3.isChecked():
parts.append("op_count")
order = "ORDER BY " + ", ".join(parts) if parts else ""
sql = f"SELECT table_name, operation_type, COUNT(*) AS op_count FROM operation_log GROUP BY table_name, operation_type {order}"
rows = DB.query(sql)
self.tw_rep.setRowCount(len(rows))
for i, (tn, ot, cnt) in enumerate(rows):
for j, val in enumerate((tn, ot, str(cnt))):
self.tw_rep.setItem(i, j, QTableWidgetItem(val))
self.status.showMessage(f"Отчёт: сформировано {len(rows)} строк")
except Exception as e:
QMessageBox.critical(self, "Ошибка", str(e))
# -----------------------------------------------------------------
# Служебные
# -----------------------------------------------------------------
def show_connect(self):
if DB.conn is None:
dlg = ConnectDialog(self)
if dlg.exec() == QDialog.Accepted:
self.status.showMessage("Подключено")
self.refresh_all_tables()
self.load_log()
def refresh_all_tables(self):
self.load_table("suppliers", ["id", "name", "category", "contact_info", "has_guarantee", "contract_id"])
self.load_table("products", ["id", "name", "article"])
self.load_orders()
def closeEvent(self, event):
if DB.conn:
DB.conn.close()
event.accept()
# =====================================================================
# Точка входа
# =====================================================================
if __name__ == "__main__":
if not QT_AVAILABLE:
print("Ошибка: PySide6 не установлен. Установите: pip install pyside6")
sys.exit(1)
if not ORACLE_AVAILABLE:
print("Ошибка: oracledb не установлен. Установите: pip install oracledb")
sys.exit(1)
app = QApplication(sys.argv)
app.setStyle("Fusion")
win = MainWindow()
win.show()
sys.exit(app.exec())
BIN
View File
Binary file not shown.
Binary file not shown.