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

@@ -112,6 +112,43 @@
</div>
</div>
<!-- Detalii Suplimentare (TVA, items, address from OCR) -->
<template v-if="hasTvaData || receipt.items_count || receipt.vendor_address">
<Divider />
<h4 style="margin-bottom: 0.75rem; color: #0284c7;">
<i class="pi pi-list"></i>
Detalii Suplimentare
</h4>
<div class="detail-list">
<!-- TVA Breakdown -->
<div v-if="parsedTvaBreakdown?.length > 0" class="detail-item tva-detail">
<span class="label">TVA</span>
<div class="tva-breakdown-display">
<div v-for="(entry, idx) in parsedTvaBreakdown" :key="idx" class="tva-line">
<span class="tva-code" v-if="entry.code">{{ entry.code }}:</span>
<span class="tva-percent">{{ entry.percent }}%</span>
<span class="tva-amount">= {{ formatAmount(entry.amount) }}</span>
</div>
<div v-if="receipt.tva_total && parsedTvaBreakdown.length > 1" class="tva-total-line">
<strong>Total TVA: {{ formatAmount(receipt.tva_total) }}</strong>
</div>
</div>
</div>
<div class="detail-item" v-if="receipt.items_count">
<span class="label">Nr. Articole</span>
<span class="value">{{ receipt.items_count }} articole</span>
</div>
<div class="detail-item" v-if="receipt.vendor_address">
<span class="label">Adresa Furnizor</span>
<span class="value">{{ receipt.vendor_address }}</span>
</div>
</div>
</template>
<Divider />
<div class="detail-list">
@@ -283,6 +320,22 @@ const isBalanced = computed(() => {
return Math.abs(totalDebit.value - totalCredit.value) < 0.01
})
const parsedTvaBreakdown = computed(() => {
if (!receipt.value?.tva_breakdown) return []
try {
// Handle both string (JSON) and array formats
return typeof receipt.value.tva_breakdown === 'string'
? JSON.parse(receipt.value.tva_breakdown)
: receipt.value.tva_breakdown
} catch {
return []
}
})
const hasTvaData = computed(() => {
return parsedTvaBreakdown.value?.length > 0 || receipt.value?.tva_total
})
onMounted(async () => {
await loadReceipt()
})
@@ -521,4 +574,56 @@ const resubmitReceipt = async () => {
border-radius: 8px;
color: #f57c00;
}
/* TVA Breakdown Display */
.detail-item.tva-detail {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.tva-breakdown-display {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.5rem;
background: #f0f9ff;
border-radius: 6px;
width: 100%;
}
.tva-line {
display: flex;
align-items: center;
gap: 0.5rem;
}
.tva-code {
font-weight: 600;
color: #475569;
min-width: 1.5rem;
}
.tva-percent {
display: inline-block;
padding: 0.1rem 0.4rem;
background: #dbeafe;
border-radius: 4px;
font-size: 0.85rem;
color: #1e40af;
min-width: 2.5rem;
text-align: center;
}
.tva-amount {
font-weight: 500;
color: #334155;
}
.tva-total-line {
margin-top: 0.25rem;
padding-top: 0.25rem;
border-top: 1px dashed #0284c7;
color: #0284c7;
}
</style>