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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user