Shared Components: - Add CompanySelector.vue and PeriodSelector.vue components - Add AppHeader.vue and SlideMenu.vue layout components - Add shared stores factories (companies.js, accountingPeriod.js) - Add shared routes factories (companies.py, calendar.py) - Add shared models (company.py, calendar.py) - Add shared layout styles (header.css, navigation.css) Data Entry App: - Update CLAUDE.md with prod/test server documentation - Improve nomenclature sync service with better error handling - Update receipts router and CRUD operations - Add company/period stores using shared factories - Update App.vue layout with shared components - Fix OCRUploadZone file handling Reports App: - Refactor stores to use shared factories - Update App.vue to use shared layout components Infrastructure: - Replace start-data-entry.sh with separate dev/test scripts - Add .claude/rules for authentication, backend patterns, etc. - Add implementation plan for OCR receipt improvements - Clean up old documentation files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
254 lines
7.2 KiB
Python
254 lines
7.2 KiB
Python
"""Nomenclature API endpoints."""
|
|
|
|
from typing import Optional, List, Annotated
|
|
from fastapi import APIRouter, Depends, HTTPException, Header
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from pydantic import BaseModel
|
|
|
|
from app.db.database import get_session
|
|
from app.services.sync_service import SyncService
|
|
|
|
# Import auth dependencies
|
|
import sys
|
|
from pathlib import Path
|
|
project_root = Path(__file__).parent.parent.parent.parent.parent
|
|
sys.path.insert(0, str(project_root / "shared"))
|
|
|
|
from auth.dependencies import get_current_user
|
|
from auth.models import CurrentUser
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ============ Selected Company Dependency ============
|
|
|
|
async def get_selected_company(
|
|
current_user: CurrentUser = Depends(get_current_user),
|
|
x_selected_company: Annotated[Optional[str], Header()] = None
|
|
) -> int:
|
|
"""
|
|
Get selected company from X-Selected-Company header.
|
|
Validates user access. Falls back to first company if no header.
|
|
"""
|
|
if x_selected_company:
|
|
try:
|
|
company_id = int(x_selected_company)
|
|
except ValueError:
|
|
raise HTTPException(400, f"Invalid company ID: {x_selected_company}")
|
|
|
|
if str(company_id) in current_user.companies:
|
|
return company_id
|
|
raise HTTPException(403, f"Nu aveți acces la firma {company_id}")
|
|
|
|
if current_user.companies:
|
|
try:
|
|
return int(current_user.companies[0])
|
|
except (ValueError, IndexError):
|
|
pass
|
|
|
|
raise HTTPException(400, "Nu aveți nicio firmă asignată")
|
|
|
|
|
|
SelectedCompany = Annotated[int, Depends(get_selected_company)]
|
|
|
|
|
|
# Request/Response Models
|
|
class SupplierSearchResult(BaseModel):
|
|
found: bool
|
|
supplier: Optional[dict] = None
|
|
source: str # 'synced', 'local', 'not_found'
|
|
|
|
|
|
class LocalSupplierCreate(BaseModel):
|
|
name: str
|
|
fiscal_code: Optional[str] = None
|
|
address: Optional[str] = None
|
|
|
|
|
|
class LocalSupplierResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
fiscal_code: Optional[str]
|
|
address: Optional[str]
|
|
is_local: bool = True
|
|
|
|
|
|
class SyncResult(BaseModel):
|
|
synced: int
|
|
errors: int
|
|
message: str
|
|
|
|
|
|
class SupplierOption(BaseModel):
|
|
id: int
|
|
oracle_id: Optional[int] = None
|
|
name: str
|
|
fiscal_code: Optional[str]
|
|
source: str # 'synced' or 'local'
|
|
|
|
|
|
class CashRegisterOption(BaseModel):
|
|
id: int
|
|
oracle_id: int
|
|
name: str
|
|
account_code: str
|
|
register_type: str
|
|
|
|
|
|
# Endpoints
|
|
@router.get("/suppliers/search", response_model=SupplierSearchResult)
|
|
async def search_supplier(
|
|
fiscal_code: Optional[str] = None,
|
|
name: Optional[str] = None,
|
|
company_id: Optional[int] = None,
|
|
session: AsyncSession = Depends(get_session),
|
|
selected_company: SelectedCompany = None,
|
|
):
|
|
"""Search for supplier by fiscal code or name."""
|
|
if not fiscal_code and not name:
|
|
raise HTTPException(status_code=400, detail="Provide fiscal_code or name")
|
|
|
|
cid = company_id or selected_company
|
|
|
|
found, supplier, source = await SyncService.search_supplier(
|
|
session, cid, fiscal_code, name
|
|
)
|
|
|
|
return SupplierSearchResult(found=found, supplier=supplier, source=source)
|
|
|
|
|
|
@router.get("/suppliers", response_model=List[SupplierOption])
|
|
async def get_suppliers(
|
|
search: Optional[str] = None,
|
|
company_id: Optional[int] = None,
|
|
session: AsyncSession = Depends(get_session),
|
|
selected_company: SelectedCompany = None,
|
|
):
|
|
"""Get all suppliers (synced + local) for dropdown/autocomplete."""
|
|
cid = company_id or selected_company
|
|
|
|
suppliers = await SyncService.get_all_suppliers(session, cid, search)
|
|
|
|
return [
|
|
SupplierOption(
|
|
id=s["id"],
|
|
oracle_id=s.get("oracle_id"),
|
|
name=s["name"],
|
|
fiscal_code=s.get("fiscal_code"),
|
|
source=s["source"]
|
|
)
|
|
for s in suppliers
|
|
]
|
|
|
|
|
|
@router.post("/suppliers/local", response_model=LocalSupplierResponse)
|
|
async def create_local_supplier(
|
|
data: LocalSupplierCreate,
|
|
company_id: Optional[int] = None,
|
|
session: AsyncSession = Depends(get_session),
|
|
selected_company: SelectedCompany = None,
|
|
current_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
"""Create a local supplier from OCR data."""
|
|
cid = company_id or selected_company
|
|
|
|
supplier = await SyncService.create_local_supplier(
|
|
session, cid, data.name, data.fiscal_code, data.address, current_user.username
|
|
)
|
|
|
|
return LocalSupplierResponse(
|
|
id=supplier.id,
|
|
name=supplier.name,
|
|
fiscal_code=supplier.fiscal_code,
|
|
address=supplier.address,
|
|
)
|
|
|
|
|
|
@router.get("/cash-registers", response_model=List[CashRegisterOption])
|
|
async def get_cash_registers(
|
|
company_id: Optional[int] = None,
|
|
session: AsyncSession = Depends(get_session),
|
|
selected_company: SelectedCompany = None,
|
|
):
|
|
"""Get all cash registers for a company."""
|
|
cid = company_id or selected_company
|
|
|
|
registers = await SyncService.get_all_cash_registers(session, cid)
|
|
|
|
return [
|
|
CashRegisterOption(
|
|
id=r["id"],
|
|
oracle_id=r["oracle_id"],
|
|
name=r["name"],
|
|
account_code=r["account_code"],
|
|
register_type=r["register_type"]
|
|
)
|
|
for r in registers
|
|
]
|
|
|
|
|
|
@router.post("/sync/suppliers", response_model=SyncResult)
|
|
async def sync_suppliers(
|
|
company_id: Optional[int] = None,
|
|
session: AsyncSession = Depends(get_session),
|
|
selected_company: SelectedCompany = None,
|
|
):
|
|
"""Manually trigger supplier sync from Oracle."""
|
|
cid = company_id or selected_company
|
|
|
|
synced, errors = await SyncService.sync_suppliers(session, cid)
|
|
|
|
return SyncResult(
|
|
synced=synced,
|
|
errors=errors,
|
|
message=f"Synced {synced} suppliers with {errors} errors"
|
|
)
|
|
|
|
|
|
@router.post("/sync/cash-registers", response_model=SyncResult)
|
|
async def sync_cash_registers(
|
|
company_id: Optional[int] = None,
|
|
session: AsyncSession = Depends(get_session),
|
|
selected_company: SelectedCompany = None,
|
|
):
|
|
"""Manually trigger cash register sync from Oracle."""
|
|
cid = company_id or selected_company
|
|
|
|
synced, errors = await SyncService.sync_cash_registers(session, cid)
|
|
|
|
return SyncResult(
|
|
synced=synced,
|
|
errors=errors,
|
|
message=f"Synced {synced} cash registers with {errors} errors"
|
|
)
|
|
|
|
|
|
@router.post("/sync/all", response_model=dict)
|
|
async def sync_all_nomenclatures(
|
|
company_id: Optional[int] = None,
|
|
session: AsyncSession = Depends(get_session),
|
|
selected_company: SelectedCompany = None,
|
|
):
|
|
"""Sync all nomenclatures (suppliers + cash registers) from Oracle."""
|
|
cid = company_id or selected_company
|
|
|
|
# Sync suppliers
|
|
suppliers_synced, suppliers_errors = await SyncService.sync_suppliers(session, cid)
|
|
|
|
# Sync cash registers
|
|
registers_synced, registers_errors = await SyncService.sync_cash_registers(session, cid)
|
|
|
|
return {
|
|
"suppliers": {
|
|
"synced": suppliers_synced,
|
|
"errors": suppliers_errors
|
|
},
|
|
"cash_registers": {
|
|
"synced": registers_synced,
|
|
"errors": registers_errors
|
|
},
|
|
"total_synced": suppliers_synced + registers_synced,
|
|
"total_errors": suppliers_errors + registers_errors,
|
|
"message": f"Synced {suppliers_synced} suppliers and {registers_synced} cash registers"
|
|
}
|