feat: Add multiple TVA entries support for Romanian receipts

- Add TvaEntry schema supporting multiple TVA rates (A, B, C, D codes)
- Update OCR extractor to extract multiple TVA entries from receipts
- Support both old (19%, 9%, 5%) and new Romanian rates (21%, 11% from Aug 2025)
- Add tva_breakdown, tva_total, items_count, vendor_address to Receipt model
- Update OCRPreview.vue to display TVA entries with rate badges
- Add "Detalii Suplimentare" section in ReceiptCreateView with editable TVA table
- Add TVA breakdown display in ReceiptDetailView
- Create database migration for new TVA columns

🤖 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-12 16:23:53 +02:00
parent 41ae97180e
commit 20448f7aa0
11 changed files with 1021 additions and 68 deletions

View File

@@ -23,24 +23,37 @@ class ImagePreprocessor:
raise ValueError(f"Could not load image: {path}")
return image
def pdf_to_images(self, path: Path, dpi: int = 300) -> List[np.ndarray]:
"""Convert PDF to images."""
def pdf_to_images(self, path: Path, dpi: int = 400) -> List[np.ndarray]:
"""
Convert PDF to images with high DPI for better OCR.
Args:
path: Path to PDF file
dpi: Resolution (400 recommended for receipts, higher = better quality but slower)
"""
if not PDF_AVAILABLE:
raise RuntimeError("pdf2image not available. Install with: pip install pdf2image")
# Use 400 DPI for better text recognition on thermal receipts
images = pdf2image.convert_from_path(str(path), dpi=dpi)
return [np.array(img) for img in images]
def preprocess(self, image: np.ndarray) -> np.ndarray:
def preprocess(self, image: np.ndarray, high_quality: bool = True) -> np.ndarray:
"""
Apply preprocessing pipeline for thermal receipt images.
Pipeline:
1. Convert to grayscale
2. Resize if too small (min 1000px width)
2. Resize if too small (min 1500px width for high quality)
3. Deskew (straighten rotated text)
4. Denoise (Non-local means)
5. Adaptive thresholding (binarization)
6. Morphological close (connect broken chars)
4. Contrast enhancement (CLAHE)
5. Denoise (Non-local means)
6. Sharpening (for clearer text edges)
7. Adaptive thresholding (binarization)
8. Morphological operations (connect broken chars)
Args:
image: Input image (BGR or grayscale)
high_quality: If True, apply more aggressive preprocessing
"""
# 1. Grayscale
if len(image.shape) == 3:
@@ -48,10 +61,11 @@ class ImagePreprocessor:
else:
gray = image.copy()
# 2. Resize if too small
# 2. Resize if too small (larger = better OCR)
height, width = gray.shape
if width < 1000:
scale = 1000 / width
min_width = 1500 if high_quality else 1000
if width < min_width:
scale = min_width / width
gray = cv2.resize(
gray, None, fx=scale, fy=scale,
interpolation=cv2.INTER_CUBIC
@@ -60,24 +74,43 @@ class ImagePreprocessor:
# 3. Deskew
gray = self._deskew(gray)
# 4. Denoise
# 4. Contrast enhancement with CLAHE (Contrast Limited Adaptive Histogram Equalization)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(gray)
# 5. Denoise (slightly less aggressive to preserve text details)
denoised = cv2.fastNlMeansDenoising(
gray, h=10,
enhanced, h=8, # Lower h = preserve more details
templateWindowSize=7,
searchWindowSize=21
)
# 5. Adaptive thresholding
# 6. Sharpening to enhance text edges
if high_quality:
# Unsharp mask for better text clarity
gaussian = cv2.GaussianBlur(denoised, (0, 0), 2.0)
sharpened = cv2.addWeighted(denoised, 1.5, gaussian, -0.5, 0)
else:
sharpened = denoised
# 7. Adaptive thresholding with optimized parameters
binary = cv2.adaptiveThreshold(
denoised, 255,
sharpened, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
blockSize=15, C=8
blockSize=11, # Smaller block = better for small text
C=5 # Lower C = darker result, better for faded receipts
)
# 6. Morphological close
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
result = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
# 8. Morphological operations
# Close small gaps in characters
kernel_close = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
result = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel_close)
# Optional: Remove small noise spots
if high_quality:
kernel_open = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 1))
result = cv2.morphologyEx(result, cv2.MORPH_OPEN, kernel_open)
return result