feat(ocr): Add docTR OCR engine with metrics infrastructure
Add docTR as primary OCR engine with 2-tier sequential processing, OCR metrics tracking, and simplified engine selection. Features: - docTR OCR engine with light+medium preprocessing tiers - doctr_plus mode with early exit optimization (~65% fast path) - OCR metrics dashboard with per-engine statistics - User OCR preference persistence - Parallel worker pool for OCR processing - Cross-validation for extraction quality Engine options: tesseract, doctr, doctr_plus (recommended), paddleocr 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -244,7 +244,20 @@
|
||||
<div class="form-group">
|
||||
<div class="form-row">
|
||||
<div class="form-field flex-2">
|
||||
<label>Furnizor</label>
|
||||
<div class="label-with-action">
|
||||
<label>Furnizor</label>
|
||||
<Button
|
||||
v-if="!isReadOnly"
|
||||
icon="pi pi-sync"
|
||||
size="small"
|
||||
text
|
||||
rounded
|
||||
:loading="syncingSuppliers"
|
||||
@click="resyncSuppliers"
|
||||
v-tooltip.top="'Re-sincronizeaza furnizorii din Oracle'"
|
||||
class="sync-btn"
|
||||
/>
|
||||
</div>
|
||||
<AutoComplete
|
||||
v-model="form.partner_name"
|
||||
:suggestions="filteredPartners"
|
||||
@@ -265,10 +278,22 @@
|
||||
<div class="form-field flex-1">
|
||||
<label>CUI</label>
|
||||
<InputText v-model="form.cui" placeholder="RO12345678" :disabled="isReadOnly" />
|
||||
<small v-if="supplierWarning.show" class="p-text-warning supplier-warning">
|
||||
<i class="pi pi-exclamation-triangle"></i>
|
||||
Negasit
|
||||
</small>
|
||||
<div v-if="supplierWarning.show" class="supplier-warning-box">
|
||||
<small class="p-text-warning">
|
||||
<i class="pi pi-exclamation-triangle"></i>
|
||||
Negasit - se va crea automat la salvare
|
||||
</small>
|
||||
<Button
|
||||
v-if="!isReadOnly"
|
||||
label="Creaza acum"
|
||||
icon="pi pi-plus"
|
||||
size="small"
|
||||
severity="warning"
|
||||
text
|
||||
@click="createLocalSupplierFromWarning"
|
||||
class="supplier-create-btn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Adresa colapsata -->
|
||||
@@ -406,14 +431,13 @@
|
||||
<div class="form-group form-group-last">
|
||||
<div class="form-row">
|
||||
<div class="form-field flex-1">
|
||||
<label>Tip Cheltuiala *</label>
|
||||
<label>Tip Cheltuiala</label>
|
||||
<Dropdown
|
||||
v-model="form.expense_type_code"
|
||||
:options="expenseTypes"
|
||||
optionLabel="name"
|
||||
optionValue="code"
|
||||
placeholder="Selecteaza tip cheltuiala"
|
||||
required
|
||||
:disabled="isReadOnly"
|
||||
class="dropdown-borderless"
|
||||
/>
|
||||
@@ -678,7 +702,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useReceiptsStore } from '@data-entry/stores/receiptsStore'
|
||||
@@ -771,6 +795,7 @@ const missingClientWarning = ref(false)
|
||||
// AutoComplete support
|
||||
const filteredPartners = ref([])
|
||||
const supplierSource = ref(null) // 'local', 'synced', or null
|
||||
const syncingSuppliers = ref(false)
|
||||
|
||||
const partners = computed(() => store.partners)
|
||||
const expenseTypes = computed(() => store.expenseTypes)
|
||||
@@ -812,7 +837,6 @@ const missingRequiredFields = computed(() => {
|
||||
const missing = []
|
||||
if (!validationState.value.hasAmount) missing.push('Suma')
|
||||
if (!validationState.value.hasDate) missing.push('Data')
|
||||
if (!validationState.value.hasExpenseType) missing.push('Tip cheltuiala')
|
||||
if (!validationState.value.hasAttachment) missing.push('Atasament')
|
||||
return missing
|
||||
})
|
||||
@@ -848,6 +872,11 @@ const searchPartners = (event) => {
|
||||
onMounted(async () => {
|
||||
await store.fetchAllNomenclatures()
|
||||
|
||||
// Sync suppliers from Oracle if list is empty (first use or no synced data)
|
||||
if (store.partners.length === 0) {
|
||||
await syncSuppliersIfNeeded()
|
||||
}
|
||||
|
||||
if (isEditMode.value || isViewMode.value) {
|
||||
await loadReceipt()
|
||||
} else {
|
||||
@@ -856,6 +885,76 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Sync suppliers from Oracle if not already synced
|
||||
const syncSuppliersIfNeeded = async () => {
|
||||
try {
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Sincronizare furnizori',
|
||||
detail: 'Se sincronizeaza furnizorii din Oracle...',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
const result = await store.syncSuppliers()
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Sincronizare completa',
|
||||
detail: `${result.synced || store.partners.length} furnizori sincronizati`,
|
||||
life: 3000,
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('[ReceiptCreateView] Supplier sync failed:', error)
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Sincronizare esuata',
|
||||
detail: 'Nu s-au putut sincroniza furnizorii. Puteti continua cu furnizori locali.',
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for company changes - sync suppliers in background
|
||||
watch(
|
||||
() => companyStore.selectedCompany,
|
||||
async (newCompany, oldCompany) => {
|
||||
// Only trigger if company actually changed (not on initial load)
|
||||
if (newCompany && oldCompany && newCompany.id_firma !== oldCompany.id_firma) {
|
||||
console.log('[ReceiptCreateView] Company changed, syncing suppliers in background...')
|
||||
// Background sync - don't await, don't block UI
|
||||
store.syncSuppliers().then(result => {
|
||||
console.log('[ReceiptCreateView] Background sync complete:', result)
|
||||
}).catch(error => {
|
||||
console.warn('[ReceiptCreateView] Background sync failed:', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Manual re-sync suppliers from Oracle (button click)
|
||||
const resyncSuppliers = async () => {
|
||||
syncingSuppliers.value = true
|
||||
try {
|
||||
const result = await store.syncSuppliers()
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Sincronizare completa',
|
||||
detail: `${result.synced || 0} furnizori noi din Oracle`,
|
||||
life: 3000,
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('[ReceiptCreateView] Manual supplier sync failed:', error)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Sincronizare esuata',
|
||||
detail: error.message || 'Nu s-au putut sincroniza furnizorii',
|
||||
life: 5000,
|
||||
})
|
||||
} finally {
|
||||
syncingSuppliers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadReceipt = async () => {
|
||||
try {
|
||||
receipt.value = await store.fetchReceiptById(receiptId.value)
|
||||
@@ -954,6 +1053,7 @@ const onOCRFileSelected = (file) => {
|
||||
}
|
||||
|
||||
const onOCRResult = (data) => {
|
||||
console.log('[OCR Result] Received data, suggested_payment_mode:', data.suggested_payment_mode, 'payment_methods:', data.payment_methods)
|
||||
ocrData.value = data
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
@@ -1142,9 +1242,11 @@ const applyOCRData = async (data) => {
|
||||
}
|
||||
|
||||
// Auto-suggest payment_mode if OCR detected CARD
|
||||
console.log('[OCR Apply] suggested_payment_mode:', data.suggested_payment_mode, 'payment_methods:', data.payment_methods)
|
||||
if (data.suggested_payment_mode) {
|
||||
form.value.payment_mode = data.suggested_payment_mode
|
||||
paymentSetFromOCR.value = true // Show OCR indicator
|
||||
console.log('[OCR Apply] Set payment_mode to:', data.suggested_payment_mode)
|
||||
}
|
||||
|
||||
// AUTO-DETECT DIRECTION (PLATĂ/ÎNCASARE) based on CUI matching
|
||||
@@ -1367,6 +1469,36 @@ const cancelCreateSupplier = () => {
|
||||
pendingSupplierData.value = null
|
||||
}
|
||||
|
||||
// Create local supplier immediately from warning (inline button)
|
||||
const createLocalSupplierFromWarning = async () => {
|
||||
if (!form.value.cui) return
|
||||
|
||||
try {
|
||||
await store.createLocalSupplier({
|
||||
name: form.value.partner_name || supplierWarning.value.name || `Furnizor ${form.value.cui}`,
|
||||
fiscal_code: form.value.cui,
|
||||
address: form.value.vendor_address || null
|
||||
})
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Furnizor creat',
|
||||
detail: `${form.value.partner_name || form.value.cui} a fost adaugat`,
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
supplierWarning.value = { show: false, cui: '', name: '' }
|
||||
supplierSource.value = 'local'
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: error.message || 'Nu s-a putut crea furnizorul',
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to calculate similarity between two CUI strings
|
||||
// Returns a value between 0 and 1 (1 = identical)
|
||||
const calculateCuiSimilarity = (cui1, cui2) => {
|
||||
@@ -1727,16 +1859,6 @@ const validateForm = () => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!form.value.expense_type_code) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Validare',
|
||||
detail: 'Tipul cheltuielii este obligatoriu',
|
||||
life: 3000,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// Payment mode is validated at submit time, not at draft save
|
||||
// (can save draft without payment mode, but submit requires it)
|
||||
|
||||
@@ -1749,6 +1871,31 @@ const saveReceipt = async () => {
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
// Auto-create local supplier if CUI is present but not found in database
|
||||
if (form.value.cui && supplierWarning.value.show) {
|
||||
try {
|
||||
await store.createLocalSupplier({
|
||||
name: form.value.partner_name || `Furnizor ${form.value.cui}`,
|
||||
fiscal_code: form.value.cui,
|
||||
address: form.value.vendor_address || null
|
||||
})
|
||||
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Furnizor local creat',
|
||||
detail: `${form.value.partner_name || form.value.cui} adaugat automat`,
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
// Clear warning since supplier is now created
|
||||
supplierWarning.value = { show: false, cui: '', name: '' }
|
||||
supplierSource.value = 'local'
|
||||
} catch (error) {
|
||||
console.warn('[saveReceipt] Failed to auto-create local supplier:', error)
|
||||
// Continue with save anyway - supplier creation is optional
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up payment_methods and tva_breakdown - convert null amounts to 0
|
||||
const cleanedPaymentMethods = form.value.payment_methods?.map(pm => ({
|
||||
...pm,
|
||||
@@ -1898,6 +2045,46 @@ const submitForReview = async () => {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Supplier warning box with inline create button */
|
||||
.supplier-warning-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.supplier-warning-box small {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.supplier-create-btn {
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Label with action button (sync) */
|
||||
.label-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.label-with-action label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sync-btn {
|
||||
width: 1.5rem !important;
|
||||
height: 1.5rem !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.sync-btn .pi {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* 2-column layout */
|
||||
.receipt-form-layout {
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user