Files
dealerhub/frontend/src/pages/Vouchers.vue
T

213 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-card shadow="never">
<div class="toolbar">
<el-input v-model="search" placeholder="搜索凭证号/摘要" style="width: 240px" clearable @keyup.enter="load" />
<el-select v-model="statusFilter" placeholder="状态" clearable style="width: 130px" @change="load">
<el-option label="草稿" value="draft" />
<el-option label="已过账" value="posted" />
<el-option label="已作废" value="cancelled" />
</el-select>
<el-button type="success" @click="openCreate">手工制单</el-button>
<el-button type="primary" @click="load">查询</el-button>
</div>
<el-table :data="rows" v-loading="loading" size="small" stripe @row-click="toggleExpand">
<el-table-column type="expand">
<template #default="{ row }">
<el-table :data="row.entries" size="small" style="margin: 4px 24px 14px">
<el-table-column prop="account_code" label="科目" width="100" />
<el-table-column prop="account_name" label="科目名称" min-width="130" />
<el-table-column prop="summary" label="摘要" min-width="180" />
<el-table-column label="借方" width="110">
<template #default="{ row: e }">{{ Number(e.debit) ? e.debit : '' }}</template>
</el-table-column>
<el-table-column label="贷方" width="110">
<template #default="{ row: e }">{{ Number(e.credit) ? e.credit : '' }}</template>
</el-table-column>
</el-table>
</template>
</el-table-column>
<el-table-column prop="bill_no" label="凭证号" width="140" />
<el-table-column prop="voucher_date" label="日期" width="110" />
<el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip />
<el-table-column label="借方合计" width="110">
<template #default="{ row }">¥ {{ row.total_debit }}</template>
</el-table-column>
<el-table-column label="贷方合计" width="110">
<template #default="{ row }">¥ {{ row.total_credit }}</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="statusTag(row.status)" effect="plain">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-button v-if="row.status === 'draft'" size="small" type="success" @click.stop="post(row)">过账</el-button>
<el-button v-if="row.status !== 'cancelled'" size="small" type="danger" plain @click.stop="cancel(row)">作废</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top: 14px"
layout="prev, pager, next, total"
:total="count"
:page-size="pageSize"
v-model:current-page="page"
@current-change="load"
/>
<!-- 手工制单 -->
<el-dialog v-model="dialogVisible" title="手工制单(借贷必须平衡)" width="680px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="凭证日期" prop="voucher_date">
<el-date-picker v-model="form.voucher_date" type="date" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="摘要" prop="summary"><el-input v-model="form.summary" /></el-form-item>
<el-form-item label="过账方式">
<el-switch v-model="form.auto_post" active-text="创建后立即过账" inactive-text="保存为草稿" />
</el-form-item>
</el-form>
<el-table :data="form.entries" size="small">
<el-table-column label="科目编码" width="140">
<template #default="{ row }"><el-input v-model="row.account_code" placeholder="1002" /></template>
</el-table-column>
<el-table-column label="摘要" min-width="150">
<template #default="{ row }"><el-input v-model="row.summary" /></template>
</el-table-column>
<el-table-column label="借方" width="140">
<template #default="{ row }"><el-input-number v-model="row.debit" :min="0" :precision="2" controls-position="right" style="width: 100%" /></template>
</el-table-column>
<el-table-column label="贷方" width="140">
<template #default="{ row }"><el-input-number v-model="row.credit" :min="0" :precision="2" controls-position="right" style="width: 100%" /></template>
</el-table-column>
<el-table-column width="60">
<template #default="$index"><el-button text type="danger" @click="form.entries.splice($index, 1)">删除</el-button></template>
</el-table-column>
</el-table>
<div style="margin-top: 10px">
<el-button size="small" @click="addEntry('debit')">+ 借方分录</el-button>
<el-button size="small" @click="addEntry('credit')">+ 贷方分录</el-button>
<span class="balance" :class="balanced ? 'ok' : 'bad'">
借方合计 {{ sumDebit }} | 贷方合计 {{ sumCredit }} {{ balanced ? '✓ 已平衡' : '✗ 未平衡' }}
</span>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="save">提交凭证</el-button>
</template>
</el-dialog>
</el-card>
</template>
<script setup>
import { onMounted, ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import api from '../api/client'
import { voucherEntryErrors } from '../utils/validate'
const formRef = ref(null)
const rules = {
voucher_date: [{ required: true, message: '请选择凭证日期', trigger: 'change' }],
summary: [{ required: true, message: '请输入摘要', trigger: 'blur' }],
}
const rows = ref([])
const count = ref(0)
const page = ref(1)
const pageSize = 20
const search = ref('')
const statusFilter = ref('')
const loading = ref(false)
const saving = ref(false)
const dialogVisible = ref(false)
const form = reactive({ voucher_date: '', summary: '', auto_post: true, entries: [] })
const sumDebit = computed(() => form.entries.reduce((s, e) => s + Number(e.debit || 0), 0))
const sumCredit = computed(() => form.entries.reduce((s, e) => s + Number(e.credit || 0), 0))
const balanced = computed(() => sumDebit.value > 0 && sumDebit.value === sumCredit.value)
const statusMap = { draft: '草稿', posted: '已过账', cancelled: '已作废' }
const statusText = (s) => statusMap[s] || s
const statusTag = (s) => (s === 'posted' ? 'success' : s === 'draft' ? 'warning' : 'info')
async function load() {
loading.value = true
try {
const data = await api.get('/finance/vouchers/', {
params: { page: page.value, page_size: pageSize, search: search.value, status: statusFilter.value || undefined },
})
rows.value = data.results || []
count.value = data.count || 0
} finally {
loading.value = false
}
}
function openCreate() {
const today = new Date().toISOString().slice(0, 10)
Object.assign(form, { voucher_date: today, summary: '', auto_post: true, entries: [] })
addEntry('debit')
addEntry('credit')
dialogVisible.value = true
}
function addEntry(side) {
form.entries.push({ account_code: '', summary: '', debit: 0, credit: 0, _side: side })
}
async function save() {
const ok = await formRef.value.validate().catch(() => false)
if (!ok) return
const entryErrors = voucherEntryErrors(form.entries)
if (entryErrors.length) {
ElMessage.error(entryErrors[0])
return
}
const payload = {
voucher_date: form.voucher_date,
summary: form.summary,
auto_post: form.auto_post,
entries: form.entries.map(({ account_code, summary, debit, credit }) => ({
account_code, summary, debit: String(debit), credit: String(credit),
})),
}
saving.value = true
try {
await api.post('/finance/vouchers/', payload)
ElMessage.success(form.auto_post ? '凭证已创建并过账' : '凭证草稿已保存')
dialogVisible.value = false
await load()
} finally {
saving.value = false
}
}
async function post(row) {
await api.post(`/finance/vouchers/${row.id}/post/`)
ElMessage.success(`凭证 ${row.bill_no} 已过账`)
await load()
}
async function cancel(row) {
await api.post(`/finance/vouchers/${row.id}/cancel/`)
ElMessage.success(`凭证 ${row.bill_no} 已作废`)
await load()
}
function toggleExpand() {}
onMounted(load)
</script>
<style scoped>
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; }
.balance { margin-left: 14px; font-size: 13px; }
.balance.ok { color: #67c23a; }
.balance.bad { color: #f56c6c; }
</style>