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:
365
data-entry-app/frontend/src/stores/receiptsStore.js
Normal file
365
data-entry-app/frontend/src/stores/receiptsStore.js
Normal 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
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user