feat(finance): 重构付款单新增功能并支持预付与应付单合并付款

- 重写 AddForm.vue 组件以支持预付和正常付款两种模式
- 新增付款计划选择器组件用于应付单付款计划管理
- 修改后端接口 URL 并增加查询应付单及采购订单的 API 方法
- 扩展付款明细服务以支持通过付款单编码查询数据
- 更新付款单退回逻辑以正确处理关联应付单金额更新
- 移除旧有的付款单应付明细查询方法及相关 XML 配置
dev_1.0.0
chenhao 2025-12-15 10:23:57 +08:00
parent 3e45254fc1
commit c940880a9d
8 changed files with 395 additions and 75 deletions

View File

@ -53,7 +53,7 @@ export function returnPayment(id) {
// 新增付款单
export function addPayment(data) {
return request({
url: '/finance/payment/add',
url: '/finance/payable/mergeAndInitiatePayment',
method: 'post',
data: data,
needLoading: true
@ -77,3 +77,21 @@ export function applyRefund(id) {
})
}
// 查询应付单列表 (用于新增付款单-非预付)
export function listPayableBills(query) {
return request({
url: 'finance/payable/list',
method: 'post',
data: query
})
}
// 查询采购订单列表 (用于新增付款单-预付)
export function listPurchaseOrders(query) {
return request({
url: '/finance/payment/purchaseOrders',
method: 'get',
params: query
})
}

View File

@ -1,56 +1,174 @@
<template>
<el-dialog title="新增付款单" :visible.sync="internalVisible" width="500px" @close="handleClose">
<el-dialog title="新增付款单" :visible.sync="internalVisible" width="1200px" @close="handleClose"
:close-on-click-modal="false" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="付款单类型" prop="paymentBillType">
<el-select v-model="form.paymentBillType" disabled placeholder="请选择付款单类型">
<el-option
v-for="dict in dicts.payment_bill_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="制造商名称" prop="vendorCode">
<el-select v-model="form.vendorCode" placeholder="请选择制造商" style="width:100%" >
<el-option v-for="item in vendorOptions" :key="item.vendorCode" :label="item.vendorName" :value="item.vendorCode"></el-option>
</el-select>
</el-form-item>
<el-form-item label="预计付款时间" prop="paymentTime">
<el-date-picker
v-model="form.paymentTime"
type="datetime"
placeholder="选择日期"
value-format="yyyy-MM-dd HH:mm:ss"
></el-date-picker>
</el-form-item>
<el-form-item label="含税总价" prop="totalPriceWithTax">
<el-input-number v-model="form.totalPriceWithTax" @change="handlePriceChange" :precision="2"
:step="1"></el-input-number>
</el-form-item>
<el-form-item label="税率" prop="taxRate">
<el-input-number v-model="form.taxRate" @change="handlePriceChange" :precision="2" :step="0.01"
:max="1"></el-input-number>
</el-form-item>
<el-form-item label="未税总价" prop="totalPriceWithoutTax">
<el-input v-model="form.totalPriceWithoutTax" :disabled="true"/>
</el-form-item>
<el-form-item label="税额" prop="taxAmount">
<el-input v-model="form.taxAmount" :disabled="true"/>
</el-form-item>
<el-row>
<el-col :span="24">
<el-form-item label="制造商名称" prop="vendorCode">
<el-select
v-model="form.vendorCode"
placeholder="请选择制造商"
style="width:100%"
filterable
@change="handleVendorChange"
>
<el-option v-for="item in vendorOptions" :key="item.vendorCode" :label="item.vendorName"
:value="item.vendorCode"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="预计付款时间" prop="estimatedPaymentTime">
<el-date-picker
v-model="form.estimatedPaymentTime"
type="date"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择日期"
></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="是否预付" prop="paymentBillType">
<el-radio-group v-model="form.paymentBillType" @change="handleTypeChange">
<el-radio label="PRE_PAYMENT"></el-radio>
<el-radio label="NORMAL"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row v-if="form.paymentBillType === 'PRE_PAYMENT'">
<el-col :span="12">
<el-form-item label="预付金额" prop="totalPriceWithTax">
<el-input-number v-model="form.totalPriceWithTax" :precision="2" :step="100"
style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<!-- Tables -->
<div v-if="form.vendorCode">
<div v-if="form.paymentBillType === 'NORMAL'" class="table-container">
<h4>应付单列表</h4>
<el-table
:data="payableList"
border
style="width: 100%"
@selection-change="handleSelectionChange"
max-height="400"
row-key="id"
>
<el-table-column type="selection" width="55" reserve-selection></el-table-column>
<el-table-column label="应付单编号" align="center" prop="payableBillCode" width="150"/>
<el-table-column label="预计付款时间" align="center" prop="planPaymentDate" width="180"/>
<el-table-column label="付款计划" align="center" width="100" prop="planAmount">
</el-table-column>
<el-table-column label="项目名称" align="center" prop="projectName" width="150"/>
<el-table-column label="合同编号" align="center" prop="orderCode" width="150"/>
<el-table-column label="出入库单号" align="center" prop="inventoryCode" width="150"/>
<el-table-column label="付款状态" align="center" prop="paymentStatus" width="120">
<template slot-scope="scope">
<dict-tag :options="dicts.payment_status" :value="scope.row.paymentStatus"/>
</template>
</el-table-column>
<el-table-column label="含税总价" align="center" prop="totalPriceWithTax" width="120"/>
<el-table-column label="未付款金额" align="center" prop="unpaidAmount" width="120"/>
<el-table-column label="本次付款金额" align="center" width="120">
<template slot-scope="scope">
{{ calculateOrderCurrentPaymentAmount(scope.row).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="本次付款比例" align="center" width="120">
<template slot-scope="scope">
{{ calculateOrderCurrentPaymentRate(scope.row) }}%
</template>
</el-table-column>
<el-table-column label="已付款金额" align="center" prop="paidAmount" width="120"/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100"
fixed="right">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleOpenPaymentPlanSelector(scope.row, scope.$index)"
>选择
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="form.paymentBillType === 'PRE_PAYMENT'" class="table-container">
<h4>采购订单列表</h4>
<el-table
:data="orderList"
border
style="width: 100%"
@selection-change="handleSelectionChange"
max-height="400"
row-key="orderNo"
>
<el-table-column type="selection" width="55" reserve-selection></el-table-column>
<el-table-column prop="orderNo" label="订单编号"></el-table-column>
<el-table-column prop="orderTime" label="订单日期" width="180"></el-table-column>
<el-table-column prop="orderAmount" label="订单金额"></el-table-column>
<el-table-column prop="remark" label="备注"></el-table-column>
</el-table>
</div>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="loadTableData"
/>
</div>
<div v-else style="text-align: center; color: #909399; padding: 20px;">
请先选择制造商
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<div v-if="form.paymentBillType === 'NORMAL'" style="float: left; line-height: 36px;">
<span style="margin-right: 20px;">计划付款总金额: <el-tag type="success">{{
totalPlannedAmount.toFixed(2)
}}</el-tag></span>
</div>
<el-button @click="handleClose"> </el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</div>
<!-- Payment Plan Selector Dialog -->
<el-dialog :title="planTitle" :visible.sync="isPaymentPlanSelectorOpen" width="70%"
@close="isPaymentPlanSelectorOpen=false" append-to-body>
<payment-plan-selector
ref="planSelector"
:payable-data="choosePayable"
:selected-plans="choosePayable.paymentPlans"
@confirm="handlePaymentPlanConfirm"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="isPaymentPlanSelectorOpen=false"> </el-button>
<el-button type="primary" @click="handleChooseConfirm"> </el-button>
</div>
</el-dialog>
</el-dialog>
</template>
<script>
import {listAllVendor} from "@/api/base/vendor";
import {listPayableBills, listPurchaseOrders} from "@/api/finance/payment";
import PaymentPlanSelector from "../../payable/components/PaymentPlan";
export default {
name: "AddForm",
components: {PaymentPlanSelector},
props: {
visible: {
type: Boolean,
@ -65,64 +183,223 @@ export default {
return {
internalVisible: this.visible,
vendorOptions: [],
payableList: [], // List for Standard/Payable Bills
orderList: [], // List for Prepayment/Purchase Orders
selectedRows: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10
},
form: {
paymentBillType:'PRE_PAYMENT',
paymentBillType: 'NORMAL', // Default to Normal (Not Prepayment)
vendorCode: null,
vendorName: null,
paymentTime: null,
remark: null,
totalPriceWithTax: 0,
taxRate: 0.13, // Default tax rate
totalPriceWithoutTax: 0,
taxAmount: 0
estimatedPaymentTime: null
},
rules: {
paymentBillType: [{required: true, message: "付款单类型不能为空", trigger: "change"}],
vendorName: [{required: true, message: "制造商名称不能为空", trigger: "blur"}],
paymentTime: [{required: true, message: "预计付款时间不能为空", trigger: "change"}],
totalPriceWithTax: [{required: true, message: "含税总价不能为空", trigger: "blur"}]
}
vendorCode: [{required: true, message: "制造商名称不能为空", trigger: "change"}],
paymentBillType: [{required: true, message: "请选择是否预付", trigger: "change"}],
totalPriceWithTax: [{required: false, message: "预付金额不能为空", trigger: "blur"}]
},
// Plan Selector Data
planTitle: '',
isPaymentPlanSelectorOpen: false,
choosePayable: {},
currentPayableOrderIndexForPlan: -1,
};
},
computed: {
totalPlannedAmount() {
if (this.form.paymentBillType === 'NORMAL') {
// Calculate based on selected rows and their plans/defaults
return this.selectedRows.reduce((sum, row) => {
return sum + this.calculateOrderCurrentPaymentAmount(row);
}, 0);
}
return 0;
}
},
watch: {
visible(newVal) {
this.internalVisible = newVal;
if (newVal) {
this.resetForm();
this.getVendorList();
}
},
internalVisible(newVal) {
this.$emit("update:visible", newVal);
},
'form.paymentBillType': function (val) {
// Toggle validation for Prepayment Amount
if (val === 'PRE_PAYMENT') {
this.rules.totalPriceWithTax[0].required = true;
} else {
this.rules.totalPriceWithTax[0].required = false;
}
this.queryParams.pageNum = 1;
this.selectedRows = [];
this.loadTableData();
}
},
methods: {
/** 获取厂商列表 */
getVendorList() {
return listAllVendor().then(res => {
listAllVendor().then(res => {
this.vendorOptions = res.data;
})
},
handlePriceChange() {
const taxed = this.form.totalPriceWithTax || 0;
const rate = this.form.taxRate || 0;
const unTaxed = this.$calc.div(taxed, this.$calc.add(1, rate));
const taxAmount = this.$calc.sub(taxed, unTaxed);
this.form.totalPriceWithoutTax = unTaxed;
this.form.taxAmount = taxAmount;
handleVendorChange(val) {
// Find name for the code
const vendor = this.vendorOptions.find(v => v.vendorCode === val);
if (vendor) {
this.form.vendorName = vendor.vendorName;
}
this.queryParams.pageNum = 1;
this.selectedRows = [];
this.loadTableData();
},
handleTypeChange() {
this.queryParams.pageNum = 1;
this.selectedRows = []; // Clear selection when switching type
this.loadTableData();
},
loadTableData() {
if (!this.form.vendorCode) return;
// Do not clear lists immediately to avoid flicker if desired, but here we reset for simplicity
this.payableList = [];
this.orderList = [];
const query = {
vendorCode: this.form.vendorCode,
pageNum: this.queryParams.pageNum,
pageSize: this.queryParams.pageSize
};
if (this.form.paymentBillType === 'NORMAL') {
listPayableBills(query).then(res => {
this.payableList = (res.rows || []).map(item => {
const paymentPlans = item.paymentPlans ? [...item.paymentPlans] : [];
if (paymentPlans.length === 0 && item.lastPaymentPlanId) {
paymentPlans.push({
id: item.lastPaymentPlanId,
planAmount: item.planAmount,
planPaymentDate: item.planPaymentDate,
planRate: this.$calc.mul(this.$calc.div(item.planAmount, item.totalPriceWithTax, 4), 100)
});
}
return {
...item,
paymentPlans: paymentPlans, // Retain existing plans if any, otherwise empty
totalPriceWithTax: item.totalPriceWithTax || 0, // Ensure numeric for calculations
unpaidAmount: item.unpaidAmount || 0,
paidAmount: item.paidAmount || 0, // Ensure numeric for calculations
}
});
this.total = res.total;
});
} else if (this.form.paymentBillType === 'PRE_PAYMENT') {
listPurchaseOrders(query).then(res => {
this.orderList = res.rows || [];
this.total = res.total;
});
}
},
handleSelectionChange(selection) {
this.selectedRows = selection;
},
// --- Payment Plan Logic ---
handleOpenPaymentPlanSelector(row, index) {
this.planTitle = `选择付款计划 - ${row.payableBillCode}`;
this.choosePayable = row;
this.currentPayableOrderIndexForPlan = index;
this.isPaymentPlanSelectorOpen = true;
},
handleChooseConfirm() {
if (!this.$refs.planSelector) {
this.$modal.msgError('无法获取计划选择器组件');
return;
}
const selectedPlans = this.$refs.planSelector.selectedPlan || [];
// Update the payment plans for the specific order
if (this.currentPayableOrderIndexForPlan !== -1) {
const row = this.payableList[this.currentPayableOrderIndexForPlan];
this.$set(row, 'paymentPlans', [...selectedPlans]);
}
this.isPaymentPlanSelectorOpen = false;
this.$modal.msgSuccess(`已更新付款计划选择,共 ${selectedPlans.length}`);
},
handlePaymentPlanConfirm(updatedPlans) {
// This might be redundant if handleChooseConfirm does the job, checking Usage in MergePaymentDialog
},
calculateOrderCurrentPaymentAmount(order) {
if (order && order.paymentPlans && order.paymentPlans.length > 0) {
return order.paymentPlans.reduce((sum, plan) => sum + (plan.planAmount || 0), 0);
}
return 0;
},
calculateOrderCurrentPaymentRate(order) {
if (order && order.paymentPlans && order.paymentPlans.length > 0 && order.totalPriceWithTax) {
const currentAmount = this.calculateOrderCurrentPaymentAmount(order);
return this.$calc.mul((this.$calc.div(currentAmount, order.totalPriceWithTax, 4)), 100);
}
return 0;
},
handleClose() {
this.internalVisible = false;
},
handleSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
// Here you would typically emit an event to the parent component
// to handle the form submission, e.g., call an API.
this.form.preResidueAmount=0;
this.$emit("submit", this.form);
if (this.form.paymentBillType === 'NORMAL') {
if (this.selectedRows.length === 0) {
this.$message.warning("请选择至少一条应付单");
return;
}
// Process selected rows
const processedPayableOrders = this.selectedRows.map(order => {
let finalPlans = order.paymentPlans;
return {
id: order.id,
payableBillCode: order.payableBillCode,
taxRate: order.taxRate,
// Map plans to structure expected by backend (similar to MergePaymentDialog)
paymentPlans: finalPlans.map(plan => ({
id: plan.id, // ID if existing plan
planPaymentDate: plan.planPaymentDate,
planAmount: plan.planAmount,
planRate: plan.planRate,
remark: plan.remark
}))
};
});
const submitData = {
paymentBillType: 'FROM_PAYABLE',
vendorCode: this.form.vendorCode,
estimatedPaymentTime: this.form.estimatedPaymentTime,
vendorName: this.form.vendorName,
remark: this.form.remark,
payableOrders: processedPayableOrders,
totalMergePaymentAmount: this.totalPlannedAmount
};
this.$emit("submit", submitData);
} else {
// Prepayment logic
const submitData = {
...this.form,
details: this.selectedRows
};
this.$emit("submit", submitData);
}
}
});
},
@ -131,16 +408,27 @@ export default {
this.$refs.form.resetFields();
}
this.form = {
paymentBillType: 'PRE_PAYMENT',
paymentBillType: 'NORMAL',
vendorCode: null,
vendorName: null,
paymentTime: null,
remark: null,
totalPriceWithTax: 0,
taxRate: 0.13,
totalPriceWithoutTax: 0,
taxAmount: 0
};
this.handlePriceChange();
this.payableList = [];
this.orderList = [];
this.selectedRows = [];
this.queryParams = {
pageNum: 1,
pageSize: 10
};
this.total = 0;
}
}
};
</script>
<style scoped>
.table-container {
margin-top: 20px;
}
</style>

View File

@ -86,7 +86,6 @@ public interface OmsPaymentBillMapper
* @param paymentBillId
* @return
*/
public List<PaymentBillPayableDetailDTO> selectPaymentBillPayableDetails(Long paymentBillId);

View File

@ -15,4 +15,6 @@ public interface IOmsPayablePaymentDetailService {
List<PaymentBillPayableDetailDTO> listPayableByPaymentCode(String paymentBillCode);
List<OmsPayablePaymentDetail> selectByPaymentPlanIds(List<Long> paymentPlanIds);
List<OmsPayablePaymentDetail> listBypaymentCode(String paymentBillCode);
}

View File

@ -105,4 +105,11 @@ public class OmsPayablePaymentDetailServiceImpl implements IOmsPayablePaymentDet
public List<OmsPayablePaymentDetail> selectByPaymentPlanIds(List<Long> paymentPlanIds) {
return omsPayablePaymentDetailMapper.selectByPaymentPlanIds(paymentPlanIds);
}
@Override
public List<OmsPayablePaymentDetail> listBypaymentCode(String paymentBillCode) {
OmsPayablePaymentDetail omsPayablePaymentDetail = new OmsPayablePaymentDetail();
omsPayablePaymentDetail.setPaymentBillCode(paymentBillCode);
return omsPayablePaymentDetailMapper.list(omsPayablePaymentDetail);
}
}

View File

@ -1,6 +1,7 @@
package com.ruoyi.sip.service.impl;
import java.util.List;
import java.util.stream.Collectors;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
@ -170,7 +171,7 @@ public class OmsPaymentBillServiceImpl implements IOmsPaymentBillService , TodoC
if (!OmsPaymentBill.PaymentBillTypeEnum.FROM_PAYABLE.getCode().equals(paymentBill.getPaymentBillType())) {
return AjaxResult.error("只有由应付单合并生成的付款单才能执行退回操作");
}
List<OmsPayablePaymentDetail> omsPayablePaymentDetails = detailService.listBypaymentCode(paymentBill.getPaymentBillCode());
// 3. 清楚关联
omsPaymentBillMapper.clearRelationPayable(paymentBill.getPaymentBillCode());
@ -181,6 +182,11 @@ public class OmsPaymentBillServiceImpl implements IOmsPaymentBillService , TodoC
throw new RuntimeException("删除付款单失败");
}
if (CollUtil.isNotEmpty(omsPayablePaymentDetails)) {
payableBillService.updatePaymentAmount(omsPayablePaymentDetails.stream().map(OmsPayablePaymentDetail::getPayableBillId).distinct().collect(Collectors.toList()));
}
return AjaxResult.success("付款单退回成功!");
} catch (Exception e) {

View File

@ -67,6 +67,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="payableBillId != null">
and t1.payable_bill_id = #{payableBillId}
</if>
<if test="paymentBillCode != null and paymentBillCode!=''">
and t1.payment_bill_code = #{paymentBillCode}
</if>
<if test="payableBillIdList != null and payableBillIdList.size>0">
and t1.payable_bill_id in
<foreach item="item" collection="payableBillIdList" separator="," open="(" close=")" index="">

View File

@ -297,10 +297,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="selectOmsPaymentBillVo"/>
where pb.id = #{id}
</select>
<select id="selectPaymentBillPayableDetails"
resultType="com.ruoyi.sip.domain.dto.PaymentBillPayableDetailDTO">
</select>
<select id="selectOmsPaymentBillByCode" resultType="com.ruoyi.sip.domain.OmsPaymentBill">
<include refid="selectOmsPaymentBillVo"/>
where pb.payment_bill_code = #{code}