mirror of
https://github.com/artagaz/bd-semestovaya.git
synced 2026-06-19 02:01:30 +07:00
gotovo
This commit is contained in:
Binary file not shown.
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
/
|
||||
@@ -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;
|
||||
/
|
||||
@@ -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
@@ -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;
|
||||
@@ -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} <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())
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user