feat: multi-Oracle server support with runtime switching
Complete implementation of multi-server Oracle database support: Backend: - Multi-pool Oracle with lazy loading per server - Email-to-server cache for automatic server discovery - JWT tokens include server_id claim - /auth/check-identity and /auth/check-email endpoints - /auth/my-servers endpoint for listing user's accessible servers - Server switch with password re-authentication Frontend: - New ServerSelector component for header dropdown - Multi-step login flow (identity → server → password) - Server switching from header with password modal - Mobile drawer menu with server selection - Dark mode support for all new components - URL bookmark support with ?server= query param Scripts: - Unified start.sh replacing start-prod.sh/start-test.sh - Unified ssh-tunnel.sh with multi-server support - Updated status.sh for new architecture Tests: - E2E tests for multi-server and single-server login flows - Backend unit tests for all new endpoints - Oracle multi-pool integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,10 @@ class CacheManager:
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# Close SQLite connection manager
|
||||
if hasattr(self.sqlite, 'close'):
|
||||
await self.sqlite.close()
|
||||
|
||||
logger.info("Cache closed")
|
||||
|
||||
async def get(self, key: str, cache_type: str) -> Optional[Any]:
|
||||
|
||||
39
backend/modules/reports/cache/decorators.py
vendored
39
backend/modules/reports/cache/decorators.py
vendored
@@ -3,6 +3,8 @@ Cache decorators for service methods
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
import sqlite3
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from typing import Callable, Optional, List
|
||||
|
||||
@@ -11,6 +13,10 @@ from .keys import generate_cache_key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Retry configuration for SQLite locked database errors
|
||||
SQLITE_MAX_RETRIES = 3
|
||||
SQLITE_RETRY_BASE_DELAY = 0.1 # 100ms base delay, exponential backoff
|
||||
|
||||
|
||||
def cached(cache_type: str, ttl: Optional[int] = None, key_params: Optional[List[str]] = None):
|
||||
"""
|
||||
@@ -73,8 +79,21 @@ def cached(cache_type: str, ttl: Optional[int] = None, key_params: Optional[List
|
||||
# Generate cache key from function parameters
|
||||
cache_key = generate_cache_key(cache_type, key_params, args, kwargs)
|
||||
|
||||
# Try to get from cache
|
||||
cached_value = await cache.get(cache_key, cache_type)
|
||||
# Try to get from cache with retry logic for SQLite locks
|
||||
cached_value = None
|
||||
for attempt in range(SQLITE_MAX_RETRIES):
|
||||
try:
|
||||
cached_value = await cache.get(cache_key, cache_type)
|
||||
break
|
||||
except sqlite3.OperationalError as e:
|
||||
if "database is locked" in str(e) and attempt < SQLITE_MAX_RETRIES - 1:
|
||||
delay = SQLITE_RETRY_BASE_DELAY * (attempt + 1)
|
||||
logger.warning(f"SQLite locked on cache.get, retry {attempt + 1}/{SQLITE_MAX_RETRIES} after {delay}s")
|
||||
await asyncio.sleep(delay)
|
||||
else:
|
||||
logger.error(f"SQLite error after {attempt + 1} retries: {e}")
|
||||
cached_value = None
|
||||
break
|
||||
|
||||
if cached_value is not None:
|
||||
# ✅ CACHE HIT
|
||||
@@ -128,9 +147,21 @@ def cached(cache_type: str, ttl: Optional[int] = None, key_params: Optional[List
|
||||
username=username
|
||||
)
|
||||
|
||||
# Store in cache for next time
|
||||
# Store in cache for next time (with retry logic for SQLite locks)
|
||||
company_id = _extract_company_id(args, kwargs, key_params)
|
||||
await cache.set(cache_key, result, cache_type, company_id, ttl)
|
||||
for attempt in range(SQLITE_MAX_RETRIES):
|
||||
try:
|
||||
await cache.set(cache_key, result, cache_type, company_id, ttl)
|
||||
break
|
||||
except sqlite3.OperationalError as e:
|
||||
if "database is locked" in str(e) and attempt < SQLITE_MAX_RETRIES - 1:
|
||||
delay = SQLITE_RETRY_BASE_DELAY * (attempt + 1)
|
||||
logger.warning(f"SQLite locked on cache.set, retry {attempt + 1}/{SQLITE_MAX_RETRIES} after {delay}s")
|
||||
await asyncio.sleep(delay)
|
||||
else:
|
||||
logger.error(f"SQLite error on cache.set after {attempt + 1} retries: {e}")
|
||||
# Don't fail the request, just skip caching
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
|
||||
424
backend/modules/reports/cache/sqlite_cache.py
vendored
424
backend/modules/reports/cache/sqlite_cache.py
vendored
@@ -1,16 +1,23 @@
|
||||
"""
|
||||
SQLite persistent cache (L2 cache)
|
||||
Persistent, survives restarts, unlimited size
|
||||
|
||||
Uses singleton connection pattern with asyncio.Lock for write serialization
|
||||
to prevent "database is locked" errors under concurrent access.
|
||||
"""
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
import aiosqlite
|
||||
from typing import Any, Optional, List, Dict
|
||||
from pathlib import Path
|
||||
from decimal import Decimal
|
||||
from datetime import datetime, date
|
||||
|
||||
# SQLite busy timeout in milliseconds (wait for lock instead of failing immediately)
|
||||
SQLITE_BUSY_TIMEOUT_MS = 5000
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -31,6 +38,163 @@ class CustomJSONEncoder(json.JSONEncoder):
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
class SQLiteConnectionManager:
|
||||
"""
|
||||
Singleton connection manager with write serialization.
|
||||
|
||||
Solves "database is locked" errors by:
|
||||
1. Maintaining a single persistent connection (instead of N connections per request)
|
||||
2. Serializing all write operations through an asyncio.Lock
|
||||
3. Using WAL mode for better concurrent read performance
|
||||
|
||||
Architecture:
|
||||
┌─────────────────────────────────────┐
|
||||
│ SQLiteConnectionManager │
|
||||
│ (SINGLETON) │
|
||||
│ │
|
||||
│ _connection: aiosqlite.Connection │
|
||||
│ _write_lock: asyncio.Lock │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
Task 1 Task 2 Task N
|
||||
cache.get() cache.set() cache.get()
|
||||
│ │ │
|
||||
└───────────────┴───────────────┘
|
||||
│
|
||||
async with _write_lock:
|
||||
(serialized writes)
|
||||
"""
|
||||
|
||||
_instance: Optional['SQLiteConnectionManager'] = None
|
||||
_instance_lock: asyncio.Lock = None # Will be created on first use
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
"""
|
||||
Initialize connection manager (called only by get_instance).
|
||||
|
||||
Args:
|
||||
db_path: Path to SQLite database file
|
||||
"""
|
||||
self.db_path = db_path
|
||||
self._connection: Optional[aiosqlite.Connection] = None
|
||||
self._write_lock: Optional[asyncio.Lock] = None
|
||||
self._initialized = False
|
||||
|
||||
@classmethod
|
||||
async def get_instance(cls, db_path: str) -> 'SQLiteConnectionManager':
|
||||
"""
|
||||
Get or create singleton instance.
|
||||
|
||||
Thread-safe singleton pattern using asyncio.Lock.
|
||||
|
||||
Args:
|
||||
db_path: Path to SQLite database file
|
||||
|
||||
Returns:
|
||||
SQLiteConnectionManager singleton instance
|
||||
"""
|
||||
# Create instance lock on first call (must be done in async context)
|
||||
if cls._instance_lock is None:
|
||||
cls._instance_lock = asyncio.Lock()
|
||||
|
||||
async with cls._instance_lock:
|
||||
if cls._instance is None or cls._instance.db_path != db_path:
|
||||
cls._instance = cls(db_path)
|
||||
return cls._instance
|
||||
|
||||
async def initialize(self):
|
||||
"""
|
||||
Create connection with WAL mode and busy timeout.
|
||||
|
||||
Sets up:
|
||||
- Busy timeout (5 seconds) - wait for locks instead of failing
|
||||
- WAL journal mode - allows concurrent reads while writing
|
||||
- Write lock for serializing write operations
|
||||
"""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
# Create write lock in async context
|
||||
self._write_lock = asyncio.Lock()
|
||||
|
||||
# Create persistent connection
|
||||
self._connection = await aiosqlite.connect(self.db_path)
|
||||
await self._connection.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
|
||||
await self._connection.execute("PRAGMA journal_mode=WAL")
|
||||
await self._connection.commit()
|
||||
|
||||
self._initialized = True
|
||||
logger.info(f"SQLite connection manager initialized: {self.db_path}")
|
||||
|
||||
async def get_connection(self) -> aiosqlite.Connection:
|
||||
"""
|
||||
Get the persistent connection, with health check.
|
||||
|
||||
If connection is unhealthy (closed or stale), reconnects automatically.
|
||||
|
||||
Returns:
|
||||
Active aiosqlite connection
|
||||
"""
|
||||
if self._connection is None or not await self._is_healthy():
|
||||
await self._reconnect()
|
||||
return self._connection
|
||||
|
||||
async def _is_healthy(self) -> bool:
|
||||
"""
|
||||
Check if connection is valid.
|
||||
|
||||
Returns:
|
||||
True if connection can execute queries, False otherwise
|
||||
"""
|
||||
try:
|
||||
async with self._connection.execute("SELECT 1") as cursor:
|
||||
await cursor.fetchone()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _reconnect(self):
|
||||
"""Reconnect if connection was lost."""
|
||||
logger.warning("SQLite connection unhealthy, reconnecting...")
|
||||
|
||||
# Close old connection if exists
|
||||
if self._connection:
|
||||
try:
|
||||
await self._connection.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Create new connection
|
||||
self._connection = await aiosqlite.connect(self.db_path)
|
||||
await self._connection.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
|
||||
await self._connection.execute("PRAGMA journal_mode=WAL")
|
||||
await self._connection.commit()
|
||||
|
||||
logger.info("SQLite connection re-established")
|
||||
|
||||
@property
|
||||
def write_lock(self) -> asyncio.Lock:
|
||||
"""Get the write lock for serializing write operations."""
|
||||
return self._write_lock
|
||||
|
||||
async def close(self):
|
||||
"""Close the connection and reset singleton."""
|
||||
if self._connection:
|
||||
try:
|
||||
await self._connection.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error closing SQLite connection: {e}")
|
||||
|
||||
self._connection = None
|
||||
self._initialized = False
|
||||
|
||||
# Reset singleton
|
||||
SQLiteConnectionManager._instance = None
|
||||
logger.info("SQLite connection manager closed")
|
||||
|
||||
|
||||
class SQLiteCache:
|
||||
"""
|
||||
SQLite-based persistent cache
|
||||
@@ -41,6 +205,7 @@ class SQLiteCache:
|
||||
- Schema mappings (permanent cache for company->schema)
|
||||
- Watermarks for event-based invalidation
|
||||
- Performance tracking and benchmarks
|
||||
- Singleton connection with write serialization (prevents "database is locked")
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
@@ -51,6 +216,7 @@ class SQLiteCache:
|
||||
db_path: Path to SQLite database file
|
||||
"""
|
||||
self.db_path = db_path
|
||||
self._conn_manager: Optional[SQLiteConnectionManager] = None
|
||||
self._ensure_db_dir()
|
||||
|
||||
def _ensure_db_dir(self):
|
||||
@@ -60,13 +226,16 @@ class SQLiteCache:
|
||||
|
||||
async def init_db(self):
|
||||
"""Initialize database schema with WAL mode enabled"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Enable Write-Ahead Logging (WAL) mode for better concurrency
|
||||
await db.execute("PRAGMA journal_mode=WAL")
|
||||
await db.commit()
|
||||
# Get or create singleton connection manager
|
||||
self._conn_manager = await SQLiteConnectionManager.get_instance(self.db_path)
|
||||
await self._conn_manager.initialize()
|
||||
|
||||
# Create tables using the persistent connection
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
|
||||
# Table: cache_entries
|
||||
await db.execute("""
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS cache_entries (
|
||||
cache_key TEXT PRIMARY KEY,
|
||||
cache_type TEXT NOT NULL,
|
||||
@@ -78,12 +247,12 @@ class SQLiteCache:
|
||||
last_accessed REAL
|
||||
)
|
||||
""")
|
||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_cache_type ON cache_entries(cache_type)")
|
||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_company_id ON cache_entries(company_id)")
|
||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_expires_at ON cache_entries(expires_at)")
|
||||
await conn.execute("CREATE INDEX IF NOT EXISTS idx_cache_type ON cache_entries(cache_type)")
|
||||
await conn.execute("CREATE INDEX IF NOT EXISTS idx_company_id ON cache_entries(company_id)")
|
||||
await conn.execute("CREATE INDEX IF NOT EXISTS idx_expires_at ON cache_entries(expires_at)")
|
||||
|
||||
# Table: schema_mappings (PERMANENT)
|
||||
await db.execute("""
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS schema_mappings (
|
||||
id_firma INTEGER PRIMARY KEY,
|
||||
schema TEXT NOT NULL,
|
||||
@@ -93,7 +262,7 @@ class SQLiteCache:
|
||||
""")
|
||||
|
||||
# Table: query_benchmarks
|
||||
await db.execute("""
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS query_benchmarks (
|
||||
cache_type TEXT PRIMARY KEY,
|
||||
avg_time_ms REAL NOT NULL,
|
||||
@@ -103,7 +272,7 @@ class SQLiteCache:
|
||||
""")
|
||||
|
||||
# Table: performance_log
|
||||
await db.execute("""
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS performance_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cache_type TEXT NOT NULL,
|
||||
@@ -116,11 +285,11 @@ class SQLiteCache:
|
||||
timestamp REAL NOT NULL
|
||||
)
|
||||
""")
|
||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_perf_timestamp ON performance_log(timestamp)")
|
||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_perf_cache_type ON performance_log(cache_type)")
|
||||
await conn.execute("CREATE INDEX IF NOT EXISTS idx_perf_timestamp ON performance_log(timestamp)")
|
||||
await conn.execute("CREATE INDEX IF NOT EXISTS idx_perf_cache_type ON performance_log(cache_type)")
|
||||
|
||||
# Table: user_cache_settings
|
||||
await db.execute("""
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS user_cache_settings (
|
||||
username TEXT PRIMARY KEY,
|
||||
cache_enabled BOOLEAN DEFAULT TRUE,
|
||||
@@ -130,7 +299,7 @@ class SQLiteCache:
|
||||
""")
|
||||
|
||||
# Table: cache_config
|
||||
await db.execute("""
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS cache_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
@@ -139,7 +308,7 @@ class SQLiteCache:
|
||||
""")
|
||||
|
||||
# Table: cache_watermarks
|
||||
await db.execute("""
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS cache_watermarks (
|
||||
company_id INTEGER PRIMARY KEY,
|
||||
schema TEXT NOT NULL,
|
||||
@@ -148,7 +317,7 @@ class SQLiteCache:
|
||||
)
|
||||
""")
|
||||
|
||||
await db.commit()
|
||||
await conn.commit()
|
||||
logger.info("SQLite cache database initialized")
|
||||
|
||||
async def get(self, key: str) -> Optional[Any]:
|
||||
@@ -161,8 +330,11 @@ class SQLiteCache:
|
||||
Returns:
|
||||
Cached value or None if not found/expired
|
||||
"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute("""
|
||||
# Use write lock because we may update hit_count or delete expired entries
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
|
||||
async with conn.execute("""
|
||||
SELECT data_json, expires_at
|
||||
FROM cache_entries
|
||||
WHERE cache_key = ?
|
||||
@@ -177,18 +349,18 @@ class SQLiteCache:
|
||||
# Check TTL expiration
|
||||
if expires_at < time.time():
|
||||
# Expired - delete and return None
|
||||
await db.execute("DELETE FROM cache_entries WHERE cache_key = ?", (key,))
|
||||
await db.commit()
|
||||
await conn.execute("DELETE FROM cache_entries WHERE cache_key = ?", (key,))
|
||||
await conn.commit()
|
||||
logger.debug(f"SQLite cache expired: {key}")
|
||||
return None
|
||||
|
||||
# Update hit_count and last_accessed
|
||||
await db.execute("""
|
||||
await conn.execute("""
|
||||
UPDATE cache_entries
|
||||
SET hit_count = hit_count + 1, last_accessed = ?
|
||||
WHERE cache_key = ?
|
||||
""", (time.time(), key))
|
||||
await db.commit()
|
||||
await conn.commit()
|
||||
|
||||
logger.debug(f"SQLite cache HIT: {key}")
|
||||
return json.loads(data_json)
|
||||
@@ -209,21 +381,23 @@ class SQLiteCache:
|
||||
now = time.time()
|
||||
expires_at = now + ttl
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("""
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
await conn.execute("""
|
||||
INSERT OR REPLACE INTO cache_entries
|
||||
(cache_key, cache_type, company_id, data_json, created_at, expires_at, hit_count, last_accessed)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0, ?)
|
||||
""", (key, cache_type, company_id, data_json, now, expires_at, now))
|
||||
await db.commit()
|
||||
await conn.commit()
|
||||
|
||||
logger.debug(f"SQLite cache SET: {key} (TTL: {ttl}s)")
|
||||
|
||||
async def delete(self, key: str) -> bool:
|
||||
"""Delete entry from cache"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
cursor = await db.execute("DELETE FROM cache_entries WHERE cache_key = ?", (key,))
|
||||
await db.commit()
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
cursor = await conn.execute("DELETE FROM cache_entries WHERE cache_key = ?", (key,))
|
||||
await conn.commit()
|
||||
deleted = cursor.rowcount > 0
|
||||
if deleted:
|
||||
logger.debug(f"SQLite cache deleted: {key}")
|
||||
@@ -231,33 +405,37 @@ class SQLiteCache:
|
||||
|
||||
async def clear(self):
|
||||
"""Clear all cache entries"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
cursor = await db.execute("DELETE FROM cache_entries")
|
||||
await db.commit()
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
cursor = await conn.execute("DELETE FROM cache_entries")
|
||||
await conn.commit()
|
||||
count = cursor.rowcount
|
||||
logger.info(f"SQLite cache cleared: {count} entries removed")
|
||||
|
||||
async def clear_by_company(self, company_id: int):
|
||||
"""Clear all entries for specific company"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
cursor = await db.execute("DELETE FROM cache_entries WHERE company_id = ?", (company_id,))
|
||||
await db.commit()
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
cursor = await conn.execute("DELETE FROM cache_entries WHERE company_id = ?", (company_id,))
|
||||
await conn.commit()
|
||||
count = cursor.rowcount
|
||||
logger.info(f"SQLite cache cleared for company {company_id}: {count} entries")
|
||||
|
||||
async def clear_by_type(self, cache_type: str):
|
||||
"""Clear all entries of specific type"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
cursor = await db.execute("DELETE FROM cache_entries WHERE cache_type = ?", (cache_type,))
|
||||
await db.commit()
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
cursor = await conn.execute("DELETE FROM cache_entries WHERE cache_type = ?", (cache_type,))
|
||||
await conn.commit()
|
||||
count = cursor.rowcount
|
||||
logger.info(f"SQLite cache cleared for type '{cache_type}': {count} entries")
|
||||
|
||||
async def cleanup_expired(self):
|
||||
"""Remove all expired entries"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
cursor = await db.execute("DELETE FROM cache_entries WHERE expires_at < ?", (time.time(),))
|
||||
await db.commit()
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
cursor = await conn.execute("DELETE FROM cache_entries WHERE expires_at < ?", (time.time(),))
|
||||
await conn.commit()
|
||||
count = cursor.rowcount
|
||||
if count > 0:
|
||||
logger.info(f"SQLite cache cleanup: {count} expired entries removed")
|
||||
@@ -265,48 +443,50 @@ class SQLiteCache:
|
||||
# Schema Mappings (PERMANENT)
|
||||
|
||||
async def get_schema_mapping(self, company_id: int) -> Optional[str]:
|
||||
"""Get permanent cached schema for company"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute("""
|
||||
SELECT schema
|
||||
FROM schema_mappings
|
||||
WHERE id_firma = ?
|
||||
""", (company_id,)) as cursor:
|
||||
result = await cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
"""Get permanent cached schema for company (READ-ONLY, no lock needed)"""
|
||||
conn = await self._conn_manager.get_connection()
|
||||
async with conn.execute("""
|
||||
SELECT schema
|
||||
FROM schema_mappings
|
||||
WHERE id_firma = ?
|
||||
""", (company_id,)) as cursor:
|
||||
result = await cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
|
||||
async def set_schema_mapping(self, company_id: int, schema: str):
|
||||
"""Set permanent schema mapping (never expires)"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("""
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
await conn.execute("""
|
||||
INSERT OR REPLACE INTO schema_mappings
|
||||
(id_firma, schema, created_at, last_verified)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (company_id, schema, time.time(), time.time()))
|
||||
await db.commit()
|
||||
await conn.commit()
|
||||
|
||||
# Benchmarks
|
||||
|
||||
async def get_benchmark(self, cache_type: str) -> Optional[float]:
|
||||
"""Get average benchmark time for cache type"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute("""
|
||||
SELECT avg_time_ms
|
||||
FROM query_benchmarks
|
||||
WHERE cache_type = ?
|
||||
""", (cache_type,)) as cursor:
|
||||
result = await cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
"""Get average benchmark time for cache type (READ-ONLY, no lock needed)"""
|
||||
conn = await self._conn_manager.get_connection()
|
||||
async with conn.execute("""
|
||||
SELECT avg_time_ms
|
||||
FROM query_benchmarks
|
||||
WHERE cache_type = ?
|
||||
""", (cache_type,)) as cursor:
|
||||
result = await cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
|
||||
async def set_benchmark(self, cache_type: str, avg_time_ms: float, sample_count: int):
|
||||
"""Set/update benchmark"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("""
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
await conn.execute("""
|
||||
INSERT OR REPLACE INTO query_benchmarks
|
||||
(cache_type, avg_time_ms, sample_count, last_updated)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (cache_type, avg_time_ms, sample_count, time.time()))
|
||||
await db.commit()
|
||||
await conn.commit()
|
||||
|
||||
# Performance Tracking
|
||||
|
||||
@@ -314,91 +494,101 @@ class SQLiteCache:
|
||||
response_time_ms: float, estimated_oracle_time_ms: Optional[float],
|
||||
time_saved_ms: Optional[float], username: Optional[str]):
|
||||
"""Log performance metric"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("""
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
await conn.execute("""
|
||||
INSERT INTO performance_log
|
||||
(cache_type, company_id, cache_hit, response_time_ms, estimated_oracle_time_ms,
|
||||
time_saved_ms, username, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (cache_type, company_id, cache_hit, response_time_ms, estimated_oracle_time_ms,
|
||||
time_saved_ms, username, time.time()))
|
||||
await db.commit()
|
||||
await conn.commit()
|
||||
|
||||
# User Settings
|
||||
|
||||
async def get_user_cache_enabled(self, username: str) -> bool:
|
||||
"""Get user cache setting (default True)"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute("""
|
||||
SELECT cache_enabled
|
||||
FROM user_cache_settings
|
||||
WHERE username = ?
|
||||
""", (username,)) as cursor:
|
||||
result = await cursor.fetchone()
|
||||
return bool(result[0]) if result else True # Default enabled, explicit bool conversion
|
||||
"""Get user cache setting (default True) - READ-ONLY, no lock needed"""
|
||||
conn = await self._conn_manager.get_connection()
|
||||
async with conn.execute("""
|
||||
SELECT cache_enabled
|
||||
FROM user_cache_settings
|
||||
WHERE username = ?
|
||||
""", (username,)) as cursor:
|
||||
result = await cursor.fetchone()
|
||||
return bool(result[0]) if result else True # Default enabled, explicit bool conversion
|
||||
|
||||
async def set_user_cache_enabled(self, username: str, enabled: bool):
|
||||
"""Set user cache setting"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("""
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
await conn.execute("""
|
||||
INSERT OR REPLACE INTO user_cache_settings
|
||||
(username, cache_enabled, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (username, enabled, time.time(), time.time()))
|
||||
await db.commit()
|
||||
await conn.commit()
|
||||
|
||||
# Watermarks
|
||||
|
||||
async def get_watermark(self, company_id: int) -> Optional[int]:
|
||||
"""Get cached watermark (max_id_act) for company"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute("""
|
||||
SELECT max_id_act
|
||||
FROM cache_watermarks
|
||||
WHERE company_id = ?
|
||||
""", (company_id,)) as cursor:
|
||||
result = await cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
"""Get cached watermark (max_id_act) for company - READ-ONLY, no lock needed"""
|
||||
conn = await self._conn_manager.get_connection()
|
||||
async with conn.execute("""
|
||||
SELECT max_id_act
|
||||
FROM cache_watermarks
|
||||
WHERE company_id = ?
|
||||
""", (company_id,)) as cursor:
|
||||
result = await cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
|
||||
async def set_watermark(self, company_id: int, schema: str, max_id_act: int):
|
||||
"""Set/update watermark for company"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("""
|
||||
async with self._conn_manager.write_lock:
|
||||
conn = await self._conn_manager.get_connection()
|
||||
await conn.execute("""
|
||||
INSERT OR REPLACE INTO cache_watermarks
|
||||
(company_id, schema, max_id_act, checked_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (company_id, schema, max_id_act, time.time()))
|
||||
await db.commit()
|
||||
await conn.commit()
|
||||
|
||||
async def get_cached_company_ids(self) -> List[int]:
|
||||
"""Get list of company_ids with active cache entries"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute("""
|
||||
SELECT DISTINCT company_id
|
||||
FROM cache_entries
|
||||
WHERE company_id IS NOT NULL
|
||||
AND expires_at > ?
|
||||
""", (time.time(),)) as cursor:
|
||||
results = await cursor.fetchall()
|
||||
return [row[0] for row in results]
|
||||
"""Get list of company_ids with active cache entries - READ-ONLY, no lock needed"""
|
||||
conn = await self._conn_manager.get_connection()
|
||||
async with conn.execute("""
|
||||
SELECT DISTINCT company_id
|
||||
FROM cache_entries
|
||||
WHERE company_id IS NOT NULL
|
||||
AND expires_at > ?
|
||||
""", (time.time(),)) as cursor:
|
||||
results = await cursor.fetchall()
|
||||
return [row[0] for row in results]
|
||||
|
||||
# Statistics
|
||||
|
||||
async def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get cache statistics"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Total entries
|
||||
async with db.execute("SELECT COUNT(*) FROM cache_entries") as cursor:
|
||||
total_entries = (await cursor.fetchone())[0]
|
||||
"""Get cache statistics - READ-ONLY, no lock needed"""
|
||||
conn = await self._conn_manager.get_connection()
|
||||
|
||||
# Active entries (not expired)
|
||||
async with db.execute("""
|
||||
SELECT COUNT(*) FROM cache_entries WHERE expires_at > ?
|
||||
""", (time.time(),)) as cursor:
|
||||
active_entries = (await cursor.fetchone())[0]
|
||||
# Total entries
|
||||
async with conn.execute("SELECT COUNT(*) FROM cache_entries") as cursor:
|
||||
total_entries = (await cursor.fetchone())[0]
|
||||
|
||||
return {
|
||||
'total_entries': total_entries,
|
||||
'active_entries': active_entries,
|
||||
'expired_entries': total_entries - active_entries
|
||||
}
|
||||
# Active entries (not expired)
|
||||
async with conn.execute("""
|
||||
SELECT COUNT(*) FROM cache_entries WHERE expires_at > ?
|
||||
""", (time.time(),)) as cursor:
|
||||
active_entries = (await cursor.fetchone())[0]
|
||||
|
||||
return {
|
||||
'total_entries': total_entries,
|
||||
'active_entries': active_entries,
|
||||
'expired_entries': total_entries - active_entries
|
||||
}
|
||||
|
||||
async def close(self):
|
||||
"""Close the connection manager"""
|
||||
if self._conn_manager:
|
||||
await self._conn_manager.close()
|
||||
self._conn_manager = None
|
||||
|
||||
@@ -36,7 +36,8 @@ async def get_dashboard_summary(
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_complete_summary(company, current_user.username, luna=luna, an=an, request=request)
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_complete_summary(company, current_user.username, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert Pydantic model to dict for JSON serialization
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
@@ -91,8 +92,9 @@ async def get_dashboard_trends(
|
||||
detail=f"Perioadă nevalidă: {period}. Valori permise: {', '.join(valid_periods)}"
|
||||
)
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
# Obține datele de trenduri
|
||||
result = await DashboardService.get_trends(int(company), period, luna=luna, an=an, request=request)
|
||||
result = await DashboardService.get_trends(int(company), period, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
@@ -120,6 +122,7 @@ async def get_dashboard_trends(
|
||||
|
||||
@router.get("/detailed-data")
|
||||
async def get_detailed_data(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
data_type: str = Query(description="Tipul de date: clients, suppliers, treasury"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
@@ -137,6 +140,7 @@ async def get_detailed_data(
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
logger.info(f"[ROUTER] Calling DashboardService.get_detailed_data")
|
||||
result = await DashboardService.get_detailed_data(
|
||||
company=company,
|
||||
@@ -145,7 +149,8 @@ async def get_detailed_data(
|
||||
an=an,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search
|
||||
search=search,
|
||||
server_id=server_id
|
||||
)
|
||||
|
||||
logger.info(f"[ROUTER] Service returned: {len(result.get('data', []))} rows")
|
||||
@@ -157,13 +162,14 @@ async def get_detailed_data(
|
||||
|
||||
@router.get("/performance")
|
||||
async def get_performance(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m|ytd|12m)$", description="Perioada pentru analiză"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează date performanță pentru perioada selectată
|
||||
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează grafice încasări vs plăți pentru perioada selectată
|
||||
- Calculează indicatori: rata încasării, cash conversion, working capital
|
||||
@@ -172,8 +178,9 @@ async def get_performance(
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_performance_data(company, period)
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_performance_data(company, period, server_id=server_id)
|
||||
|
||||
# Convert to Chart.js compatible format
|
||||
return {
|
||||
@@ -195,13 +202,14 @@ async def get_performance(
|
||||
|
||||
@router.get("/cashflow")
|
||||
async def get_cashflow(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m)$", description="Perioada pentru previziune"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează previziune cash flow pentru perioada selectată
|
||||
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Analizează scadențele viitoare pentru calculul cash flow-ului
|
||||
- Identifică zilele critice cu deficit de cash
|
||||
@@ -210,8 +218,9 @@ async def get_cashflow(
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_cashflow_forecast(company, period)
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_cashflow_forecast(company, period, server_id=server_id)
|
||||
|
||||
# Convert to Chart.js compatible format
|
||||
return {
|
||||
@@ -263,7 +272,8 @@ async def get_maturity_analysis(
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_maturity_analysis(company, period, luna=luna, an=an, request=request)
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_maturity_analysis(company, period, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
@@ -308,8 +318,9 @@ async def get_monthly_flows(
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
# Apelăm serviciul cu request pentru cache metadata
|
||||
result = await DashboardService.get_monthly_flows(company, luna=luna, an=an, request=request)
|
||||
result = await DashboardService.get_monthly_flows(company, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
@@ -353,7 +364,8 @@ async def get_treasury_breakdown(
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_treasury_breakdown(company, luna=luna, an=an, request=request)
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_treasury_breakdown(company, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
@@ -398,7 +410,8 @@ async def get_net_balance_breakdown(
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_net_balance_breakdown(company, luna=luna, an=an, request=request)
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_net_balance_breakdown(company, luna=luna, an=an, request=request, server_id=server_id)
|
||||
|
||||
# Convert to dict if needed
|
||||
result_dict = result.dict() if hasattr(result, 'dict') else result
|
||||
@@ -424,6 +437,7 @@ async def get_net_balance_breakdown(
|
||||
|
||||
@router.get("/current-period")
|
||||
async def get_current_period(
|
||||
request: Request,
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
@@ -439,7 +453,8 @@ async def get_current_period(
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_current_period(company)
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
result = await DashboardService.get_current_period(company, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
@@ -502,9 +517,11 @@ async def get_financial_indicators(
|
||||
resolved_luna: int
|
||||
resolved_an: int
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
if luna is None or an is None:
|
||||
try:
|
||||
current_period = await DashboardService.get_current_period(company)
|
||||
current_period = await DashboardService.get_current_period(company, server_id=server_id)
|
||||
resolved_luna = luna if luna is not None else current_period.get('luna', 12)
|
||||
resolved_an = an if an is not None else current_period.get('an', 2024)
|
||||
except Exception as e:
|
||||
@@ -519,13 +536,22 @@ async def get_financial_indicators(
|
||||
# Dacă include_sparklines este True, folosim metoda care include datele istorice
|
||||
if include_sparklines:
|
||||
response = await FinancialIndicatorsService.get_indicators_with_sparklines(
|
||||
company, resolved_luna, resolved_an, months=12, request=request
|
||||
company, resolved_luna, resolved_an, months=12, request=request, server_id=server_id
|
||||
)
|
||||
|
||||
# FIX: Cache poate returna dict în loc de obiect Pydantic
|
||||
# Extragem valorile pentru logging în mod compatibil cu ambele tipuri
|
||||
if isinstance(response, dict):
|
||||
zscore_val = response.get('altman_zscore', {}).get('zscore', {}).get('value')
|
||||
zscore_status = response.get('altman_zscore', {}).get('zscore', {}).get('status')
|
||||
else:
|
||||
zscore_val = response.altman_zscore.zscore.value
|
||||
zscore_status = response.altman_zscore.zscore.status
|
||||
|
||||
logger.info(
|
||||
f"Financial indicators with sparklines for company {company}, "
|
||||
f"luna={resolved_luna}, an={resolved_an}: "
|
||||
f"Z-Score={response.altman_zscore.zscore.value} ({response.altman_zscore.zscore.status}), "
|
||||
f"Z-Score={zscore_val} ({zscore_status}), "
|
||||
f"cache_hit={getattr(request.state, 'cache_hit', False)}, "
|
||||
f"response_time={getattr(request.state, 'response_time_ms', 0):.1f}ms"
|
||||
)
|
||||
@@ -545,28 +571,28 @@ async def get_financial_indicators(
|
||||
|
||||
# Apelăm serviciul pentru fiecare categorie de indicatori
|
||||
lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
|
||||
company, resolved_luna, resolved_an
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
|
||||
company, resolved_luna, resolved_an
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
risc_task = FinancialIndicatorsService.calculate_risk_indicators(
|
||||
company, resolved_luna, resolved_an
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
|
||||
company, resolved_luna, resolved_an
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
|
||||
company, resolved_luna, resolved_an
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
|
||||
company, resolved_luna, resolved_an
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
|
||||
company, resolved_luna, resolved_an
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
solvabilitate_task = FinancialIndicatorsService.calculate_solvability_indicators(
|
||||
company, resolved_luna, resolved_an
|
||||
company, resolved_luna, resolved_an, server_id=server_id
|
||||
)
|
||||
|
||||
# Executăm toate calculele în paralel pentru performanță
|
||||
@@ -602,9 +628,17 @@ async def get_financial_indicators(
|
||||
solvabilitate=solvabilitate
|
||||
)
|
||||
|
||||
# FIX: Cache poate returna dict în loc de obiect Pydantic
|
||||
if isinstance(altman_zscore, dict):
|
||||
zscore_val = altman_zscore.get('zscore', {}).get('value')
|
||||
zscore_status = altman_zscore.get('zscore', {}).get('status')
|
||||
else:
|
||||
zscore_val = altman_zscore.zscore.value
|
||||
zscore_status = altman_zscore.zscore.status
|
||||
|
||||
logger.info(
|
||||
f"Financial indicators for company {company}, luna={resolved_luna}, an={resolved_an}: "
|
||||
f"Z-Score={altman_zscore.zscore.value} ({altman_zscore.zscore.status})"
|
||||
f"Z-Score={zscore_val} ({zscore_status})"
|
||||
)
|
||||
|
||||
# Add cache metadata if requested (for Telegram Bot / Dashboard)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
API Router pentru facturi
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from typing import List, Optional
|
||||
from datetime import date
|
||||
# import sys # Removed - no longer needed
|
||||
@@ -16,6 +16,7 @@ router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=InvoiceListResponse)
|
||||
async def get_invoices(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
@@ -41,6 +42,8 @@ async def get_invoices(
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
filter_params = InvoiceFilter(
|
||||
company=company,
|
||||
partner_type=partner_type,
|
||||
@@ -55,7 +58,7 @@ async def get_invoices(
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
result = await InvoiceService.get_invoices(filter_params, current_user.username)
|
||||
result = await InvoiceService.get_invoices(filter_params, current_user.username, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
@@ -65,6 +68,7 @@ async def get_invoices(
|
||||
|
||||
@router.get("/summary", response_model=InvoiceSummary)
|
||||
async def get_invoices_summary(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
@@ -75,7 +79,9 @@ async def get_invoices_summary(
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await InvoiceService.get_invoice_summary(company, partner_type, current_user.username)
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
result = await InvoiceService.get_invoice_summary(company, partner_type, current_user.username, server_id=server_id)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
@@ -83,6 +89,7 @@ async def get_invoices_summary(
|
||||
|
||||
@router.get("/{invoice_number}")
|
||||
async def get_invoice_details(
|
||||
request: Request,
|
||||
invoice_number: str,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
@@ -92,8 +99,10 @@ async def get_invoice_details(
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await InvoiceService.get_invoice_details(company, invoice_number, current_user.username)
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
result = await InvoiceService.get_invoice_details(company, invoice_number, current_user.username, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
@@ -103,6 +112,7 @@ async def get_invoice_details(
|
||||
|
||||
@router.get("/export/{format}")
|
||||
async def export_invoices(
|
||||
request: Request,
|
||||
format: str,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
@@ -119,6 +129,8 @@ async def export_invoices(
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None) # For future use
|
||||
|
||||
# Verifică formatul
|
||||
if format not in ["excel", "pdf", "csv"]:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
# import sys # Removed - no longer needed
|
||||
@@ -13,6 +13,7 @@ router = APIRouter()
|
||||
|
||||
@router.get("/bank-cash-register", response_model=RegisterListResponse)
|
||||
async def get_bank_cash_register(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
register_type: Optional[str] = Query(None, description="Tipul registrului: BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA sau None pentru toate"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
|
||||
@@ -37,6 +38,8 @@ async def get_bank_cash_register(
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
# Validează register_type dacă e specificat
|
||||
valid_types = ['BANCA_LEI', 'BANCA_VALUTA', 'CASA_LEI', 'CASA_VALUTA']
|
||||
if register_type and register_type not in valid_types:
|
||||
@@ -74,7 +77,7 @@ async def get_bank_cash_register(
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
result = await TreasuryService.get_bank_cash_register(filter_params, current_user.username)
|
||||
result = await TreasuryService.get_bank_cash_register(filter_params, current_user.username, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
@@ -85,6 +88,7 @@ async def get_bank_cash_register(
|
||||
|
||||
@router.get("/bank-cash-accounts", response_model=List[str])
|
||||
async def get_bank_cash_accounts(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
register_type: str = Query(description="Tipul registrului: BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
@@ -100,6 +104,8 @@ async def get_bank_cash_accounts(
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
# Validează register_type
|
||||
valid_types = ['BANCA_LEI', 'BANCA_VALUTA', 'CASA_LEI', 'CASA_VALUTA']
|
||||
if register_type not in valid_types:
|
||||
@@ -108,7 +114,7 @@ async def get_bank_cash_accounts(
|
||||
detail=f"Tip registru invalid. Valori acceptate: {', '.join(valid_types)}"
|
||||
)
|
||||
|
||||
result = await TreasuryService.get_bank_cash_accounts(int(company), register_type)
|
||||
result = await TreasuryService.get_bank_cash_accounts(int(company), register_type, server_id=server_id)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
API Router for Trial Balance (Balanță de Verificare)
|
||||
Refactored to use service layer with caching
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from typing import Optional
|
||||
from datetime import date
|
||||
# import sys # Removed - no longer needed
|
||||
@@ -20,6 +20,7 @@ router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=TrialBalanceResponse)
|
||||
async def get_trial_balance(
|
||||
request: Request,
|
||||
company: str = Query(description="Codul firmei (ID)"),
|
||||
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna (1-12), default: luna curentă"),
|
||||
an: Optional[int] = Query(None, ge=2000, le=2100, description="An, default: anul curent"),
|
||||
@@ -48,6 +49,8 @@ async def get_trial_balance(
|
||||
detail=f"Nu aveți acces la firma {company}"
|
||||
)
|
||||
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
# Setează valorile implicite pentru lună și an (luna și anul curent)
|
||||
current_date = date.today()
|
||||
if luna is None:
|
||||
@@ -69,7 +72,8 @@ async def get_trial_balance(
|
||||
sort_order=sort_order,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
username=current_user.username
|
||||
username=current_user.username,
|
||||
server_id=server_id
|
||||
)
|
||||
|
||||
return TrialBalanceResponse(
|
||||
|
||||
@@ -3,6 +3,7 @@ Calendar service for fetching available accounting periods
|
||||
"""
|
||||
# import sys # Removed - no longer needed
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
from ..models.calendar import CalendarPeriod, CalendarPeriodsResponse
|
||||
@@ -22,10 +23,10 @@ class CalendarService:
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='schema', key_params=['company_id'])
|
||||
async def _get_schema(company_id: int) -> str:
|
||||
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
|
||||
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
|
||||
"""Get schema for company (CACHED 24h)"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme
|
||||
@@ -35,19 +36,19 @@ class CalendarService:
|
||||
return result[0] if result else None
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='calendar_periods', key_params=['company_id'])
|
||||
async def get_available_periods(company_id: int) -> CalendarPeriodsResponse:
|
||||
@cached(cache_type='calendar_periods', key_params=['company_id', 'server_id'])
|
||||
async def get_available_periods(company_id: int, server_id: Optional[str] = None) -> CalendarPeriodsResponse:
|
||||
"""
|
||||
Get all available accounting periods for a company (CACHED 1h)
|
||||
|
||||
Returns periods ordered by year DESC, month DESC with Romanian month names.
|
||||
"""
|
||||
schema = await CalendarService._get_schema(company_id)
|
||||
schema = await CalendarService._get_schema(company_id, server_id)
|
||||
if not schema:
|
||||
logger.warning(f"Schema not found for company {company_id}")
|
||||
return CalendarPeriodsResponse(periods=[], current_period=None, total_count=0)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"""
|
||||
SELECT anul, luna
|
||||
|
||||
@@ -44,15 +44,15 @@ class DashboardService:
|
||||
return cte_sql, params
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='schema', key_params=['company_id'])
|
||||
async def _get_schema(company_id: int) -> str:
|
||||
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
|
||||
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Obține schema pentru company_id (CACHED PERMANENT)
|
||||
|
||||
CRITICAL: Acest query este cel mai frecvent - executat la FIECARE request API.
|
||||
Cache permanent reduce queries cu 99.99%.
|
||||
"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
schema_query = """
|
||||
SELECT schema
|
||||
@@ -68,8 +68,8 @@ class DashboardService:
|
||||
return schema_result[0]
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='dashboard_summary', key_params=['company', 'username', 'luna', 'an'])
|
||||
async def get_complete_summary(company: str, username: str, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> DashboardSummary:
|
||||
@cached(cache_type='dashboard_summary', key_params=['company', 'username', 'luna', 'an', 'server_id'])
|
||||
async def get_complete_summary(company: str, username: str, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> DashboardSummary:
|
||||
"""
|
||||
Obține toate datele pentru dashboard într-un singur apel (CACHED 30 min)
|
||||
Execută 2 query-uri separate: facturi și trezorerie
|
||||
@@ -80,14 +80,15 @@ class DashboardService:
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
request: Request object pentru cache metadata
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
company_id = int(company)
|
||||
schema = await DashboardService._get_schema(company_id)
|
||||
schema = await DashboardService._get_schema(company_id, server_id)
|
||||
|
||||
# Construiește CTE pentru perioada curentă
|
||||
period_cte, period_params = DashboardService._build_period_cte(schema, luna, an)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
# Query 1: Statistici facturi cu breakdown pe perioade - FIXED ORA-00937
|
||||
@@ -565,8 +566,8 @@ class DashboardService:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='dashboard_trends', key_params=['company_id', 'period', 'luna', 'an'])
|
||||
async def get_trends(company_id: int, period: str = "12m", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='dashboard_trends', key_params=['company_id', 'period', 'luna', 'an', 'server_id'])
|
||||
async def get_trends(company_id: int, period: str = "12m", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get comprehensive trend analysis data for all dashboard indicators (CACHED 30 min)
|
||||
|
||||
Args:
|
||||
@@ -575,11 +576,12 @@ class DashboardService:
|
||||
luna: Luna contabilă (1-12), opțional - dacă nu e specificată, folosește MAX
|
||||
an: Anul contabil, opțional - dacă nu e specificat, folosește MAX
|
||||
request: Request object pentru cache metadata
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
try:
|
||||
schema = await DashboardService._get_schema(company_id)
|
||||
schema = await DashboardService._get_schema(company_id, server_id)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
# Determine current period from params or database
|
||||
@@ -962,7 +964,7 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
async def get_detailed_data(company: str, data_type: str, luna: Optional[int] = None, an: Optional[int] = None, page: int = 1, page_size: int = 25, search: str = ""):
|
||||
async def get_detailed_data(company: str, data_type: str, luna: Optional[int] = None, an: Optional[int] = None, page: int = 1, page_size: int = 25, search: str = "", server_id: Optional[str] = None):
|
||||
"""
|
||||
Obține date detaliate pentru tabelele din dashboard
|
||||
Fixed to use existing vireg_parteneri view instead of missing tables
|
||||
@@ -975,9 +977,10 @@ class DashboardService:
|
||||
page: Pagina curentă
|
||||
page_size: Mărimea paginii
|
||||
search: Termen de căutare
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
logger.info(f"get_detailed_data called: company={company}, data_type={data_type}, luna={luna}, an={an}, page={page}")
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
# Get schema for company
|
||||
@@ -1168,14 +1171,15 @@ class DashboardService:
|
||||
return {"error": f"Database error: {str(e)}", "data": [], "total": 0}
|
||||
|
||||
@staticmethod
|
||||
async def get_performance_data(company_id: int, period: str = "7d") -> Dict[str, Any]:
|
||||
async def get_performance_data(company_id: int, period: str = "7d", server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculează performanța încasări/plăți pentru perioada selectată
|
||||
|
||||
|
||||
Args:
|
||||
company_id: ID-ul companiei
|
||||
period: Perioada ("7d", "1m", "3m", "6m", "ytd", "12m")
|
||||
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
|
||||
Returns:
|
||||
{
|
||||
labels: List[str] - etichete pentru perioadele de timp
|
||||
@@ -1190,7 +1194,7 @@ class DashboardService:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Get schema
|
||||
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
|
||||
@@ -1262,14 +1266,15 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
async def get_cashflow_forecast(company_id: int, period: str = "7d") -> Dict[str, Any]:
|
||||
async def get_cashflow_forecast(company_id: int, period: str = "7d", server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculează previziunea cash flow bazată pe scadențe
|
||||
|
||||
|
||||
Args:
|
||||
company_id: ID-ul companiei
|
||||
period: Perioada ("7d", "1m", "3m", "6m")
|
||||
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
|
||||
Returns:
|
||||
{
|
||||
periods: List[str] - perioadele de timp
|
||||
@@ -1282,7 +1287,7 @@ class DashboardService:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Get schema
|
||||
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
|
||||
@@ -1347,8 +1352,8 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='maturity_analysis', key_params=['company_id', 'period', 'luna', 'an'])
|
||||
async def get_maturity_analysis(company_id: int, period: str = "7d", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='maturity_analysis', key_params=['company_id', 'period', 'luna', 'an', 'server_id'])
|
||||
async def get_maturity_analysis(company_id: int, period: str = "7d", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Analizează scadențele clienți vs furnizori cu date reale din Oracle (CACHED 30 min)
|
||||
|
||||
@@ -1357,6 +1362,7 @@ class DashboardService:
|
||||
period: Perioada ("7d", "1m", "3m", "6m", "12m", "over12m")
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
|
||||
Returns:
|
||||
{
|
||||
@@ -1367,7 +1373,7 @@ class DashboardService:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Get schema
|
||||
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
|
||||
@@ -1546,8 +1552,8 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='monthly_flows', key_params=['company', 'luna', 'an'])
|
||||
async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='monthly_flows', key_params=['company', 'luna', 'an', 'server_id'])
|
||||
async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Obține fluxurile lunare de intrare și ieșire pentru luna curentă (CACHED 30 min)
|
||||
|
||||
@@ -1556,9 +1562,10 @@ class DashboardService:
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
request: Request object pentru cache metadata
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema
|
||||
company_id = company
|
||||
@@ -1640,8 +1647,8 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='treasury_breakdown', key_params=['company', 'luna', 'an'])
|
||||
async def get_treasury_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='treasury_breakdown', key_params=['company', 'luna', 'an', 'server_id'])
|
||||
async def get_treasury_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Obține breakdown-ul trezoreriei pe casă și bancă (CACHED 30 min)
|
||||
|
||||
@@ -1649,9 +1656,10 @@ class DashboardService:
|
||||
company: ID-ul firmei
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema
|
||||
company_id = company
|
||||
@@ -1745,8 +1753,8 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='net_balance_breakdown', key_params=['company', 'luna', 'an'])
|
||||
async def get_net_balance_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='net_balance_breakdown', key_params=['company', 'luna', 'an', 'server_id'])
|
||||
async def get_net_balance_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Obține breakdown-ul balanței nete pe clienți și furnizori cu detaliere pe perioade (CACHED 30 min)
|
||||
|
||||
@@ -1754,9 +1762,10 @@ class DashboardService:
|
||||
company: ID-ul firmei
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema
|
||||
company_id = company
|
||||
@@ -1938,12 +1947,13 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
async def get_current_period(company: int) -> Dict[str, Any]:
|
||||
async def get_current_period(company: int, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Obține perioada curentă (an și lună) din calendarul Oracle
|
||||
|
||||
Args:
|
||||
company: ID-ul companiei
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
|
||||
Returns:
|
||||
{
|
||||
@@ -1953,7 +1963,7 @@ class DashboardService:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema
|
||||
company_id = company
|
||||
|
||||
@@ -278,14 +278,14 @@ class FinancialIndicatorsService:
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='schema', key_params=['company_id'])
|
||||
async def _get_schema(company_id: int) -> str:
|
||||
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
|
||||
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Obține schema pentru company_id (CACHED PERMANENT)
|
||||
|
||||
Schema este stocată permanent în cache deoarece nu se schimbă.
|
||||
"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
schema_query = """
|
||||
SELECT schema
|
||||
@@ -319,11 +319,12 @@ class FinancialIndicatorsService:
|
||||
return f"SUM(CASE WHEN ({conditions}) THEN NVL({column}, 0) ELSE 0 END)"
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='fin_balance_sheet', key_params=['company_id', 'luna', 'an'])
|
||||
@cached(cache_type='fin_balance_sheet', key_params=['company_id', 'luna', 'an', 'server_id'])
|
||||
async def get_balance_sheet_aggregates(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> BalanceSheetAggregates:
|
||||
"""
|
||||
Obține soldurile agregate din balanța de verificare pentru calculul
|
||||
@@ -343,9 +344,9 @@ class FinancialIndicatorsService:
|
||||
Raises:
|
||||
ValueError: Dacă schema nu este găsită pentru firma specificată
|
||||
"""
|
||||
schema = await FinancialIndicatorsService._get_schema(company_id)
|
||||
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Construim query-ul cu CASE pentru fiecare categorie
|
||||
# Soldurile din VBAL: SOLDDEB (sold debitor), SOLDCRED (sold creditor)
|
||||
@@ -546,11 +547,12 @@ class FinancialIndicatorsService:
|
||||
return aggregates
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='fin_achizitii', key_params=['company_id', 'luna', 'an'])
|
||||
@cached(cache_type='fin_achizitii', key_params=['company_id', 'luna', 'an', 'server_id'])
|
||||
async def get_achizitii_ytd(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> Decimal:
|
||||
"""
|
||||
Calculează totalul achizițiilor YTD din Registrul Jurnal (ACT).
|
||||
@@ -575,9 +577,9 @@ class FinancialIndicatorsService:
|
||||
Returns:
|
||||
Total achiziții YTD fără TVA (Decimal)
|
||||
"""
|
||||
schema = await FinancialIndicatorsService._get_schema(company_id)
|
||||
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
query = f"""
|
||||
SELECT
|
||||
@@ -611,11 +613,12 @@ class FinancialIndicatorsService:
|
||||
return achizitii_total
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='fin_cashflow_vbal', key_params=['company_id', 'luna', 'an'])
|
||||
@cached(cache_type='fin_cashflow_vbal', key_params=['company_id', 'luna', 'an', 'server_id'])
|
||||
async def get_cashflow_from_vbal(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> dict:
|
||||
"""
|
||||
Calculează datele de Cash Flow direct din VBAL (balanța de verificare).
|
||||
@@ -642,9 +645,9 @@ class FinancialIndicatorsService:
|
||||
- incasari_ytd: Încasări YTD (4111+461 TOTCRED)
|
||||
- plati_ytd: Plăți YTD (401+404+462 TOTDEB)
|
||||
"""
|
||||
schema = await FinancialIndicatorsService._get_schema(company_id)
|
||||
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
query = f"""
|
||||
SELECT
|
||||
@@ -737,7 +740,8 @@ class FinancialIndicatorsService:
|
||||
async def calculate_liquidity_indicators(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> LiquidityIndicators:
|
||||
"""
|
||||
Calculează indicatorii de lichiditate pentru evaluarea capacității
|
||||
@@ -763,7 +767,7 @@ class FinancialIndicatorsService:
|
||||
"""
|
||||
# Obținem agregatele din balanță
|
||||
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
|
||||
if isinstance(aggregates, dict):
|
||||
@@ -906,11 +910,12 @@ class FinancialIndicatorsService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='fin_efficiency', key_params=['company_id', 'luna', 'an'])
|
||||
@cached(cache_type='fin_efficiency', key_params=['company_id', 'luna', 'an', 'server_id'])
|
||||
async def calculate_efficiency_indicators(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> EfficiencyIndicators:
|
||||
"""
|
||||
Calculează indicatorii de eficiență pentru evaluarea vitezei de conversie
|
||||
@@ -944,7 +949,8 @@ class FinancialIndicatorsService:
|
||||
company=str(company_id),
|
||||
username="system", # System call for indicators
|
||||
luna=luna,
|
||||
an=an
|
||||
an=an,
|
||||
server_id=server_id
|
||||
)
|
||||
# Ensure summary is a DashboardSummary model (cache may return dict)
|
||||
if isinstance(summary, dict):
|
||||
@@ -953,7 +959,8 @@ class FinancialIndicatorsService:
|
||||
# Obținem datele din trends (facturări/încasări/achiziții/plăți lunare)
|
||||
trends = await DashboardService.get_trends(
|
||||
company_id=company_id,
|
||||
period='12m' # Ultimele 12 luni pentru media lunară
|
||||
period='12m', # Ultimele 12 luni pentru media lunară
|
||||
server_id=server_id
|
||||
)
|
||||
|
||||
# Extragem soldurile din summary
|
||||
@@ -1162,11 +1169,12 @@ class FinancialIndicatorsService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='fin_risk', key_params=['company_id', 'luna', 'an'])
|
||||
@cached(cache_type='fin_risk', key_params=['company_id', 'luna', 'an', 'server_id'])
|
||||
async def calculate_risk_indicators(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> RiskIndicators:
|
||||
"""
|
||||
Calculează indicatorii de risc și aging pentru evaluarea sănătății
|
||||
@@ -1205,7 +1213,8 @@ class FinancialIndicatorsService:
|
||||
company=str(company_id),
|
||||
username="system", # System call for indicators
|
||||
luna=luna,
|
||||
an=an
|
||||
an=an,
|
||||
server_id=server_id
|
||||
)
|
||||
# Ensure summary is a DashboardSummary model (cache may return dict)
|
||||
if isinstance(summary, dict):
|
||||
@@ -1384,11 +1393,12 @@ class FinancialIndicatorsService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='fin_cashflow', key_params=['company_id', 'luna', 'an'])
|
||||
@cached(cache_type='fin_cashflow', key_params=['company_id', 'luna', 'an', 'server_id'])
|
||||
async def calculate_cashflow_indicators(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> CashFlowIndicators:
|
||||
"""
|
||||
Calculează indicatorii de cash flow pentru evaluarea generării și
|
||||
@@ -1421,10 +1431,10 @@ class FinancialIndicatorsService:
|
||||
# Obținem datele de cash flow din VBAL (sursa preferată)
|
||||
# VBAL oferă date directe: RULCRED/RULDEB pentru lunar, TOTCRED/TOTDEB pentru YTD
|
||||
cf_data_curent = await FinancialIndicatorsService.get_cashflow_from_vbal(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
cf_data_anterior = await FinancialIndicatorsService.get_cashflow_from_vbal(
|
||||
company_id, luna, an - 1
|
||||
company_id, luna, an - 1, server_id
|
||||
)
|
||||
|
||||
# Obținem datele din summary pentru datorii restante
|
||||
@@ -1432,7 +1442,8 @@ class FinancialIndicatorsService:
|
||||
company=str(company_id),
|
||||
username="system",
|
||||
luna=luna,
|
||||
an=an
|
||||
an=an,
|
||||
server_id=server_id
|
||||
)
|
||||
# Ensure summary is a DashboardSummary model (cache may return dict)
|
||||
if isinstance(summary, dict):
|
||||
@@ -1609,11 +1620,12 @@ class FinancialIndicatorsService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='fin_dynamics', key_params=['company_id', 'luna', 'an'])
|
||||
@cached(cache_type='fin_dynamics', key_params=['company_id', 'luna', 'an', 'server_id'])
|
||||
async def calculate_dynamics_indicators(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> DynamicsIndicators:
|
||||
"""
|
||||
Calculează indicatorii de dinamică pentru evaluarea evoluției afacerii.
|
||||
@@ -1653,10 +1665,10 @@ class FinancialIndicatorsService:
|
||||
# Obținem agregatele pentru anul curent și anul anterior
|
||||
# Cifra de Afaceri (70x) din VBAL - FĂRĂ TVA
|
||||
aggregates_curent = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
aggregates_anterior = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||||
company_id, luna, an - 1
|
||||
company_id, luna, an - 1, server_id
|
||||
)
|
||||
|
||||
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
|
||||
@@ -1674,10 +1686,10 @@ class FinancialIndicatorsService:
|
||||
# Include: 3x=4x (stocuri) + 6x=4x (servicii, consumabile)
|
||||
# Exclude: discount/rabat (40x=667/609)
|
||||
achizitii_curent = await FinancialIndicatorsService.get_achizitii_ytd(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
achizitii_anterior = await FinancialIndicatorsService.get_achizitii_ytd(
|
||||
company_id, luna, an - 1
|
||||
company_id, luna, an - 1, server_id
|
||||
)
|
||||
total_achizitii_curent = float(achizitii_curent)
|
||||
total_achizitii_anterior = float(achizitii_anterior)
|
||||
@@ -1843,11 +1855,12 @@ class FinancialIndicatorsService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='fin_altman', key_params=['company_id', 'luna', 'an'])
|
||||
@cached(cache_type='fin_altman', key_params=['company_id', 'luna', 'an', 'server_id'])
|
||||
async def calculate_altman_zscore(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> AltmanZScore:
|
||||
"""
|
||||
Calculează Altman Z-Score pentru evaluarea riscului de faliment.
|
||||
@@ -1880,7 +1893,7 @@ class FinancialIndicatorsService:
|
||||
"""
|
||||
# Obținem agregatele din balanță
|
||||
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
|
||||
if isinstance(aggregates, dict):
|
||||
@@ -2088,11 +2101,12 @@ class FinancialIndicatorsService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='fin_profitability', key_params=['company_id', 'luna', 'an'])
|
||||
@cached(cache_type='fin_profitability', key_params=['company_id', 'luna', 'an', 'server_id'])
|
||||
async def calculate_profitability_indicators(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> ProfitabilityIndicators:
|
||||
"""
|
||||
Calculează indicatorii de profitabilitate pentru evaluarea randamentului afacerii.
|
||||
@@ -2120,7 +2134,7 @@ class FinancialIndicatorsService:
|
||||
"""
|
||||
# Obținem agregatele din balanță (include venituri, cheltuieli, active, capital)
|
||||
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
|
||||
if isinstance(aggregates, dict):
|
||||
@@ -2356,7 +2370,8 @@ class FinancialIndicatorsService:
|
||||
async def calculate_solvability_indicators(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int
|
||||
an: int,
|
||||
server_id: Optional[str] = None
|
||||
) -> SolvabilityIndicators:
|
||||
"""
|
||||
Calculează indicatorii de solvabilitate pentru evaluarea capacității
|
||||
@@ -2384,7 +2399,7 @@ class FinancialIndicatorsService:
|
||||
"""
|
||||
# Obținem agregatele din balanță
|
||||
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
|
||||
if isinstance(aggregates, dict):
|
||||
@@ -2555,12 +2570,13 @@ class FinancialIndicatorsService:
|
||||
return periods
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='financial_indicators_historical', ttl=3600, key_params=['company_id', 'months', 'luna', 'an'])
|
||||
@cached(cache_type='financial_indicators_historical', ttl=3600, key_params=['company_id', 'months', 'luna', 'an', 'server_id'])
|
||||
async def get_historical_indicators(
|
||||
company_id: int,
|
||||
months: int = 12,
|
||||
luna: Optional[int] = None,
|
||||
an: Optional[int] = None
|
||||
an: Optional[int] = None,
|
||||
server_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculează indicatorii financiari pentru ultimele `months` luni
|
||||
@@ -2672,7 +2688,7 @@ class FinancialIndicatorsService:
|
||||
try:
|
||||
# Lichiditate
|
||||
lichiditate = await FinancialIndicatorsService.calculate_liquidity_indicators(
|
||||
company_id, period_luna, period_an
|
||||
company_id, period_luna, period_an, server_id
|
||||
)
|
||||
# Ensure lichiditate is a model (cache may return dict)
|
||||
if isinstance(lichiditate, dict):
|
||||
@@ -2690,7 +2706,7 @@ class FinancialIndicatorsService:
|
||||
|
||||
# Eficiență
|
||||
eficienta = await FinancialIndicatorsService.calculate_efficiency_indicators(
|
||||
company_id, period_luna, period_an
|
||||
company_id, period_luna, period_an, server_id
|
||||
)
|
||||
# Ensure eficienta is a model (cache may return dict)
|
||||
if isinstance(eficienta, dict):
|
||||
@@ -2706,7 +2722,7 @@ class FinancialIndicatorsService:
|
||||
|
||||
# Risc
|
||||
risc = await FinancialIndicatorsService.calculate_risk_indicators(
|
||||
company_id, period_luna, period_an
|
||||
company_id, period_luna, period_an, server_id
|
||||
)
|
||||
# Ensure risc is a model (cache may return dict)
|
||||
if isinstance(risc, dict):
|
||||
@@ -2725,7 +2741,7 @@ class FinancialIndicatorsService:
|
||||
|
||||
# Cash Flow
|
||||
cash_flow = await FinancialIndicatorsService.calculate_cashflow_indicators(
|
||||
company_id, period_luna, period_an
|
||||
company_id, period_luna, period_an, server_id
|
||||
)
|
||||
# Ensure cash_flow is a model (cache may return dict)
|
||||
if isinstance(cash_flow, dict):
|
||||
@@ -2742,7 +2758,7 @@ class FinancialIndicatorsService:
|
||||
|
||||
# Dinamica
|
||||
dinamica = await FinancialIndicatorsService.calculate_dynamics_indicators(
|
||||
company_id, period_luna, period_an
|
||||
company_id, period_luna, period_an, server_id
|
||||
)
|
||||
# Ensure dinamica is a model (cache may return dict)
|
||||
if isinstance(dinamica, dict):
|
||||
@@ -2758,7 +2774,7 @@ class FinancialIndicatorsService:
|
||||
|
||||
# Altman Z-Score
|
||||
altman = await FinancialIndicatorsService.calculate_altman_zscore(
|
||||
company_id, period_luna, period_an
|
||||
company_id, period_luna, period_an, server_id
|
||||
)
|
||||
# Ensure altman is a model (cache may return dict)
|
||||
if isinstance(altman, dict):
|
||||
@@ -2772,7 +2788,7 @@ class FinancialIndicatorsService:
|
||||
|
||||
# Profitabilitate
|
||||
profitabilitate = await FinancialIndicatorsService.calculate_profitability_indicators(
|
||||
company_id, period_luna, period_an
|
||||
company_id, period_luna, period_an, server_id
|
||||
)
|
||||
# Ensure profitabilitate is a model (cache may return dict)
|
||||
if isinstance(profitabilitate, dict):
|
||||
@@ -2795,7 +2811,7 @@ class FinancialIndicatorsService:
|
||||
|
||||
# Solvabilitate
|
||||
solvabilitate = await FinancialIndicatorsService.calculate_solvability_indicators(
|
||||
company_id, period_luna, period_an
|
||||
company_id, period_luna, period_an, server_id
|
||||
)
|
||||
# Ensure solvabilitate is a model (cache may return dict)
|
||||
if isinstance(solvabilitate, dict):
|
||||
@@ -2829,13 +2845,14 @@ class FinancialIndicatorsService:
|
||||
return historical_data
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='financial_indicators_sparklines', key_params=['company_id', 'luna', 'an', 'months'])
|
||||
@cached(cache_type='financial_indicators_sparklines', key_params=['company_id', 'luna', 'an', 'months', 'server_id'])
|
||||
async def get_indicators_with_sparklines(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
an: int,
|
||||
months: int = 12,
|
||||
request: Optional[Request] = None
|
||||
request: Optional[Request] = None,
|
||||
server_id: Optional[str] = None
|
||||
) -> FinancialIndicatorsResponse:
|
||||
"""
|
||||
Calculează toți indicatorii financiari și adaugă datele de sparkline
|
||||
@@ -2858,32 +2875,32 @@ class FinancialIndicatorsService:
|
||||
|
||||
# Obținem datele istorice și indicatorii curenți în paralel
|
||||
historical_task = FinancialIndicatorsService.get_historical_indicators(
|
||||
company_id, months, luna, an
|
||||
company_id, months, luna, an, server_id
|
||||
)
|
||||
|
||||
lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
risc_task = FinancialIndicatorsService.calculate_risk_indicators(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
solvabilitate_task = FinancialIndicatorsService.calculate_solvability_indicators(
|
||||
company_id, luna, an
|
||||
company_id, luna, an, server_id
|
||||
)
|
||||
|
||||
(
|
||||
|
||||
@@ -5,7 +5,7 @@ Service pentru logica facturi - Portează query-urile din aplicația Flask
|
||||
import os
|
||||
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Optional
|
||||
from ..models.invoice import Invoice, InvoiceFilter, InvoiceListResponse, InvoiceSummary
|
||||
from ..cache.decorators import cached
|
||||
from decimal import Decimal
|
||||
@@ -17,10 +17,10 @@ class InvoiceService:
|
||||
"""Service pentru gestionarea facturilor"""
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='schema', key_params=['company_id'])
|
||||
async def _get_schema(company_id: int) -> str:
|
||||
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
|
||||
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
|
||||
"""Obține schema pentru company_id (CACHED PERMANENT)"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
schema_query = """
|
||||
SELECT schema
|
||||
@@ -36,15 +36,15 @@ class InvoiceService:
|
||||
return schema_result[0]
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='invoices', key_params=['filter_params', 'username'])
|
||||
async def get_invoices(filter_params: InvoiceFilter, username: str) -> InvoiceListResponse:
|
||||
@cached(cache_type='invoices', key_params=['filter_params', 'username', 'server_id'])
|
||||
async def get_invoices(filter_params: InvoiceFilter, username: str, server_id: Optional[str] = None) -> InvoiceListResponse:
|
||||
"""
|
||||
Obține lista de facturi - Query simplu pentru afișare în tabel (CACHED 10 min)
|
||||
"""
|
||||
company_id = int(filter_params.company)
|
||||
schema = await InvoiceService._get_schema(company_id)
|
||||
schema = await InvoiceService._get_schema(company_id, server_id)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
# Determină conturile în funcție de partner_type
|
||||
@@ -240,11 +240,11 @@ class InvoiceService:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def get_invoice_details(company: str, invoice_number: str, username: str) -> Invoice:
|
||||
async def get_invoice_details(company: str, invoice_number: str, username: str, server_id: Optional[str] = None) -> Invoice:
|
||||
"""
|
||||
Obține detaliile unei facturi specifice
|
||||
"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema din v_nom_firme bazat pe id_firma
|
||||
company_id = int(company)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# import sys # Removed - no longer needed
|
||||
import os
|
||||
from typing import Optional, List, Tuple, Any
|
||||
|
||||
import oracledb
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
from ..models.treasury import BankCashRegister, RegisterFilter, RegisterListResponse, AccountingPeriod
|
||||
from ..cache.decorators import cached
|
||||
from decimal import Decimal
|
||||
from typing import Optional, List, Tuple, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -15,10 +15,10 @@ class TreasuryService:
|
||||
"""Service pentru trezorerie - registru casă și bancă"""
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='schema', key_params=['company_id'])
|
||||
async def _get_schema(company_id: int) -> str:
|
||||
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
|
||||
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
|
||||
"""Obține schema pentru company_id (CACHED PERMANENT)"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
schema_query = """
|
||||
SELECT schema
|
||||
@@ -99,8 +99,8 @@ class TreasuryService:
|
||||
return " UNION ALL ".join(queries)
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='treasury', key_params=['filter_params', 'username'])
|
||||
async def get_bank_cash_register(filter_params: RegisterFilter, username: str) -> RegisterListResponse:
|
||||
@cached(cache_type='treasury', key_params=['filter_params', 'username', 'server_id'])
|
||||
async def get_bank_cash_register(filter_params: RegisterFilter, username: str, server_id: Optional[str] = None) -> RegisterListResponse:
|
||||
"""
|
||||
Obține registrul de casă și bancă din vbancasa views (CACHED 10 min)
|
||||
|
||||
@@ -114,9 +114,9 @@ class TreasuryService:
|
||||
Toate în aceeași tranzacție!
|
||||
"""
|
||||
company_id = int(filter_params.company)
|
||||
schema = await TreasuryService._get_schema(company_id)
|
||||
schema = await TreasuryService._get_schema(company_id, server_id)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
# Construiește query-ul pentru tipul de registru selectat
|
||||
@@ -350,14 +350,14 @@ class TreasuryService:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='treasury', key_params=['company_id', 'register_type'])
|
||||
async def get_bank_cash_accounts(company_id: int, register_type: str) -> List[str]:
|
||||
@cached(cache_type='treasury', key_params=['company_id', 'register_type', 'server_id'])
|
||||
async def get_bank_cash_accounts(company_id: int, register_type: str, server_id: Optional[str] = None) -> List[str]:
|
||||
"""
|
||||
Obține lista distinctă de conturi bancă/casă (bancasa) pentru dropdown.
|
||||
Cached pentru performanță.
|
||||
IMPORTANT: Trebuie să setăm contextul PACK_SESIUNE înainte de a accesa vbancasa views!
|
||||
"""
|
||||
schema = await TreasuryService._get_schema(company_id)
|
||||
schema = await TreasuryService._get_schema(company_id, server_id)
|
||||
|
||||
# Map register_type to view
|
||||
view_map = {
|
||||
@@ -372,7 +372,7 @@ class TreasuryService:
|
||||
|
||||
view_name = view_map[register_type]
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# PL/SQL block to set session context and get accounts
|
||||
plsql_block = f"""
|
||||
|
||||
@@ -4,9 +4,9 @@ Refactored to use caching system for optimal performance
|
||||
"""
|
||||
# import sys # Removed - no longer needed
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
from typing import Dict, Any
|
||||
from ..models.trial_balance import (
|
||||
TrialBalanceItem,
|
||||
TrialBalanceFilters,
|
||||
@@ -25,14 +25,14 @@ class TrialBalanceService:
|
||||
"""Service pentru gestionarea balanței de verificare cu cache"""
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='schema', key_params=['company_id'])
|
||||
async def _get_schema(company_id: int) -> str:
|
||||
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
|
||||
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Obține schema pentru company_id (CACHED 24h)
|
||||
|
||||
This is cached permanently because company schemas rarely change.
|
||||
"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
schema_query = """
|
||||
SELECT schema
|
||||
@@ -50,7 +50,7 @@ class TrialBalanceService:
|
||||
@staticmethod
|
||||
@cached(cache_type='trial_balance', key_params=['company_id', 'luna', 'an', 'cont_filter',
|
||||
'denumire_filter', 'sort_by', 'sort_order',
|
||||
'page', 'page_size', 'username'])
|
||||
'page', 'page_size', 'username', 'server_id'])
|
||||
async def get_trial_balance(
|
||||
company_id: int,
|
||||
luna: int,
|
||||
@@ -61,7 +61,8 @@ class TrialBalanceService:
|
||||
sort_order: str,
|
||||
page: int,
|
||||
page_size: int,
|
||||
username: str
|
||||
username: str,
|
||||
server_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Obține balanța de verificare sintetică (CACHED 10 min)
|
||||
@@ -80,12 +81,13 @@ class TrialBalanceService:
|
||||
page: Pagina
|
||||
page_size: Mărimea paginii
|
||||
username: Username pentru cache tracking
|
||||
server_id: Optional Oracle server identifier for multi-server support
|
||||
|
||||
Returns:
|
||||
Dictionary cu items, pagination, filters_applied
|
||||
"""
|
||||
# Get schema (cached separately)
|
||||
schema = await TrialBalanceService._get_schema(company_id)
|
||||
schema = await TrialBalanceService._get_schema(company_id, server_id)
|
||||
|
||||
# Validate sort_order
|
||||
if sort_order.lower() not in ['asc', 'desc']:
|
||||
@@ -97,7 +99,7 @@ class TrialBalanceService:
|
||||
if sort_by.upper() not in valid_sort_columns:
|
||||
sort_by = 'CONT'
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Build base query for VBAL VIEW
|
||||
base_query = f"""
|
||||
|
||||
Reference in New Issue
Block a user