feat: 付款申请创建采购付款分栏与财务审核权限、工资表银行账号与卡号识别银行、采购台账移除采购油品
| | |
| | | <template #operation="{ row }"> |
| | | <el-button type="primary" link @click="view(row)">查看</el-button> |
| | | <el-button type="primary" link @click="edit(row)" v-if="isPendingStatus(row.status)">编辑</el-button> |
| | | <el-button type="success" link @click="handleAudit(row)" v-if="isPendingStatus(row.status)">审核</el-button> |
| | | <el-button type="success" link :disabled="!canAudit(row)" @click="handleAudit(row)" v-if="isPendingStatus(row.status)">审核</el-button> |
| | | <el-button type="warning" link @click="openPaymentDialog(row)" v-if="isApprovedStatus(row.status)">付款</el-button> |
| | | <el-button type="danger" link @click="handleDelete(row)" v-if="isPendingStatus(row.status)">删除</el-button> |
| | | </template> |
| | |
| | | <el-form-item label="备注" prop="remark"> |
| | | <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" :disabled="isView" /> |
| | | </el-form-item> |
| | | <div class="form-section-title">创建采购付款</div> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="款项用途" prop="paymentPurpose"> |
| | | <el-select v-model="form.paymentPurpose" placeholder="请选择款项用途" clearable style="width: 100%" :disabled="isView"> |
| | | <el-option label="采购货款" value="采购货款" /> |
| | | <el-option label="运费" value="运费" /> |
| | | <el-option label="押金" value="押金" /> |
| | | <el-option label="服务费" value="服务费" /> |
| | | <el-option label="其他" value="其他" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="项目名称" prop="projectName"> |
| | | <el-input v-model="form.projectName" placeholder="请输入项目名称" clearable :disabled="isView" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="经办人" prop="operatorName"> |
| | | <el-input v-model="form.operatorName" placeholder="请输入经办人" clearable :disabled="isView" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="财务审核" prop="financeAuditorId"> |
| | | <el-select |
| | | v-model="form.financeAuditorId" |
| | | placeholder="请选择审核人员" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | :disabled="isView" |
| | | @change="handleFinanceAuditorChange" |
| | | > |
| | | <el-option |
| | | v-for="item in userList" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template v-if="!isView" #footer> |
| | | <el-button type="primary" :loading="submitLoading" @click="submitForm">确定</el-button> |
| | |
| | | deleteAccountPaymentApplication, |
| | | } from "@/api/financialManagement/accountPaymentApplication.js"; |
| | | import { addAccountPurchasePayment } from "@/api/financialManagement/accountPurchasePayment.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | |
| | | defineOptions({ |
| | | name: "付款申请", |
| | |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const { checkout_payment } = proxy.useDict("checkout_payment"); |
| | | const userStore = useUserStore(); |
| | | |
| | | const filters = reactive({ |
| | | invoiceApplicationNo: "", |
| | |
| | | { label: "付款金额", prop: "amount", dataType: "slot", slot: "amount" }, |
| | | { label: "付款方式", prop: "paymentMethod", dataType: "slot", slot: "paymentMethod", width: "120" }, |
| | | { label: "申请日期", prop: "applyDate", width: "120" }, |
| | | { label: "款项用途", prop: "paymentPurpose", width: "120" }, |
| | | { label: "财务审核", prop: "financeAuditor", width: "120" }, |
| | | { label: "状态", prop: "status", dataType: "slot", slot: "status", width: "100" }, |
| | | { label: "操作", prop: "operation", dataType: "slot", slot: "operation", width: "260", fixed: "right" }, |
| | | ]; |
| | |
| | | const submitLoading = ref(false); |
| | | const currentId = ref(null); |
| | | const supplierList = ref([]); |
| | | const userList = ref([]); |
| | | |
| | | const inboundBatchList = ref([]); |
| | | const inboundBatchOptions = ref([]); |
| | |
| | | inboundBatches: "", |
| | | status: 0, |
| | | createTime: "", |
| | | paymentPurpose: "", |
| | | projectName: "", |
| | | operatorName: "", |
| | | financeAuditor: "", |
| | | financeAuditorId: null, |
| | | }); |
| | | const formCreateTimeDate = computed({ |
| | | get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""), |
| | |
| | | paymentAmount: [{ required: true, message: "请输入付款金额", trigger: "blur" }], |
| | | paymentMethod: [{ required: true, message: "请选择付款方式", trigger: "change" }], |
| | | applyDate: [{ required: true, message: "请选择申请日期", trigger: "change" }], |
| | | paymentPurpose: [{ required: true, message: "请选择款项用途", trigger: "change" }], |
| | | projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }], |
| | | operatorName: [{ required: true, message: "请输入经办人", trigger: "blur" }], |
| | | financeAuditorId: [{ required: true, message: "请选择财务审核人员", trigger: "change" }], |
| | | }; |
| | | |
| | | const getSummaries = ({ columns, data }) => { |
| | |
| | | }; |
| | | |
| | | const isPendingStatus = (status) => normalizeStatus(status) === 0; |
| | | |
| | | // 审核权限:指定了财务审核人时仅本人可审核;未指定时保持原逻辑(谁都能审) |
| | | const canAudit = (row) => { |
| | | const auditorId = row.financeAuditorId; |
| | | if (auditorId == null || auditorId === "") { |
| | | return true; |
| | | } |
| | | return Number(userStore.id) === Number(auditorId); |
| | | }; |
| | | |
| | | const isApprovedStatus = (status) => normalizeStatus(status) === 1; |
| | | |
| | |
| | | |
| | | const fillFormFromRow = (row) => { |
| | | const stockInRecordIds = parseStockInRecordIds(row.stockInRecordIds); |
| | | // 审核人已不在用户列表时,用已保存的姓名补进下拉,保证查看/编辑显示名字而非裸ID |
| | | const auditorId = row.financeAuditorId ?? null; |
| | | if (auditorId != null && !userList.value.some((u) => u.userId === auditorId)) { |
| | | userList.value.push({ userId: auditorId, nickName: row.financeAuditor ?? `用户${auditorId}` }); |
| | | } |
| | | Object.assign(form, { |
| | | invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "", |
| | | supplierId: row.supplierId, |
| | |
| | | inboundBatches: formatInboundBatches(row.inboundBatches), |
| | | status: normalizeStatus(row.status), |
| | | createTime: row.createTime ?? "", |
| | | paymentPurpose: row.paymentPurpose ?? "", |
| | | projectName: row.projectName ?? "", |
| | | operatorName: row.operatorName ?? "", |
| | | financeAuditor: row.financeAuditor ?? "", |
| | | financeAuditorId: row.financeAuditorId ?? null, |
| | | }); |
| | | }; |
| | | |
| | |
| | | status: 0, |
| | | paymentAmount: form.paymentAmount, |
| | | createTime: form.createTime, |
| | | paymentPurpose: form.paymentPurpose || "", |
| | | projectName: form.projectName || "", |
| | | operatorName: form.operatorName || "", |
| | | financeAuditor: form.financeAuditor || "", |
| | | financeAuditorId: form.financeAuditorId ?? null, |
| | | }; |
| | | if (forUpdate) { |
| | | payload.id = currentId.value; |
| | |
| | | if (res.code === 200) { |
| | | supplierList.value = res.data ?? []; |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 财务审核人员选择:绑定系统用户,选中后带出姓名 |
| | | const handleFinanceAuditorChange = (userId) => { |
| | | const user = userList.value.find((item) => item.userId === userId); |
| | | form.financeAuditor = user?.nickName ?? ""; |
| | | }; |
| | | |
| | | const getUserList = () => { |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data ?? []; |
| | | }); |
| | | }; |
| | | |
| | |
| | | inboundBatches: "", |
| | | status: 0, |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | paymentPurpose: "", |
| | | projectName: "", |
| | | operatorName: "", |
| | | financeAuditor: "", |
| | | financeAuditorId: null, |
| | | }); |
| | | inboundBatchList.value = []; |
| | | inboundBatchOptions.value = []; |
| | |
| | | |
| | | onMounted(() => { |
| | | getSupplierList(); |
| | | getUserList(); |
| | | getTableData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .form-section-title { |
| | | margin: 18px 0 14px; |
| | | padding: 8px 14px; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #1d6fd8; |
| | | background: linear-gradient(90deg, rgba(64, 158, 255, 0.14), rgba(64, 158, 255, 0.02)); |
| | | border-left: 4px solid #409eff; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | .actions { |
| | | display: flex; |
| | | justify-content: space-between; |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="银行账号" |
| | | prop="bankAccount"> |
| | | <el-input v-model="form.bankAccount" |
| | | placeholder="请输入16-19位银行账号" |
| | | maxlength="19" |
| | | clearable |
| | | @input="handleBankAccountInput" |
| | | @blur="handleBankAccountBlur" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </el-card> |
| | |
| | | requestEmployeeRecalculate(row); |
| | | }; |
| | | |
| | | // 银行账号输入:只允许数字,自动剔除空格/横线等,最多19位;输入到16-19位立即自动识别银行 |
| | | const handleBankAccountInput = () => { |
| | | form.value.bankAccount = String(form.value.bankAccount ?? "") |
| | | .replace(/\D/g, "") |
| | | .slice(0, 19); |
| | | autoFillPayBank(); |
| | | }; |
| | | |
| | | // 卡号识别银行并回填"支付银行"下拉(识别不到不处理,不影响提交) |
| | | const autoFillPayBank = () => { |
| | | const value = form.value.bankAccount; |
| | | if (!/^\d{16,19}$/.test(value)) { |
| | | return; |
| | | } |
| | | const bankName = recognizeBankByCardNo(value); |
| | | if (!bankName) { |
| | | return; |
| | | } |
| | | if (bankOptions.value.includes(bankName)) { |
| | | if (form.value.payBank !== bankName) { |
| | | form.value.payBank = bankName; |
| | | proxy.$modal.msgSuccess(`已自动识别为:${bankName}`); |
| | | } |
| | | } else { |
| | | proxy.$modal.msg(`识别到银行卡属于【${bankName}】,请手动选择支付银行`); |
| | | } |
| | | }; |
| | | |
| | | // 银行账号 Luhn 校验(仅用于警告提示,不阻断提交) |
| | | const luhnValid = account => { |
| | | let sum = 0; |
| | | let alternate = false; |
| | | for (let i = account.length - 1; i >= 0; i--) { |
| | | let n = Number(account[i]); |
| | | if (alternate) { |
| | | n *= 2; |
| | | if (n > 9) n -= 9; |
| | | } |
| | | sum += n; |
| | | alternate = !alternate; |
| | | } |
| | | return sum % 10 === 0; |
| | | }; |
| | | |
| | | // 常见银行借记卡 BIN 前缀表(前6位,仅用于自动识别,不做强校验) |
| | | const BANK_BINS = { |
| | | "中国工商银行": ["620200", "621210", "621211", "621225", "621226", "621227", "621288", "621723", "622202", "622203"], |
| | | "中国建设银行": ["621284", "621466", "621488", "621499", "621598", "621599", "621700", "622700"], |
| | | "中国农业银行": ["621282", "621336", "622827", "622828", "622845", "622848", "622849"], |
| | | "中国银行": ["621660", "621661", "621662", "621663", "621665", "621666", "621667", "621668", "621669", "621725", "621756", "621790", "622760"], |
| | | "交通银行": ["622258", "622260", "622261", "622262"], |
| | | "招商银行": ["621286", "621483", "621485", "621486", "622580", "622588"], |
| | | "中国邮政储蓄银行": ["621095", "621799", "622150", "622151", "622188"], |
| | | "兴业银行": ["622908", "622909"], |
| | | "中国民生银行": ["621691", "622615", "622617", "622622"], |
| | | "中国光大银行": ["622661", "622662", "622663"], |
| | | "中信银行": ["621771", "621773", "622690", "622698"], |
| | | "上海浦东发展银行": ["621792", "621793", "621795", "622521", "622522"], |
| | | "华夏银行": ["622630", "622631", "622632"], |
| | | "平安银行": ["621625", "621626", "622155", "622156"], |
| | | "广发银行": ["622568", "622569"], |
| | | }; |
| | | |
| | | // 根据卡号前6位识别银行名称(识别不到返回空串) |
| | | const recognizeBankByCardNo = account => { |
| | | if (!/^\d{16,19}$/.test(account)) { |
| | | return ""; |
| | | } |
| | | const prefix6 = account.slice(0, 6); |
| | | for (const [bank, prefixes] of Object.entries(BANK_BINS)) { |
| | | if (prefixes.includes(prefix6)) { |
| | | return bank; |
| | | } |
| | | } |
| | | return ""; |
| | | }; |
| | | |
| | | // 失焦校验:长度/格式错误由表单规则红色提示并拦截;Luhn 不通过仅黄色警告;再识别一次银行 |
| | | const handleBankAccountBlur = () => { |
| | | const value = form.value.bankAccount; |
| | | if (!value) { |
| | | return; |
| | | } |
| | | if (!/^\d{16,19}$/.test(value)) { |
| | | return; // 红色错误由表单规则处理 |
| | | } |
| | | if (!luhnValid(value)) { |
| | | proxy.$modal.msgWarning("银行账号 Luhn 校验未通过(仅提示,可继续提交)"); |
| | | } |
| | | autoFillPayBank(); |
| | | }; |
| | | |
| | | // 基础资料表单 |
| | | const data = reactive({ |
| | | form: { |
| | |
| | | remark: "", |
| | | payBank: "", |
| | | auditUserId: undefined, |
| | | bankAccount: "", |
| | | }, |
| | | rules: { |
| | | salaryTitle: [ |
| | |
| | | ], |
| | | auditUserId: [ |
| | | { required: true, message: "请选择审核人", trigger: "change" }, |
| | | ], |
| | | bankAccount: [ |
| | | { |
| | | validator: (rule, value, callback) => { |
| | | if (!value) { |
| | | return callback(); |
| | | } |
| | | if (!/^\d{16,19}$/.test(value)) { |
| | | return callback(new Error("银行账号必须为16-19位纯数字")); |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger: "blur", |
| | | }, |
| | | ], |
| | | }, |
| | | }); |
| | |
| | | remark: "", |
| | | payBank: "", |
| | | auditUserId: undefined, |
| | | bankAccount: "", |
| | | }); |
| | | // 编辑:列表页已返回主表字段;这里只做回显(明细由“生成工资表/计算工资”得到) |
| | | if (type === "edit" && row?.id) { |
| | |
| | | form.value.remark = row.remark ?? ""; |
| | | form.value.payBank = row.payBank ?? ""; |
| | | form.value.auditUserId = row.auditUserId ?? undefined; |
| | | form.value.bankAccount = row.bankAccount ?? ""; |
| | | |
| | | // 如果有员工明细数据,直接反显 |
| | | if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) { |
| | |
| | | }; |
| | | |
| | | const saveData = status => { |
| | | // 提交前兜底校验银行账号:长度/格式不符直接拦截 |
| | | const account = String(form.value.bankAccount ?? ""); |
| | | if (account && !/^\d{16,19}$/.test(account)) { |
| | | proxy.$modal.msgError("银行账号必须为16-19位纯数字,请修改后再提交"); |
| | | return; |
| | | } |
| | | const payload = { |
| | | id: form.value.id, |
| | | salaryTitle: form.value.salaryTitle, |
| | |
| | | payBank: form.value.payBank, |
| | | auditUserId: form.value.auditUserId, |
| | | auditUserName: auditUserName.value, |
| | | bankAccount: account, |
| | | totalSalary: totalSalary.value, |
| | | staffSalaryDetailList: employeeList.value.map(e => ({ |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | |
| | | }, |
| | | { label: "工资总额", prop: "totalSalary", width: 120 }, |
| | | { label: "支付银行", prop: "payBank", width: 120 }, |
| | | { label: "银行账号", prop: "bankAccount", width: 180 }, |
| | | { label: "审核人", prop: "auditUserName", width: 110 }, |
| | | { label: "备注", prop: "remark", minWidth: 120 }, |
| | | { |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="采购油品:" |
| | | prop="purchaseOilProduct"> |
| | | <el-select v-model="form.purchaseOilProduct" |
| | | placeholder="请选择" |
| | | clearable |
| | | style="width: 100%"> |
| | | <el-option label="汽#92" |
| | | value="汽#92" /> |
| | | <el-option label="汽#95" |
| | | value="汽#95" /> |
| | | <el-option label="柴#0" |
| | | value="柴#0" /> |
| | | <el-option label="柴#-35" |
| | | value="柴#-35" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="物流方式:" |
| | | prop="logisticsMethod"> |
| | | <el-select v-model="form.logisticsMethod" |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="提货方式:" |
| | | prop="pickupMethod"> |
| | |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="采购油库:" |
| | | prop="oilDepotId"> |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客户名称:" |
| | | prop="customerName"> |
| | |
| | | :disabled="!['代储', '采购直销'].includes(form.purchaseCategory)" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="采购数量(吨):" |
| | | prop="purchaseQuantity"> |