Browse Source

评估-开票项目添加开票金额 以及历史数据处理

徐滕 4 days atrás
parent
commit
97efb9659d

+ 224 - 0
src/utils/invoiceBaseAccount.js

@@ -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

+ 28 - 3
src/views/finance/invoice/InvoiceForm.vue

@@ -16,7 +16,8 @@
 					<el-col :span="24">
 						<vxe-table border show-overflow show-footer ref="baseTable" class="vxe-table-element"
 							:data="inputForm.financeInvoiceBaseDTOList" style="margin-left: 5em" @cell-click=""
-							@edit-closed="" highlight-current-row
+							@edit-closed="syncBaseAccountTotal" highlight-current-row
+							:footer-method="baseAccountFooter" :edit-rules="baseAccountRules"
 							:edit-config="{ trigger: 'click', mode: 'row', showStatus: true, autoClear: true, icon: '#' }">
 							<vxe-table-column field="programName" title="项目名称" :edit-render="{}" show-overflow="title">
 								<template v-slot:edit="scope">
@@ -40,6 +41,17 @@
 									<el-input :readonly="true" placeholder="请填写委托方" v-model="scope.row.clientName" />
 								</template>
 							</vxe-table-column>
+							<vxe-table-column field="account" title="开票金额(元)" :edit-render="{}" show-overflow="title"
+								min-width="160">
+								<template v-slot:edit="scope">
+									<el-input v-model="scope.row.account" placeholder="请输入该行开票金额"
+										@input="onBaseAccountInput(scope.row, scope.row.account)"
+										@blur="syncBaseAccountTotal" clearable />
+								</template>
+								<template v-slot="scope">
+									<span :title="baseAccountBlankHint">{{ commonJS.isEmpty(scope.row.account) ? '-' : scope.row.account }}</span>
+								</template>
+							</vxe-table-column>
 							<!--            <vxe-table-column field="reportNo" title="报告号" :edit-render="{}">-->
 							<!--              <template v-slot:edit="scope">-->
 							<!--                <el-input v-model="scope.row.reportNo" placeholder="请填写报告号"/>-->
@@ -263,9 +275,16 @@
 							{ required: true, message: '发票金额不能为空', trigger: 'blur' }
 						]">
 							<el-input-number v-model="inputForm.account" controls-position="right" :controls="false"
-								style="width:100%;" :max="999999999999999" :precision="2" placeholder="请填写发票金额"
-								:step="0.01" clearable>
+								style="width:100%;" :max="999999999999999" :precision="2"
+								:placeholder="accountTotalReadonly ? '由基本信息各行开票金额自动汇总' : '请填写发票金额'"
+								:disabled="accountTotalReadonly" :step="0.01" clearable>
 							</el-input-number>
+							<div v-if="baseAccountUnsplit" style="color: #E6A23C; font-size: 12px; line-height: 18px;">
+								历史一票多项目单据未拆分金额,发票金额以「发票详情」中的总额为准
+							</div>
+<!--							<div v-else-if="!commonJS.isEmpty(inputForm.financeInvoiceBaseDTOList)" style="color: #909399; font-size: 12px; line-height: 18px;">
+								已按基本信息各行开票金额自动汇总,如需调整请修改对应行金额
+							</div>-->
 						</el-form-item>
 					</el-col>
 
@@ -665,7 +684,9 @@ import SelectUserTree from '@/views/utils/treeUserSelect'
 import SelectTree from '@/components/treeSelect/treeSelect.vue'
 import UserSelect1 from '@/views/utils/UserTreeSelect'
 import UserSelect2 from '@/views/utils/UserTreeSelect'
+import { invoiceBaseAccountMixin } from '@/utils/invoiceBaseAccount'
 export default {
+	mixins: [invoiceBaseAccountMixin],
 	data() {
 		return {
 			title: '',
@@ -880,6 +901,10 @@ export default {
 					return
 				}
 			}
+			// 基本信息各行开票金额校验(全空的历史未拆分单据放行,与后端 validateBaseAccount 一致)
+			if (!this.checkBaseAccount()) {
+				return
+			}
 			let acc = 0
 			this.inputForm.financeInvoiceDetailDTOList.forEach((item, index) => {
 				if (this.commonJS.isEmpty(item.number)) {

+ 32 - 5
src/views/finance/invoice/InvoiceFormTask.vue

@@ -15,7 +15,8 @@
 				<el-col :span="24">
 					<vxe-table border show-overflow show-footer ref="baseTable" :key="baseKey" class="vxe-table-element"
 						:data="inputForm.financeInvoiceBaseDTOList" :style="{ marginLeft: isMobile ? '0' : '5em' }"
-						highlight-current-row
+						highlight-current-row :footer-method="baseAccountFooter" :edit-rules="baseAccountRules"
+						@edit-closed="syncBaseAccountTotal"
 						:edit-config="{ trigger: 'click', mode: 'cell', showStatus: true, autoClear: true }">
 						<vxe-table-column field="programName" title="项目名称" :edit-render="{}" show-overflow="title"
 							:min-width="isMobile ? '200' : ''">
@@ -49,6 +50,17 @@
 								<el-input :readonly="true" placeholder="请填写委托方" v-model="scope.row.clientName" />
 							</template>
 						</vxe-table-column>
+						<vxe-table-column field="account" title="开票金额(元)" :edit-render="{}" show-overflow="title"
+							:min-width="isMobile ? '200' : '160'">
+							<template v-slot:edit="scope">
+								<el-input v-model="scope.row.account" placeholder="请输入该行开票金额"
+									@input="onBaseAccountInput(scope.row, scope.row.account)"
+									@blur="syncBaseAccountTotal" clearable />
+							</template>
+							<template v-slot="scope">
+								<span :title="baseAccountBlankHint">{{ commonJS.isEmpty(scope.row.account) ? '-' : scope.row.account }}</span>
+							</template>
+						</vxe-table-column>
 						<!--            <vxe-table-column field="reportNo" title="报告号" :edit-render="{}">-->
 						<!--              <template v-slot:edit="scope">-->
 						<!--                <el-input v-model="scope.row.reportNo" placeholder="请填写报告号"/>-->
@@ -273,9 +285,15 @@
 					<el-form-item label="发票金额(元)" prop="account" :rules="[
 						{ required: true, message: '发票金额不能为空', trigger: 'blur' }
 					]">
-						<el-input @input="fixedAccount" placeholder="请填写发票金额" maxlength="15"
-							v-model="inputForm.account">
+						<el-input @input="fixedAccount" placeholder="由基本信息各行开票金额自动汇总" maxlength="15"
+							:readonly="accountTotalReadonly" v-model="inputForm.account">
 						</el-input>
+						<div v-if="baseAccountUnsplit" style="color: #E6A23C; font-size: 12px; line-height: 18px;">
+							历史一票多项目单据未拆分金额,发票金额以「发票详情」中的总额为准
+						</div>
+<!--						<div v-else-if="!commonJS.isEmpty(inputForm.financeInvoiceBaseDTOList)" style="color: #909399; font-size: 12px; line-height: 18px;">
+							已按基本信息各行开票金额自动汇总,如需调整请修改对应行金额
+						</div>-->
 						<!--            <el-input-number-->
 						<!--              v-model="inputForm.account"-->
 						<!--              controls-position="right"-->
@@ -762,7 +780,9 @@ import userService from "../../../api/sys/userService";
 import UserSelect1 from '@/views/utils/UserTreeSelect'
 import UserSelect2 from '@/views/utils/UserTreeSelect'
 import WorkInvoiceForm from './WorkInvoiceChooseRadio'
+import { invoiceBaseAccountMixin } from '@/utils/invoiceBaseAccount'
 export default {
+	mixins: [invoiceBaseAccountMixin],
 	props: {
 		businessId: {
 			type: String,
@@ -1122,7 +1142,10 @@ export default {
 						// 恢复
 						this.inputForm.financeInvoiceBaseDTOList.forEach(item => {
 							item.id = ""
-							item.account = -Math.abs(item.account);
+							// 历史未拆分单据行级金额为空,保持为空,避免 -Math.abs(null) 变成 0
+							if (!this.commonJS.isEmpty(item.account)) {
+								item.account = -Math.abs(item.account);
+							}
 						})
 						this.inputForm.workAttachmentDtoList.forEach(item => {
 							item.id = ""
@@ -1216,7 +1239,7 @@ export default {
 			} else if (status === 'reapply') {
 				this.inputForm.status = '2'
 			}
-			if (this.inputForm.billingType === 1) {
+			if (this.inputForm.billingType == '1') {
 				if (this.commonJS.isEmpty(this.inputForm.financeInvoiceBaseDTOList)) {
 					this.loading = false
 					this.$message.error('至少新增一条基本信息')
@@ -1267,6 +1290,10 @@ export default {
 					}
 				}
 			}
+			// 基本信息各行开票金额校验(全空的历史未拆分单据放行,与后端 validateBaseAccount 一致)
+			if (!this.checkBaseAccount()) {
+				throw new Error()
+			}
 			if (this.inputForm.status === '5') {
 				if (
 					this.inputForm.isOmsBilling == 1 ||

+ 8 - 0
src/views/finance/invoice/InvoiceFormTaskInvalid.vue

@@ -21,6 +21,7 @@
 				  @cell-click=""
 				  @edit-closed=""
 				  highlight-current-row
+				  :footer-method="baseAccountFooter"
 				  :edit-config="{trigger: 'click', mode: 'cell', showStatus: true, autoClear: true}"
 			  >
 				  <vxe-table-column field="programName" title="项目名称" :edit-render="{}">
@@ -43,6 +44,11 @@
 						  <el-input :readonly="true" placeholder="请填写委托方" v-model="scope.row.clientName"/>
 					  </template>
 				  </vxe-table-column>
+				  <vxe-table-column field="account" title="开票金额(元)" min-width="160">
+					  <template v-slot="scope">
+						  <span :title="baseAccountBlankHint">{{ commonJS.isEmpty(scope.row.account) ? '-' : scope.row.account }}</span>
+					  </template>
+				  </vxe-table-column>
 				  <!--            <vxe-table-column field="reportNo" title="报告号" :edit-render="{}">-->
 				  <!--              <template v-slot:edit="scope">-->
 				  <!--                <el-input v-model="scope.row.reportNo" placeholder="请填写报告号"/>-->
@@ -573,7 +579,9 @@
   import WorkClientForm from '@/views/program/registered/WorkClientForm'
   import SelectUserTree from '@/views/utils/treeUserSelect'
   import SelectTree from '@/components/treeSelect/treeSelect.vue'
+  import { invoiceBaseAccountMixin } from '@/utils/invoiceBaseAccount'
   export default {
+    mixins: [invoiceBaseAccountMixin],
     props: {
       businessId: {
         type: String,

+ 11 - 0
src/views/flowable/task/HistoryList.vue

@@ -31,6 +31,9 @@
 					</template>
 				</el-input>
 			</el-form-item>
+			<el-form-item v-if="$store.state.user.company.name === '江苏兴光资产评估有限公司'" label="关联项" prop="relatedNo">
+				<el-input v-model="searchForm.relatedNo" placeholder="报告号/合同号/报销单号" clearable></el-input>
+			</el-form-item>
 			<el-form-item>
 				<el-button type="primary" @click="refreshList()" icon="search"
 					>查询</el-button
@@ -111,6 +114,13 @@
 					></vxe-column>
 					<vxe-column field="endTime" title="完成时间"> </vxe-column>
 					<vxe-column
+						v-if="$store.state.user.company.name === '江苏兴光资产评估有限公司'"
+						field="relatedNo"
+						title="关联项"
+						min-width="160"
+					>
+					</vxe-column>
+					<vxe-column
 						title="操作"
 						width="100px"
 						fixed="right"
@@ -172,6 +182,7 @@ export default {
 				endDate: "",
 				title: "",
 				assigneeId: "",
+				relatedNo: "",
 			},
 			searchVisible: true,
 			searchDates: "",

+ 5 - 5
src/views/program/registered/ProjectList.vue

@@ -459,7 +459,7 @@
 		<!--			  </div>-->
 		<!--		  </template>-->
 		<!--	  </el-dialog>-->
-		<!-- 省级备案导入对话框 -->
+		<!-- 报告备案导入对话框 -->
 		<el-dialog v-model="importDialogVisible" title="省级项目备案数据导入" width="500" :close-on-click-modal="false">
 			<el-upload
 				ref="importUpload"
@@ -2130,7 +2130,7 @@ export default {
 			console.log(user[0].name)
 			this.searchForm.createById = user[0].name
 		},
-		// 省级备案导入 - 文件选择
+		// 报告备案导入 - 文件选择
 		handleImportFileChange(file) {
 			const fileName = file.name.toLowerCase()
 			if (!fileName.endsWith('.xls') && !fileName.endsWith('.xlsx')) {
@@ -2141,11 +2141,11 @@ export default {
 			}
 			this.importFileList = [file]
 		},
-		// 省级备案导入 - 文件移除
+		// 报告备案导入 - 文件移除
 		handleImportFileRemove() {
 			this.importFileList = []
 		},
-		// 省级备案导入 - 提交
+		// 报告备案导入 - 提交
 		submitImport() {
 			if (this.importFileList.length === 0) {
 				this.$message.warning('请先选择文件')
@@ -2165,7 +2165,7 @@ export default {
 				const url = window.URL.createObjectURL(blob)
 				const link = document.createElement('a')
 				link.href = url
-				link.download = '省级备案数据_' + file.name
+				link.download = '报告备案数据_' + file.name
 				link.click()
 				window.URL.revokeObjectURL(url)
 				this.$message.success('导入成功')