feat(ocr): Add modular store profiles with hot-reload support
## Store Profiles System
- Add ProfileRegistry for CUI-based profile lookup
- Add BaseStoreProfile with generic extraction patterns
- Implement hot-reload via POST /api/data-entry/ocr/profiles/reload
## 12 Store Profiles
- LIDL: Multi-rate TVA (A, B, C, D codes)
- OMV, SOCAR: B2B with client CUI, YYYY.MM.DD dates
- BRICK, DEDEMAN: Standard TVA, e-factura support
- KINETERRA, BEST PRINT: Non-VAT payers (returns [])
- STEPOUT MARKET: TVA 5% (books/reduced rate)
- UNLIMITED KEYS: NUMERAR payment detection
- GAMA INK, ELECTROBERING, PICTUS VELUM: Standard TVA
## Flexible TVA Patterns
- All patterns use (\d{1,2})% to accept any rate
- Supports historical (19%, 9%, 5%) and current (21%, 11%)
## Payment Methods Fix
- Fixed base.py to support multiple payments of same type
- Changed deduplication from method-only to (method, amount) tuple
- Returns separate entries for split payments
## Tools
- Add generate_store_profile.py for automatic profile generation
- Analyzes PDFs via OCR API and detects patterns
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -628,3 +628,86 @@ def _dict_to_extraction_data(data: dict) -> ExtractionData:
|
||||
validation_errors=data.get('validation_errors', []),
|
||||
inter_ocr_ratios=data.get('inter_ocr_ratios', {}),
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Store Profiles Management Endpoints
|
||||
# ============================================================================
|
||||
|
||||
@router.post("/profiles/reload")
|
||||
async def reload_store_profiles(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
"""
|
||||
Hot-reload all store profiles.
|
||||
|
||||
Reloads profile Python modules without server restart.
|
||||
Use after adding/modifying profile files.
|
||||
|
||||
Returns:
|
||||
Dict with reloaded count and profile list
|
||||
"""
|
||||
from backend.modules.data_entry.services.ocr.profiles import ProfileRegistry
|
||||
|
||||
count = ProfileRegistry.reload_all()
|
||||
status = ProfileRegistry.get_reload_status()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"reloaded_modules": count,
|
||||
"profiles_count": status["profiles_count"],
|
||||
"registered_cuis": status["registered_cuis"],
|
||||
"last_reload": status["last_reload"],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/profiles")
|
||||
async def list_store_profiles(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
"""
|
||||
List all registered store profiles.
|
||||
|
||||
Returns:
|
||||
Dict with profiles list and status
|
||||
"""
|
||||
from backend.modules.data_entry.services.ocr.profiles import ProfileRegistry
|
||||
|
||||
profiles = ProfileRegistry.list_profiles()
|
||||
status = ProfileRegistry.get_reload_status()
|
||||
|
||||
return {
|
||||
"profiles": profiles,
|
||||
"count": len(profiles),
|
||||
"last_reload": status["last_reload"],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/profiles/{cui}")
|
||||
async def get_store_profile(
|
||||
cui: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
"""
|
||||
Get details for a specific store profile.
|
||||
|
||||
Args:
|
||||
cui: Store CUI (with or without RO prefix)
|
||||
|
||||
Returns:
|
||||
Profile details including validation hints
|
||||
|
||||
Raises:
|
||||
404: If no profile exists for this CUI
|
||||
"""
|
||||
from backend.modules.data_entry.services.ocr.profiles import ProfileRegistry
|
||||
|
||||
info = ProfileRegistry.get_profile_info(cui)
|
||||
|
||||
if not info:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No profile registered for CUI: {cui}"
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
Reference in New Issue
Block a user