feat: Add data-entry-app for fiscal receipts with approval workflow

New application for entering fiscal receipts (bonuri fiscale) with:

Backend (FastAPI + SQLModel + Alembic):
- Receipt, ReceiptAttachment, AccountingEntry models
- CRUD operations with async SQLite database
- Workflow: DRAFT → PENDING_REVIEW → APPROVED/REJECTED
- Auto-generation of accounting entries with VAT calculation
- File upload support (images, PDFs)
- Predefined expense types (Fuel, Materials, Office, etc.)
- Nomenclature service for partners, accounts, cash registers

Frontend (Vue.js 3 + PrimeVue + Pinia):
- ReceiptsListView with filters and stats
- ReceiptCreateView with image upload
- ReceiptDetailView with accounting entries
- ReceiptApprovalView for accountant approval

Documentation:
- REQUIREMENTS.md with functional specifications
- ARCHITECTURE.md with technical decisions
- CLAUDE.md for AI assistant guidance

Phase 1 MVP uses SQLite, prepared for Oracle integration in Phase 2.

🤖 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-11 17:30:51 +02:00
parent 5823cedb94
commit 21c12ddb0f
45 changed files with 7524 additions and 0 deletions

View File

@@ -0,0 +1,365 @@
import { defineStore } from 'pinia'
import axios from 'axios'
const api = axios.create({
baseURL: '/api/receipts',
headers: {
'Content-Type': 'application/json',
},
})
export const useReceiptsStore = defineStore('receipts', {
state: () => ({
receipts: [],
currentReceipt: null,
pendingReceipts: [],
stats: null,
loading: false,
error: null,
pagination: {
page: 1,
pageSize: 20,
total: 0,
pages: 1,
},
filters: {
status: null,
search: '',
dateFrom: null,
dateTo: null,
},
// Nomenclatures
partners: [],
accounts: [],
cashRegisters: [],
expenseTypes: [],
}),
getters: {
hasReceipts: (state) => state.receipts.length > 0,
hasPendingReceipts: (state) => state.pendingReceipts.length > 0,
pendingCount: (state) => state.pendingReceipts.length,
},
actions: {
// ============ Receipts CRUD ============
async fetchReceipts() {
this.loading = true
this.error = null
try {
const params = {
page: this.pagination.page,
page_size: this.pagination.pageSize,
}
if (this.filters.status) {
params.status = this.filters.status
}
if (this.filters.search) {
params.search = this.filters.search
}
if (this.filters.dateFrom) {
params.date_from = this.filters.dateFrom
}
if (this.filters.dateTo) {
params.date_to = this.filters.dateTo
}
const response = await api.get('/', { params })
this.receipts = response.data.items
this.pagination.total = response.data.total
this.pagination.pages = response.data.pages
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to fetch receipts'
throw error
} finally {
this.loading = false
}
},
async fetchReceiptById(id) {
this.loading = true
this.error = null
try {
const response = await api.get(`/${id}`)
this.currentReceipt = response.data
return response.data
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to fetch receipt'
throw error
} finally {
this.loading = false
}
},
async createReceipt(data) {
this.loading = true
this.error = null
try {
const response = await api.post('/', data)
return response.data
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to create receipt'
throw error
} finally {
this.loading = false
}
},
async updateReceipt(id, data) {
this.loading = true
this.error = null
try {
const response = await api.put(`/${id}`, data)
return response.data
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to update receipt'
throw error
} finally {
this.loading = false
}
},
async deleteReceipt(id) {
this.loading = true
this.error = null
try {
await api.delete(`/${id}`)
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to delete receipt'
throw error
} finally {
this.loading = false
}
},
// ============ Workflow Actions ============
async submitReceipt(id) {
this.loading = true
this.error = null
try {
const response = await api.post(`/${id}/submit`)
return response.data
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to submit receipt'
throw error
} finally {
this.loading = false
}
},
async approveReceipt(id) {
this.loading = true
this.error = null
try {
const response = await api.post(`/${id}/approve`)
return response.data
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to approve receipt'
throw error
} finally {
this.loading = false
}
},
async rejectReceipt(id, reason) {
this.loading = true
this.error = null
try {
const response = await api.post(`/${id}/reject`, { reason })
return response.data
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to reject receipt'
throw error
} finally {
this.loading = false
}
},
async resubmitReceipt(id) {
this.loading = true
this.error = null
try {
const response = await api.post(`/${id}/resubmit`)
return response.data
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to resubmit receipt'
throw error
} finally {
this.loading = false
}
},
// ============ Pending Receipts ============
async fetchPendingReceipts() {
this.loading = true
this.error = null
try {
const response = await api.get('/pending')
this.pendingReceipts = response.data
return response.data
} catch (error) {
this.error = error.response?.data?.detail || 'Failed to fetch pending receipts'
throw error
} finally {
this.loading = false
}
},
// ============ Attachments ============
async uploadAttachment(receiptId, file) {
const formData = new FormData()
formData.append('file', file)
try {
const response = await api.post(`/${receiptId}/attachments`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || 'Failed to upload attachment')
}
},
async deleteAttachment(attachmentId) {
try {
await api.delete(`/attachments/${attachmentId}`)
} catch (error) {
throw new Error(error.response?.data?.detail || 'Failed to delete attachment')
}
},
getAttachmentUrl(attachmentId) {
return `/api/receipts/attachments/${attachmentId}/download`
},
// ============ Accounting Entries ============
async fetchEntries(receiptId) {
try {
const response = await api.get(`/${receiptId}/entries`)
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || 'Failed to fetch entries')
}
},
async updateEntries(receiptId, entries) {
try {
const response = await api.put(`/${receiptId}/entries`, { entries })
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || 'Failed to update entries')
}
},
async regenerateEntries(receiptId) {
try {
const response = await api.post(`/${receiptId}/entries/regenerate`)
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || 'Failed to regenerate entries')
}
},
// ============ Nomenclatures ============
async fetchPartners(search = '') {
try {
const response = await api.get('/nomenclature/partners', {
params: { search },
})
this.partners = response.data
return response.data
} catch (error) {
console.error('Failed to fetch partners:', error)
return []
}
},
async fetchAccounts(prefix = '') {
try {
const response = await api.get('/nomenclature/accounts', {
params: { prefix },
})
this.accounts = response.data
return response.data
} catch (error) {
console.error('Failed to fetch accounts:', error)
return []
}
},
async fetchCashRegisters() {
try {
const response = await api.get('/nomenclature/cash-registers')
this.cashRegisters = response.data
return response.data
} catch (error) {
console.error('Failed to fetch cash registers:', error)
return []
}
},
async fetchExpenseTypes() {
try {
const response = await api.get('/nomenclature/expense-types')
this.expenseTypes = response.data
return response.data
} catch (error) {
console.error('Failed to fetch expense types:', error)
return []
}
},
async fetchAllNomenclatures() {
await Promise.all([
this.fetchPartners(),
this.fetchCashRegisters(),
this.fetchExpenseTypes(),
])
},
// ============ Stats ============
async fetchStats() {
try {
const response = await api.get('/stats')
this.stats = response.data
return response.data
} catch (error) {
console.error('Failed to fetch stats:', error)
return null
}
},
// ============ Filters & Pagination ============
setFilters(filters) {
this.filters = { ...this.filters, ...filters }
this.pagination.page = 1
},
clearFilters() {
this.filters = {
status: null,
search: '',
dateFrom: null,
dateTo: null,
}
this.pagination.page = 1
},
setPage(page) {
this.pagination.page = page
},
clearCurrentReceipt() {
this.currentReceipt = null
},
},
})