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

@@ -246,11 +246,59 @@
<Textarea
v-model="form.description"
rows="3"
placeholder="Detalii suplimentare..."
placeholder="Descriere optionala..."
/>
</div>
</div>
<!-- Detalii Suplimentare (populated from OCR) -->
<div v-if="form.tva_breakdown?.length > 0 || form.items_count || form.vendor_address" class="extra-details-section">
<h3>
<i class="pi pi-list"></i>
Detalii Suplimentare (din OCR)
</h3>
<!-- TVA Breakdown -->
<div class="form-field form-field-full" v-if="form.tva_breakdown?.length > 0">
<label>Defalcare TVA</label>
<div class="tva-table">
<div v-for="(entry, idx) in form.tva_breakdown" :key="idx" class="tva-row">
<span class="tva-label">TVA {{ entry.code }} ({{ entry.percent }}%):</span>
<InputNumber
v-model="form.tva_breakdown[idx].amount"
mode="currency"
currency="RON"
locale="ro-RO"
:minFractionDigits="2"
class="tva-input"
/>
</div>
<div class="tva-row total" v-if="form.tva_breakdown.length > 0">
<span class="tva-label"><strong>Total TVA:</strong></span>
<span class="tva-value">{{ formatTvaTotal() }} RON</span>
</div>
</div>
</div>
<div class="form-grid">
<div class="form-field" v-if="form.items_count">
<label>Nr. Articole</label>
<InputNumber
v-model="form.items_count"
:min="1"
placeholder="Ex: 17"
/>
</div>
<div class="form-field" v-if="form.vendor_address">
<label>Adresa Furnizor</label>
<InputText
v-model="form.vendor_address"
placeholder="Adresa din bon"
/>
</div>
</div>
</div>
<Divider />
<!-- Actions -->
@@ -314,6 +362,11 @@ const form = ref({
receipt_number: '',
description: '',
company_id: 1, // Default company for Phase 1
// TVA info (multiple entries support)
tva_breakdown: [], // Array of {code, percent, amount}
tva_total: null,
items_count: null,
vendor_address: '',
})
const selectedFiles = ref([])
@@ -435,6 +488,24 @@ const applyOCRData = (data) => {
}
}
// Apply TVA entries
if (data.tva_entries?.length > 0) {
form.value.tva_breakdown = data.tva_entries.map(e => ({
code: e.code,
percent: e.percent,
amount: parseFloat(e.amount)
}))
}
if (data.tva_total) {
form.value.tva_total = parseFloat(data.tva_total)
}
if (data.items_count) {
form.value.items_count = data.items_count
}
if (data.address) {
form.value.vendor_address = data.address
}
// Clear OCR preview
ocrData.value = null
@@ -499,6 +570,12 @@ const formatFileSize = (bytes) => {
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
const formatTvaTotal = () => {
if (!form.value.tva_breakdown?.length) return '0.00'
const total = form.value.tva_breakdown.reduce((sum, e) => sum + (e.amount || 0), 0)
return total.toLocaleString('ro-RO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
const validateForm = () => {
// Check if we have at least one file (for new receipts)
if (!isEditMode.value && selectedFiles.value.length === 0) {
@@ -725,4 +802,55 @@ const submitForReview = async () => {
font-size: 0.85rem;
color: #64748b;
}
/* Extra details section (TVA, items, address) */
.extra-details-section {
margin-top: 1.5rem;
padding: 1rem;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
}
.extra-details-section h3 {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
color: #0284c7;
}
.tva-table {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tva-row {
display: flex;
align-items: center;
gap: 1rem;
}
.tva-row.total {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px dashed #0284c7;
}
.tva-label {
min-width: 150px;
font-weight: 500;
color: #334155;
}
.tva-input {
max-width: 150px;
}
.tva-value {
font-weight: 600;
color: #0284c7;
}
</style>