Setup Docker infrastructure for GoMag vending import system
- Add Flask admin interface with Oracle connection pool - Create ARTICOLE_TERTI table for SKU mappings - Configure Docker container with Oracle Instant Client - Setup project documentation and requirements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
78
api/01_create_table.sql
Normal file
78
api/01_create_table.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- ====================================================================
|
||||
-- P1-001: Tabel ARTICOLE_TERTI pentru mapări SKU → CODMAT
|
||||
-- Sistem Import Comenzi Web → ROA
|
||||
-- ====================================================================
|
||||
|
||||
-- Creare tabel pentru mapări complexe articole
|
||||
CREATE TABLE ARTICOLE_TERTI (
|
||||
sku VARCHAR2(100) NOT NULL, -- SKU din platforma web
|
||||
codmat VARCHAR2(50) NOT NULL, -- CODMAT din nom_articole
|
||||
cantitate_roa NUMBER(10,3) DEFAULT 1, -- Câte unități ROA = 1 web
|
||||
procent_pret NUMBER(5,2) DEFAULT 100, -- % din preț pentru seturi
|
||||
activ NUMBER(1) DEFAULT 1, -- 1=activ, 0=inactiv
|
||||
data_creare DATE DEFAULT SYSDATE, -- Timestamp creare
|
||||
data_modif DATE DEFAULT SYSDATE, -- Timestamp ultima modificare
|
||||
id_util_creare NUMBER(10) DEFAULT -3, -- ID utilizator care a creat
|
||||
id_util_modif NUMBER(10) DEFAULT -3, -- ID utilizator care a modificat
|
||||
|
||||
-- Primary key compus
|
||||
CONSTRAINT pk_articole_terti PRIMARY KEY (sku, codmat),
|
||||
|
||||
-- Validări
|
||||
CONSTRAINT chk_art_terti_cantitate CHECK (cantitate_roa > 0),
|
||||
CONSTRAINT chk_art_terti_procent CHECK (procent_pret >= 0 AND procent_pret <= 100),
|
||||
CONSTRAINT chk_art_terti_activ CHECK (activ IN (0, 1))
|
||||
);
|
||||
|
||||
-- Index pentru performanță pe căutări frecvente după SKU
|
||||
CREATE INDEX idx_articole_terti_sku ON ARTICOLE_TERTI (sku, activ);
|
||||
|
||||
-- Index pentru audit și raportare
|
||||
CREATE INDEX idx_articole_terti_data ON ARTICOLE_TERTI (data_creare, activ);
|
||||
|
||||
-- Comentarii pentru documentație
|
||||
COMMENT ON TABLE ARTICOLE_TERTI IS 'Mapări SKU-uri web → CODMAT ROA pentru reîmpachetări și seturi';
|
||||
COMMENT ON COLUMN ARTICOLE_TERTI.sku IS 'SKU din platforma web (ex: GoMag)';
|
||||
COMMENT ON COLUMN ARTICOLE_TERTI.codmat IS 'CODMAT din nom_articole ROA';
|
||||
COMMENT ON COLUMN ARTICOLE_TERTI.cantitate_roa IS 'Câte unități ROA pentru 1 unitate web';
|
||||
COMMENT ON COLUMN ARTICOLE_TERTI.procent_pret IS 'Procent din preț web alocat acestui CODMAT (pentru seturi)';
|
||||
COMMENT ON COLUMN ARTICOLE_TERTI.activ IS '1=mapare activă, 0=dezactivată';
|
||||
|
||||
-- Date de test pentru validare
|
||||
INSERT INTO ARTICOLE_TERTI (sku, codmat, cantitate_roa, procent_pret, activ) VALUES
|
||||
('CAFE100', 'CAF01', 10, 100, 1); -- Reîmpachetare: 1 web = 10 ROA
|
||||
|
||||
INSERT INTO ARTICOLE_TERTI (sku, codmat, cantitate_roa, procent_pret, activ) VALUES
|
||||
('SET01', 'CAF01', 2, 60, 1); -- Set compus partea 1
|
||||
|
||||
INSERT INTO ARTICOLE_TERTI (sku, codmat, cantitate_roa, procent_pret, activ) VALUES
|
||||
('SET01', 'FILT01', 1, 40, 1); -- Set compus partea 2
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verificare creare tabel
|
||||
SELECT 'ARTICOLE_TERTI creat cu succes' AS STATUS,
|
||||
COUNT(*) AS NR_RECORDS
|
||||
FROM ARTICOLE_TERTI;
|
||||
|
||||
-- Test integritate constraintelor
|
||||
BEGIN
|
||||
-- Test cantitate invalidă
|
||||
BEGIN
|
||||
INSERT INTO ARTICOLE_TERTI (sku, codmat, cantitate_roa) VALUES ('TEST', 'TST01', -1);
|
||||
DBMS_OUTPUT.PUT_LINE('ERROR: Constraint cantitate nu funcționează!');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
DBMS_OUTPUT.PUT_LINE('OK: Constraint cantitate funcționează');
|
||||
END;
|
||||
|
||||
-- Test procent invalid
|
||||
BEGIN
|
||||
INSERT INTO ARTICOLE_TERTI (sku, codmat, procent_pret) VALUES ('TEST2', 'TST02', 150);
|
||||
DBMS_OUTPUT.PUT_LINE('ERROR: Constraint procent nu funcționează!');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
DBMS_OUTPUT.PUT_LINE('OK: Constraint procent funcționează');
|
||||
END;
|
||||
END;
|
||||
/
|
||||
33
api/Dockerfile
Normal file
33
api/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# Multi-stage build for Oracle Instant Client + Python Flask
|
||||
FROM python:3.11.4-slim-buster as oracle_base
|
||||
|
||||
# Installing Oracle instant client
|
||||
WORKDIR /opt/oracle
|
||||
RUN apt-get update && apt-get install -y libaio1 wget unzip curl \
|
||||
&& wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip \
|
||||
&& unzip instantclient-basiclite-linuxx64.zip \
|
||||
&& rm -f instantclient-basiclite-linuxx64.zip \
|
||||
&& cd /opt/oracle/instantclient* \
|
||||
&& rm -f *jdbc* *occi* *mysql* *README *jar uidrvci genezi adrci \
|
||||
&& echo /opt/oracle/instantclient* > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& ldconfig
|
||||
|
||||
# Application layer
|
||||
FROM oracle_base
|
||||
WORKDIR /app
|
||||
|
||||
# Copy TNS configuration for Oracle connection
|
||||
COPY tnsnames.ora /app/tnsnames.ora
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Health check endpoint
|
||||
EXPOSE 5000
|
||||
|
||||
# Run Flask application
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "admin:app", "--reload", "--access-logfile", "-"]
|
||||
241
api/admin.py
Normal file
241
api/admin.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
Flask Admin Interface pentru Import Comenzi Web → ROA
|
||||
Gestionează mapările SKU în tabelul ARTICOLE_TERTI
|
||||
"""
|
||||
|
||||
from flask import Flask, jsonify, request, render_template_string
|
||||
from flask_cors import CORS
|
||||
from dotenv import load_dotenv
|
||||
import oracledb
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
# Configurare environment
|
||||
load_dotenv()
|
||||
|
||||
# Configurare logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s | %(levelname)s | %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('/app/logs/admin.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Environment Variables pentru Oracle
|
||||
user = os.environ['ORACLE_USER']
|
||||
password = os.environ['ORACLE_PASSWORD']
|
||||
dsn = os.environ['ORACLE_DSN']
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
def start_pool():
|
||||
"""Inițializează connection pool Oracle"""
|
||||
try:
|
||||
# Configurare Oracle client
|
||||
instantclient_path = os.environ.get('INSTANTCLIENTPATH')
|
||||
if instantclient_path:
|
||||
oracledb.init_oracle_client(lib_dir=instantclient_path)
|
||||
else:
|
||||
oracledb.init_oracle_client(config_dir='/app')
|
||||
|
||||
pool = oracledb.create_pool(
|
||||
user=user,
|
||||
password=password,
|
||||
dsn=dsn,
|
||||
min=2,
|
||||
max=4,
|
||||
increment=1
|
||||
)
|
||||
logger.info(f"Oracle pool creat cu succes pentru {dsn}")
|
||||
return pool
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare creare pool Oracle: {e}")
|
||||
raise
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
"""Health check pentru Docker"""
|
||||
return jsonify({"status": "ok", "timestamp": datetime.now().isoformat()})
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
"""Pagina principală admin interface"""
|
||||
html_template = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GoMag Admin - Mapări SKU</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }
|
||||
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
h1 { color: #333; border-bottom: 3px solid #007bff; padding-bottom: 10px; }
|
||||
.status { padding: 10px; border-radius: 4px; margin: 10px 0; }
|
||||
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||
.btn { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }
|
||||
.btn:hover { background: #0056b3; }
|
||||
.table-container { margin-top: 20px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #ddd; }
|
||||
th { background-color: #f8f9fa; font-weight: bold; }
|
||||
tr:hover { background-color: #f5f5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🛍️ GoMag Admin - Import Comenzi Web → ROA</h1>
|
||||
|
||||
<div id="status-area">
|
||||
<div class="success">✅ Container Docker activ pe port 5003</div>
|
||||
<div id="db-status">🔄 Verificare conexiune Oracle...</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<h2>📋 Mapări SKU Active</h2>
|
||||
<button class="btn" onclick="loadMappings()">🔄 Reîmprospătează</button>
|
||||
<button class="btn" onclick="testConnection()">🔍 Test Conexiune DB</button>
|
||||
|
||||
<div id="mappings-container">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Test conexiune la load
|
||||
window.onload = function() {
|
||||
testConnection();
|
||||
loadMappings();
|
||||
}
|
||||
|
||||
function testConnection() {
|
||||
fetch('/test-db')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const statusDiv = document.getElementById('db-status');
|
||||
if (data.success) {
|
||||
statusDiv.className = 'status success';
|
||||
statusDiv.innerHTML = '✅ Oracle conectat: ' + data.message;
|
||||
} else {
|
||||
statusDiv.className = 'status error';
|
||||
statusDiv.innerHTML = '❌ Eroare Oracle: ' + data.error;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('db-status').innerHTML = '❌ Eroare fetch: ' + error;
|
||||
});
|
||||
}
|
||||
|
||||
function loadMappings() {
|
||||
fetch('/api/mappings')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let html = '<table>';
|
||||
html += '<tr><th>SKU</th><th>CODMAT</th><th>Cantitate ROA</th><th>Procent Preț</th><th>Activ</th><th>Data Creare</th></tr>';
|
||||
|
||||
if (data.mappings && data.mappings.length > 0) {
|
||||
data.mappings.forEach(row => {
|
||||
const activIcon = row[4] === 1 ? '✅' : '❌';
|
||||
html += `<tr>
|
||||
<td><strong>${row[0]}</strong></td>
|
||||
<td>${row[1]}</td>
|
||||
<td>${row[2]}</td>
|
||||
<td>${row[3]}%</td>
|
||||
<td>${activIcon}</td>
|
||||
<td>${new Date(row[5]).toLocaleDateString()}</td>
|
||||
</tr>`;
|
||||
});
|
||||
} else {
|
||||
html += '<tr><td colspan="6">Nu există mapări configurate</td></tr>';
|
||||
}
|
||||
html += '</table>';
|
||||
|
||||
document.getElementById('mappings-container').innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('mappings-container').innerHTML = '❌ Eroare: ' + error;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return render_template_string(html_template)
|
||||
|
||||
@app.route('/test-db')
|
||||
def test_db():
|
||||
"""Test conexiune Oracle și verificare tabel"""
|
||||
try:
|
||||
with pool.acquire() as con:
|
||||
with con.cursor() as cur:
|
||||
# Test conexiune de bază
|
||||
cur.execute("SELECT SYSDATE FROM DUAL")
|
||||
db_date = cur.fetchone()[0]
|
||||
|
||||
# Verificare existență tabel ARTICOLE_TERTI
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM USER_TABLES
|
||||
WHERE TABLE_NAME = 'ARTICOLE_TERTI'
|
||||
""")
|
||||
table_exists = cur.fetchone()[0] > 0
|
||||
|
||||
if not table_exists:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Tabelul ARTICOLE_TERTI nu există. Rulează 01_create_table.sql"
|
||||
})
|
||||
|
||||
# Count records
|
||||
cur.execute("SELECT COUNT(*) FROM ARTICOLE_TERTI")
|
||||
record_count = cur.fetchone()[0]
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"DB Time: {db_date}, Records: {record_count}",
|
||||
"table_exists": table_exists,
|
||||
"record_count": record_count
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Test DB failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)})
|
||||
|
||||
@app.route('/api/mappings')
|
||||
def get_mappings():
|
||||
"""Returnează toate mapările SKU active"""
|
||||
try:
|
||||
with pool.acquire() as con:
|
||||
with con.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT sku, codmat, cantitate_roa, procent_pret, activ, data_creare
|
||||
FROM ARTICOLE_TERTI
|
||||
ORDER BY sku, codmat
|
||||
""")
|
||||
mappings = cur.fetchall()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"mappings": mappings,
|
||||
"count": len(mappings)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get mappings failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)})
|
||||
|
||||
# Inițializare pool la startup
|
||||
try:
|
||||
pool = start_pool()
|
||||
logger.info("Admin interface started successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start admin interface: {e}")
|
||||
pool = None
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
5
api/requirements.txt
Normal file
5
api/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Flask==2.3.2
|
||||
Flask-CORS==4.0.0
|
||||
oracledb==1.4.2
|
||||
python-dotenv==1.0.0
|
||||
gunicorn==21.2.0
|
||||
9
api/tnsnames.ora
Normal file
9
api/tnsnames.ora
Normal file
@@ -0,0 +1,9 @@
|
||||
ROA_ROMFAST =
|
||||
(DESCRIPTION =
|
||||
(ADDRESS_LIST =
|
||||
(ADDRESS = (PROTOCOL = TCP)(HOST = 10.0.20.36)(PORT = 1521))
|
||||
)
|
||||
(CONNECT_DATA =
|
||||
(SID = ROA)
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user