|
@@ -0,0 +1,224 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 发票「基本信息」行级开票金额公共逻辑。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 背景:一张发票可能对应多个项目/报告,原先只在发票详情里填一个总额,
|
|
|
|
|
+ * 盘点时无法知道金额如何分配。现在基本信息每行各自填写开票金额,
|
|
|
|
|
+ * 发票详情的「发票金额(元)」改为各行自动汇总(只读)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 历史数据兼容:迁移脚本只回填了「单条基本信息」的历史单据,
|
|
|
|
|
+ * 一票多项目的历史单据 base.account 仍为 NULL(无法推断拆分方式)。
|
|
|
|
|
+ * 这类单据在前端表现为「行级金额全空」,此时:
|
|
|
|
|
+ * - 不把主表金额改写为 0,保留数据库里的原值;
|
|
|
|
|
+ * - 允许手工修改主表金额(否则历史单据无法走审批/红冲);
|
|
|
|
|
+ * - 提交时不做行级合计校验,与后端 validateBaseAccount 的放行规则一致。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 金额累加统一换算成「分」(整数)后再运算,避免 0.1 + 0.2 的浮点误差。
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+/** 判断金额是否为空(null / undefined / 空串 / 仅空白都算空) */
|
|
|
|
|
+export function isBlankAmount (value) {
|
|
|
|
|
+ return value === null || value === undefined || String(value).trim() === ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 输入框实时清洗:只允许负号、数字和最多两位小数。
|
|
|
|
|
+ * 与会计端 cw/invoice/InvoiceFormTask.vue 的规则保持一致。
|
|
|
|
|
+ */
|
|
|
|
|
+export function sanitizeAmount (value) {
|
|
|
|
|
+ if (value === null || value === undefined) {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+ return String(value)
|
|
|
|
|
+ .replace(/[^-0-9.]/g, '')
|
|
|
|
|
+ .replace(/^\./g, '')
|
|
|
|
|
+ .replace(/\.{2,}/g, '.')
|
|
|
|
|
+ .replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 把金额字符串换算成整数「分」。无法解析或为空时返回 null。
|
|
|
|
|
+ */
|
|
|
|
|
+export function toCents (value) {
|
|
|
|
|
+ if (isBlankAmount(value)) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+ const num = Number(String(value).trim())
|
|
|
|
|
+ if (!isFinite(num)) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+ return Math.round(num * 100)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 整数「分」格式化成两位小数的金额字符串 */
|
|
|
|
|
+export function formatCents (cents) {
|
|
|
|
|
+ return (cents / 100).toFixed(2)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 汇总基本信息各行的开票金额。
|
|
|
|
|
+ * @returns {{filledCount:number, blankCount:number, totalCents:number, anyFilled:boolean}}
|
|
|
|
|
+ * totalCents 只累加已填写的行;anyFilled=false 表示整票未拆分(历史单据或尚未录入)。
|
|
|
|
|
+ */
|
|
|
|
|
+export function sumBaseAccount (baseList) {
|
|
|
|
|
+ const result = { filledCount: 0, blankCount: 0, totalCents: 0, anyFilled: false }
|
|
|
|
|
+ if (!Array.isArray(baseList)) {
|
|
|
|
|
+ return result
|
|
|
|
|
+ }
|
|
|
|
|
+ baseList.forEach(row => {
|
|
|
|
|
+ const cents = toCents(row && row.account)
|
|
|
|
|
+ if (cents === null) {
|
|
|
|
|
+ result.blankCount++
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.filledCount++
|
|
|
|
|
+ result.anyFilled = true
|
|
|
|
|
+ result.totalCents += cents
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ return result
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 提交前校验行级金额。返回错误提示字符串,校验通过返回 null。
|
|
|
|
|
+ * 规则与后端 FinanceInvoiceService.validateBaseAccount 完全对应:
|
|
|
|
|
+ * 全空 -> 放行(历史未拆分单据);有任意一行填写 -> 每行必填且合计等于主表金额。
|
|
|
|
|
+ */
|
|
|
|
|
+export function validateBaseAccount (baseList, totalAccount) {
|
|
|
|
|
+ if (!Array.isArray(baseList) || baseList.length === 0) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+ const summary = sumBaseAccount(baseList)
|
|
|
|
|
+ if (!summary.anyFilled) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+ for (let i = 0; i < baseList.length; i++) {
|
|
|
|
|
+ const raw = baseList[i] && baseList[i].account
|
|
|
|
|
+ if (isBlankAmount(raw)) {
|
|
|
|
|
+ return '基本信息第 ' + (i + 1) + ' 行的开票金额未填写,请补全后再提交'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (toCents(raw) === null) {
|
|
|
|
|
+ return '基本信息第 ' + (i + 1) + ' 行的开票金额格式不正确:' + raw
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const totalCents = toCents(totalAccount)
|
|
|
|
|
+ if (totalCents === null) {
|
|
|
|
|
+ return '发票金额不能为空'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (summary.totalCents !== totalCents) {
|
|
|
|
|
+ return '基本信息各行开票金额合计 ' + formatCents(summary.totalCents) +
|
|
|
|
|
+ ' 与发票金额 ' + formatCents(totalCents) + ' 不一致,请检查'
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 供 finance/invoice 下三个发票表单复用的 mixin。
|
|
|
|
|
+ * 宿主组件需提供 inputForm.financeInvoiceBaseDTOList / inputForm.account / inputForm.redInvoiceFlag。
|
|
|
|
|
+ */
|
|
|
|
|
+export const invoiceBaseAccountMixin = {
|
|
|
|
|
+ data () {
|
|
|
|
|
+ return {
|
|
|
|
|
+ // vxe-table 行级必填校验
|
|
|
|
|
+ baseAccountRules: {
|
|
|
|
|
+ account: [{ required: true, message: '请填写开票金额' }]
|
|
|
|
|
+ },
|
|
|
|
|
+ // 行级金额是否全部为空且已有基本信息(历史一票多项目未拆分)
|
|
|
|
|
+ baseAccountUnsplit: false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 主表「发票金额(元)」是否只读。
|
|
|
|
|
+ * 历史未拆分单据、以及查看/审批态下表单本身已 disabled,这里只控制录入态。
|
|
|
|
|
+ */
|
|
|
|
|
+ accountTotalReadonly () {
|
|
|
|
|
+ return !this.baseAccountUnsplit
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 基本信息金额为空时的悬浮提示 */
|
|
|
|
|
+ baseAccountBlankHint () {
|
|
|
|
|
+ return this.baseAccountUnsplit
|
|
|
|
|
+ ? '历史一票多项目单据未拆分金额,发票金额以「发票详情」中的总额为准'
|
|
|
|
|
+ : '请输入该行开票金额'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ 'inputForm.financeInvoiceBaseDTOList': {
|
|
|
|
|
+ deep: true,
|
|
|
|
|
+ handler () {
|
|
|
|
|
+ this.syncBaseAccountTotal()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ /** 基本信息金额输入框的实时清洗 */
|
|
|
|
|
+ onBaseAccountInput (row, value) {
|
|
|
|
|
+ row.account = sanitizeAmount(value)
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 把各行金额汇总写回主表 account。
|
|
|
|
|
+ * 全空时不动主表值(保留历史金额 / 保留新增单据的空值),只更新未拆分标记。
|
|
|
|
|
+ */
|
|
|
|
|
+ syncBaseAccountTotal () {
|
|
|
|
|
+ const baseList = this.inputForm.financeInvoiceBaseDTOList
|
|
|
|
|
+ const summary = sumBaseAccount(baseList)
|
|
|
|
|
+ this.baseAccountUnsplit = Array.isArray(baseList) && baseList.length > 0 && !summary.anyFilled
|
|
|
|
|
+ if (!summary.anyFilled) {
|
|
|
|
|
+ // 新建单据尚未录入金额时清空合计;历史未拆分单据保留主表原值,也不篡改红字发票标记
|
|
|
|
|
+ if (this.baseAccountUnsplit && this.commonJS.isEmpty(this.inputForm.id)) {
|
|
|
|
|
+ this.inputForm.account = ''
|
|
|
|
|
+ }
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.refreshBaseFooter()
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.inputForm.account = formatCents(summary.totalCents)
|
|
|
|
|
+ this.inputForm.redInvoiceFlag = summary.totalCents < 0 ? '1' : '0'
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.refreshBaseFooter()
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ /** vxe-table 表脚不会因单元格编辑自动重算,编辑结束后手动刷新 */
|
|
|
|
|
+ refreshBaseFooter () {
|
|
|
|
|
+ const table = this.$refs.baseTable
|
|
|
|
|
+ if (table && typeof table.updateFooter === 'function') {
|
|
|
|
|
+ table.updateFooter()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 基本信息表格表脚:首列显示标题,金额列显示合计。
|
|
|
|
|
+ * 历史未拆分单据合计显示 '-',避免误认为金额是 0。
|
|
|
|
|
+ */
|
|
|
|
|
+ baseAccountFooter ({ columns, data }) {
|
|
|
|
|
+ const summary = sumBaseAccount(data)
|
|
|
|
|
+ return [
|
|
|
|
|
+ columns.map((column, columnIndex) => {
|
|
|
|
|
+ if (columnIndex === 0) {
|
|
|
|
|
+ return '开票金额汇总(元)'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (column.property === 'account') {
|
|
|
|
|
+ if (!summary.anyFilled) {
|
|
|
|
|
+ return data && data.length > 0 ? '未拆分' : ''
|
|
|
|
|
+ }
|
|
|
|
|
+ return formatCents(summary.totalCents)
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+ })
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 提交前校验,失败时弹出提示并返回 false */
|
|
|
|
|
+ checkBaseAccount () {
|
|
|
|
|
+ const message = validateBaseAccount(
|
|
|
|
|
+ this.inputForm.financeInvoiceBaseDTOList,
|
|
|
|
|
+ this.inputForm.account
|
|
|
|
|
+ )
|
|
|
|
|
+ if (message) {
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ this.$message.error(message)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default invoiceBaseAccountMixin
|