Consolidate 3 separate applications (reports-app, data-entry-app, telegram-bot) into a unified
architecture with single backend and frontend:
Backend Changes:
- Unified FastAPI backend at backend/ with modular structure
- Modules: reports, data_entry, telegram in backend/modules/
- Centralized config.py and main.py with all routers registered
- Single worker mode (--workers 1) for Telegram bot compatibility
- Shared Oracle connection pool and JWT authentication
- Unified requirements.txt and environment configuration
Frontend Changes:
- Single Vue.js SPA with module-based routing
- Unified frontend at src/ with modules in src/modules/{reports,data-entry}/
- Shared components and stores in src/shared/
- Error boundaries for module isolation
- Dual API proxy in Vite for module communication
Infrastructure:
- New unified startup scripts: start-prod.sh, start-test.sh, start-backend.sh
- Environment templates: .env.dev.example, .env.test.example, .env.prod.example
- Updated deployment scripts for Windows IIS
- Simplified SSH tunnel management
Documentation:
- Comprehensive CLAUDE.md with architecture overview
- Module-specific docs in docs/{data-entry,telegram}/
- Architecture decision records in docs/ARCHITECTURE-DECISIONS.md
- Deployment guides consolidated in deployment/windows/docs/
This migration reduces complexity, improves maintainability, and enables easier
deployment while maintaining all existing functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
235 lines
8.8 KiB
Python
235 lines
8.8 KiB
Python
"""Service for fetching nomenclatures from Oracle (read-only)."""
|
|
|
|
from typing import List, Optional
|
|
from decimal import Decimal
|
|
|
|
from sqlmodel import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from backend.modules.data_entry.schemas.receipt import (
|
|
PartnerOption,
|
|
AccountOption,
|
|
CashRegisterOption,
|
|
ExpenseTypeOption,
|
|
)
|
|
from backend.modules.data_entry.services.expense_types import EXPENSE_TYPES
|
|
from backend.modules.data_entry.db.models.nomenclature import SyncedSupplier, LocalSupplier, SyncedCashRegister
|
|
|
|
|
|
class NomenclatureService:
|
|
"""
|
|
Service for fetching nomenclatures.
|
|
|
|
In Phase 1 (MVP), some nomenclatures are hardcoded.
|
|
In Phase 2, these will be fetched from Oracle.
|
|
"""
|
|
|
|
@staticmethod
|
|
async def get_partners(
|
|
company_id: int,
|
|
search: Optional[str] = None,
|
|
session: Optional[AsyncSession] = None
|
|
) -> List[PartnerOption]:
|
|
"""
|
|
Get partners (suppliers/customers) for a company.
|
|
|
|
Phase 1: Returns mock data.
|
|
Phase 2: Returns synced data from SQLite (from Oracle sync).
|
|
Phase 3: Will fetch live from Oracle.
|
|
"""
|
|
# If session is provided, try to get from synced SQLite data
|
|
if session:
|
|
# Try to get from SQLite synced data
|
|
stmt = select(SyncedSupplier).where(SyncedSupplier.company_id == company_id)
|
|
if search:
|
|
stmt = stmt.where(
|
|
(SyncedSupplier.name.ilike(f"%{search}%")) |
|
|
(SyncedSupplier.fiscal_code.ilike(f"%{search}%"))
|
|
)
|
|
stmt = stmt.order_by(SyncedSupplier.name) # Order alphabetically, no limit for AutoComplete
|
|
|
|
result = await session.execute(stmt)
|
|
suppliers = result.scalars().all()
|
|
|
|
if suppliers:
|
|
# Also get local suppliers
|
|
local_stmt = select(LocalSupplier).where(LocalSupplier.company_id == company_id)
|
|
if search:
|
|
local_stmt = local_stmt.where(
|
|
(LocalSupplier.name.ilike(f"%{search}%")) |
|
|
(LocalSupplier.fiscal_code.ilike(f"%{search}%"))
|
|
)
|
|
local_stmt = local_stmt.order_by(LocalSupplier.name) # Order alphabetically
|
|
|
|
local_result = await session.execute(local_stmt)
|
|
local_suppliers = local_result.scalars().all()
|
|
|
|
# Combine both - no IDs needed, just text data for autocomplete
|
|
partners = []
|
|
for s in suppliers:
|
|
partners.append(PartnerOption(
|
|
name=s.name,
|
|
fiscal_code=s.fiscal_code,
|
|
address=s.address,
|
|
source="oracle"
|
|
))
|
|
for l in local_suppliers:
|
|
partners.append(PartnerOption(
|
|
name=l.name, # No suffix - must match search results
|
|
fiscal_code=l.fiscal_code,
|
|
address=l.address,
|
|
source="local"
|
|
))
|
|
|
|
return partners
|
|
|
|
# Fallback to mock data for Phase 1 (when no synced data)
|
|
mock_partners = [
|
|
PartnerOption(name="OMV Petrom", fiscal_code="RO123456", source="mock"),
|
|
PartnerOption(name="Dedeman", fiscal_code="RO789012", source="mock"),
|
|
PartnerOption(name="Kaufland", fiscal_code="RO345678", source="mock"),
|
|
PartnerOption(name="Emag", fiscal_code="RO901234", source="mock"),
|
|
PartnerOption(name="Altex", fiscal_code="RO567890", source="mock"),
|
|
]
|
|
|
|
if search:
|
|
search_lower = search.lower()
|
|
mock_partners = [
|
|
p for p in mock_partners
|
|
if search_lower in p.name.lower() or (p.fiscal_code and search_lower in p.fiscal_code.lower())
|
|
]
|
|
|
|
return mock_partners
|
|
|
|
@staticmethod
|
|
async def get_accounts(company_id: int, prefix: Optional[str] = None) -> List[AccountOption]:
|
|
"""
|
|
Get chart of accounts for a company.
|
|
|
|
Phase 1: Returns common expense/income accounts.
|
|
Phase 2: Will fetch from Oracle PLAN_CONTURI.
|
|
"""
|
|
# Common accounts for expenses and receipts
|
|
accounts = [
|
|
# Expense accounts (Class 6)
|
|
AccountOption(code="6022", name="Cheltuieli cu combustibilii"),
|
|
AccountOption(code="6024", name="Cheltuieli materiale pentru ambalat"),
|
|
AccountOption(code="6028", name="Alte cheltuieli cu materiale consumabile"),
|
|
AccountOption(code="624", name="Cheltuieli cu transportul de bunuri si personal"),
|
|
AccountOption(code="626", name="Cheltuieli postale si taxe telecomunicatii"),
|
|
AccountOption(code="628", name="Alte cheltuieli cu serviciile executate de terti"),
|
|
|
|
# VAT
|
|
AccountOption(code="4426", name="TVA deductibila"),
|
|
AccountOption(code="4427", name="TVA colectata"),
|
|
|
|
# Cash and Bank (Class 5)
|
|
AccountOption(code="5311", name="Casa in lei"),
|
|
AccountOption(code="5121", name="Conturi la banci in lei"),
|
|
|
|
# Income accounts (Class 7)
|
|
AccountOption(code="7588", name="Alte venituri din exploatare"),
|
|
]
|
|
|
|
if prefix:
|
|
accounts = [a for a in accounts if a.code.startswith(prefix)]
|
|
|
|
return accounts
|
|
|
|
@staticmethod
|
|
async def get_cash_registers(
|
|
company_id: int,
|
|
session: Optional[AsyncSession] = None
|
|
) -> List[CashRegisterOption]:
|
|
"""
|
|
Get cash registers and bank accounts for a company.
|
|
|
|
Phase 1: Returns default options.
|
|
Phase 2: Returns synced data from SQLite (from Oracle sync).
|
|
Phase 3: Will fetch live from Oracle NOM_CASE / NOM_BANCI.
|
|
"""
|
|
# If session is provided, try to get from synced SQLite data
|
|
if session:
|
|
stmt = select(SyncedCashRegister).where(SyncedCashRegister.company_id == company_id)
|
|
result = await session.execute(stmt)
|
|
registers = result.scalars().all()
|
|
|
|
if registers:
|
|
return [
|
|
CashRegisterOption(id=r.id, name=r.name, account_code=r.account_code)
|
|
for r in registers
|
|
]
|
|
|
|
# Fallback to default cash registers for Phase 1
|
|
return [
|
|
CashRegisterOption(id=1, name="Casa principala", account_code="5311"),
|
|
CashRegisterOption(id=2, name="Cont BCR", account_code="5121"),
|
|
CashRegisterOption(id=3, name="Cont BRD", account_code="5121"),
|
|
]
|
|
|
|
@staticmethod
|
|
async def get_expense_types() -> List[ExpenseTypeOption]:
|
|
"""
|
|
Get predefined expense types with their accounting configuration.
|
|
"""
|
|
return [
|
|
ExpenseTypeOption(
|
|
code=et.code,
|
|
name=et.name,
|
|
account_code=et.account_code,
|
|
has_vat=et.has_vat,
|
|
vat_percent=et.vat_percent,
|
|
)
|
|
for et in EXPENSE_TYPES.values()
|
|
]
|
|
|
|
@staticmethod
|
|
async def get_companies(username: str) -> List[dict]:
|
|
"""
|
|
Get companies accessible by user.
|
|
|
|
Phase 1: Returns mock data.
|
|
Phase 2: Will fetch from shared auth based on user permissions.
|
|
"""
|
|
# TODO: Integrate with shared auth to get user's companies
|
|
return [
|
|
{"id": 1, "name": "SC Test SRL", "cui": "RO12345678"},
|
|
{"id": 2, "name": "SC Demo SA", "cui": "RO87654321"},
|
|
]
|
|
|
|
# ============ Phase 2 Oracle Integration Methods ============
|
|
|
|
@staticmethod
|
|
async def _fetch_partners_oracle(company_id: int, search: Optional[str] = None) -> List[PartnerOption]:
|
|
"""
|
|
Fetch partners from Oracle NOM_PARTENERI.
|
|
|
|
Will be implemented in Phase 2.
|
|
"""
|
|
# TODO: Implement using shared oracle_pool
|
|
# Example query:
|
|
# SELECT ID_PART, DEN_PART, COD_FISCAL
|
|
# FROM {schema}.NOM_PARTENERI
|
|
# WHERE DEN_PART LIKE :search
|
|
raise NotImplementedError("Oracle integration pending - Phase 2")
|
|
|
|
@staticmethod
|
|
async def _fetch_accounts_oracle(company_id: int, prefix: Optional[str] = None) -> List[AccountOption]:
|
|
"""
|
|
Fetch chart of accounts from Oracle PLAN_CONTURI.
|
|
|
|
Will be implemented in Phase 2.
|
|
"""
|
|
# TODO: Implement using shared oracle_pool
|
|
raise NotImplementedError("Oracle integration pending - Phase 2")
|
|
|
|
@staticmethod
|
|
async def _fetch_cash_registers_oracle(company_id: int) -> List[CashRegisterOption]:
|
|
"""
|
|
Fetch cash registers from Oracle NOM_CASE / NOM_BANCI.
|
|
|
|
Will be implemented in Phase 2.
|
|
"""
|
|
# TODO: Implement using shared oracle_pool
|
|
raise NotImplementedError("Oracle integration pending - Phase 2")
|