213 lines
8.5 KiB
Vue
213 lines
8.5 KiB
Vue
<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>
|