feat: Add payment methods extraction, OCR improvements, and AutoComplete fix

Backend:
- Add payment_methods and payment_mode fields to Receipt model
- Add payment method extraction (CARD/NUMERAR) with auto-suggestion logic
- Improve OCR service with TVA validation and reverse calculation
- Fix nomenclature service supplier limit (was 50, now unlimited)
- Add OCR fields migrations (ocr_raw_text, ocr_confidence, payment_mode)

Frontend:
- Fix AutoComplete to properly display supplier name after OCR
- Add payment methods display in OCR preview with suggested payment mode
- Improve ReceiptCreateView form handling and OCR data application

Database migrations:
- 20251215_add_ocr_fields_to_receipt.py
- 20251215_remove_partner_id.py
- 20251216_add_payment_mode.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-16 13:43:15 +02:00
parent 46d9be0c08
commit c1220e86a6
15 changed files with 734 additions and 94 deletions

View File

@@ -37,6 +37,30 @@ def _serialize_tva_breakdown(tva_breakdown: Optional[List[Any]]) -> Optional[str
return json.dumps(serializable)
def _serialize_payment_methods(payment_methods: Optional[List[Any]]) -> Optional[str]:
"""Serialize payment methods list to JSON string for SQLite storage."""
if payment_methods is None:
return None
serializable = []
for pm in payment_methods:
if hasattr(pm, 'model_dump'):
item = pm.model_dump()
elif isinstance(pm, dict):
item = pm.copy()
else:
item = dict(pm)
# Convert Decimal to float for JSON
if 'amount' in item:
if hasattr(item['amount'], '__float__'):
item['amount'] = float(item['amount'])
serializable.append(item)
return json.dumps(serializable)
class ReceiptCRUD:
"""CRUD operations for Receipt model."""
@@ -47,9 +71,10 @@ class ReceiptCRUD:
created_by: str,
) -> Receipt:
"""Create a new receipt."""
# Get data as dict and serialize tva_breakdown to JSON string
# Get data as dict and serialize tva_breakdown and payment_methods to JSON string
receipt_data = data.model_dump()
receipt_data['tva_breakdown'] = _serialize_tva_breakdown(receipt_data.get('tva_breakdown'))
receipt_data['payment_methods'] = _serialize_payment_methods(receipt_data.get('payment_methods'))
receipt = Receipt(
**receipt_data,
@@ -165,9 +190,11 @@ class ReceiptCRUD:
"""Update receipt fields."""
update_data = data.model_dump(exclude_unset=True)
# Serialize tva_breakdown to JSON string if present
# Serialize tva_breakdown and payment_methods to JSON string if present
if 'tva_breakdown' in update_data:
update_data['tva_breakdown'] = _serialize_tva_breakdown(update_data['tva_breakdown'])
if 'payment_methods' in update_data:
update_data['payment_methods'] = _serialize_payment_methods(update_data['payment_methods'])
for field, value in update_data.items():
setattr(receipt, field, value)

View File

@@ -29,6 +29,13 @@ class ReceiptStatus(str, Enum):
SYNCED = "synced" # Synced to Oracle (Phase 2)
class PaymentMode(str, Enum):
"""Payment mode - how the expense was paid."""
CASA = "casa" # Numerar firma (5311)
BANCA = "banca" # Virament/POS (5121)
AVANS_DECONTARE = "avans_decontare" # Decont angajat (542)
if TYPE_CHECKING:
from .accounting_entry import AccountingEntry
@@ -62,11 +69,15 @@ class Receipt(SQLModel, table=True):
# Oracle references (nomenclatures)
company_id: int
partner_id: Optional[int] = Field(default=None)
partner_name: Optional[str] = Field(default=None, max_length=200) # Cache for display
# partner_id removed - supplier data is text-only (partner_name, cui)
partner_name: Optional[str] = Field(default=None, max_length=200) # Supplier name from OCR/selection
cui: Optional[str] = Field(default=None, max_length=20) # Fiscal code from OCR
ocr_raw_text: Optional[str] = Field(default=None) # Raw OCR text for debugging
payment_methods: Optional[str] = Field(default=None, max_length=500) # JSON: [{"method":"CARD","amount":"50.00"}]
cash_register_id: Optional[int] = Field(default=None) # Cash/Bank ID from Oracle
cash_register_name: Optional[str] = Field(default=None, max_length=100) # Cache for display
cash_register_account: Optional[str] = Field(default=None, max_length=20) # Account code (5311, 5121)
payment_mode: Optional[str] = Field(default=None, max_length=20) # PaymentMode value: casa/banca/avans_decontare
# Workflow
status: ReceiptStatus = Field(default=ReceiptStatus.DRAFT)