feat(finance): 新增采购应付单管理功能

- 新增采购应付单列表页面,支持多条件筛选和分页查询
- 实现应付单详情弹窗,展示应付单基本信息和状态
- 添加合并付款单功能,支持多应付单合并发起付款
- 实现付款计划选择器,可为每个应付单配置付款计划
- 新增应付单修改和删除功能
- 添加数据字典支持,用于展示产品类型、付款状态等枚举值
- 实现应付单导出功能
- 新增后端付款计划相关接口和实体类
- 添加付款计划Mapper接口及实现
- 实现付款计划服务层接口及业务逻辑
dev_1.0.0
chenhao 2025-12-05 14:18:54 +08:00
parent 1d842d3bf2
commit 94348aebc4
14 changed files with 1423 additions and 8 deletions

View File

@ -0,0 +1,27 @@
import request from '@/utils/request'
// 查询采购应付单列表
export function listPayable(query) {
return request({
url: '/finance/payable/list',
method: 'post',
data: query
})
}
// 查询付款计划列表
export function getPaymentPlan(payableBillId) {
return request({
url: `/finance/payable/plan/${payableBillId}`,
method: 'get'
})
}
// 更新付款计划
export function updatePaymentPlan(payableBillId, data) {
return request({
url: `/finance/payable/plan/${payableBillId}`,
method: 'post',
data: data
})
}

View File

@ -99,7 +99,7 @@ export default {
</script>
<style lang="scss" scoped>
::v-deep .el-submenu__title {
font-size: 15px;
font-size: 14px;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<el-dialog title="修改采购应付单" :visible.sync="internalVisible" width="70%" @close="handleClose">
<div class="dialog-body">
<!-- Part 1: Details -->
<div>
<el-divider content-position="left">采购-应付单</el-divider>
<div class="details-container">
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item"><strong>应付单编号:</strong> {{ data.payableBillCode }}</div>
</el-col>
<el-col :span="16">
<div class="detail-item"><strong>生成时间:</strong> {{ data.createTime }}</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item"><strong>制造商名称:</strong> {{ data.vendorName }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item"><strong>项目编号:</strong> {{ data.projectCode }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item"><strong>项目名称:</strong> {{ data.projectName }}</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item"><strong>合同编号:</strong> {{ data.orderCode }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item"><strong>出入库单号:</strong> {{ data.inventoryCode }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item" style="display: flex"><strong>产品类型:</strong>
<dict-tag :options="dict.type.product_type" :value="data.productType"/>
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item"><strong>含税总价:</strong> {{ data.totalPriceWithTax }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item"><strong>未税总价:</strong> {{ data.totalPriceWithoutTax }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item"><strong>税额:</strong> {{ data.taxAmount }}</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item"><strong>未付款金额:</strong> {{ data.unpaidAmount }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item"><strong>已付款金额:</strong> {{ data.paidAmount }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item"><strong>付款中金额:</strong> {{ data.payingAmount }}</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item"><strong>未收票金额:</strong> {{ data.unInvoicedAmount }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item"><strong>已收票金额:</strong> {{ data.invoicedAmount }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item"><strong>收票中金额:</strong> {{ data.invoicingAmount }}</div>
</el-col>
</el-row>
</div>
</div>
<!-- Part 2: Tabs -->
<div style="margin-top: 20px;">
<el-tabs v-model="activeTab">
<el-tab-pane label="明细" name="details">
<!-- Content for 明细 tab will be added later -->
明细内容
</el-tab-pane>
<el-tab-pane label="付款计划" name="paymentPlan">
<PaymentPlan :isInitEdit=true :payableData="data"/>
</el-tab-pane>
</el-tabs>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</div>
</el-dialog>
</template>
<script>
import PaymentPlan from './PaymentPlan.vue';
export default {
name: "EditForm",
dicts: ['product_type'],
components: {
PaymentPlan
},
props: {
visible: {
type: Boolean,
default: false
},
data: {
type: Object,
default: () => ({})
}
}, data() {
return {
internalVisible: this.visible, // Local copy of the visible prop
activeTab: 'details',
};
},
watch: {
visible(newVal) {
this.internalVisible = newVal; // Sync prop to local data
},
internalVisible(newVal) {
this.$emit('update:visible', newVal); // Emit changes to parent
}
},
methods: {
handleClose() {
this.internalVisible = false; // Close dialog locally
},
handleSubmit() {
this.handleClose();
},
}
};
</script>
<style scoped>.details-container {
border: 1px solid #EBEEF5;
padding: 20px;
border-radius: 4px;
}
.detail-item {
border: 1px solid #EBEEF5;
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
}
.dialog-body {
max-height: 70vh;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,330 @@
<template>
<el-dialog title="合并发起付款单" :visible.sync="dialogVisible" width="80%" @close="handleClose" append-to-body>
<div class="dialog-body">
<el-form ref="form" :model="form" :inline="true" label-width="120px">
<el-row>
<el-col :span="8">
<el-form-item label="付款单类型" prop="paymentBillType">
<el-select v-model="form.paymentBillType" placeholder="请选择付款单类型" clearable>
<el-option
v-for="dict in paymentBillTypeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="制造商名称">
<el-input v-model="form.vendorName" readonly/>
</el-form-item>
</el-col>
<el-col :span="8">
<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-form>
<el-divider content-position="left">采购应付单表</el-divider>
<el-table :data="payableOrdersWithPlans" border max-height="300px" style="margin-bottom: 20px;">
<el-table-column label="应付单编号" align="center" prop="payableBillCode" width="150"/>
<el-table-column label="预计付款时间" align="center" prop="estimatedPaymentTime" width="180"/>
<el-table-column label="付款计划" align="center" width="100">
<template slot-scope="scope">
<span>{{ scope.row.paymentPlans ? scope.row.paymentPlans.length : 0 }} 条计划</span>
</template>
</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="dict.type.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.id).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="本次付款比例" align="center" width="120">
<template slot-scope="scope">
{{ calculateOrderCurrentPaymentRate(scope.row.id).toFixed(2) }}%
</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 class="total-info">
<span>合并应付总金额 (含税): <el-tag type="info">{{ totalPayableAmountWithTax.toFixed(2) }}</el-tag></span>
<span style="margin-left: 20px;">计划付款总金额: <el-tag type="success">{{
totalPlannedAmount.toFixed(2)
}}</el-tag></span>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel"> </el-button>
<el-button type="primary" @click="handleConfirm"> </el-button>
</div>
<el-dialog :title="planTitle" :visible.sync="isPaymentPlanSelectorOpen" width="70%"
@close="isPaymentPlanSelectorOpen=false" append-to-body>
<payment-plan-selector
:payable-data="choosePayable"
@confirm="handlePaymentPlanConfirm"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="isPaymentPlanSelectorOpen=false"> </el-button>
<!-- <el-button type="primary" @click="handleConfirm" > </el-button>-->
<el-button type="primary" @click="handleConfirm"> </el-button>
</div>
</el-dialog>
<!-- 付款计划选择器弹窗 -->
</el-dialog>
</template>
<script>
import PaymentPlan from './PaymentPlan.vue';
import {getPaymentPlan} from "@/api/finance/payable";
export default {
name: "MergePaymentDialog",
components: {PaymentPlanSelector: PaymentPlan},
dicts: ['payment_status'], // Add dicts for dict-tag
props: {
visible: {
type: Boolean,
default: false
},
payableOrders: {
type: Array,
default: () => []
}
},
data() {
return {
internalVisible: this.visible,
planTitle: '',
choosePayable: {},
form: {
paymentBillType: '1', // Default to a type, or make it dynamic
vendorName: '',
estimatedPaymentTime: null,
},
paymentBillTypeOptions: [
{label: '普通付款单', value: '1'},
{label: '紧急付款单', value: '2'},
],
payableOrdersWithPlans: [], // Each order will now have its own paymentPlans array
isPaymentPlanSelectorOpen: false,
currentPayableOrderIndexForPlan: -1, // Index of the order in payableOrdersWithPlans
loadingPaymentPlans: false, // Loading state for fetching payment plans
};
},
computed: {
dialogVisible: {
get() {
return this.internalVisible;
},
set(val) {
this.internalVisible = val;
this.$emit('update:visible', val);
}
},
totalPayableAmountWithTax() {
return this.payableOrdersWithPlans.reduce((sum, order) => sum + (order.totalPriceWithTax || 0), 0);
},
totalPlannedAmount() {
return this.payableOrdersWithPlans.reduce((orderSum, order) => {
const orderPlansTotal = (order.paymentPlans || []).reduce((planSum, plan) => planSum + (plan.planAmount || 0), 0);
return orderSum + orderPlansTotal;
}, 0);
},
},
watch: {
visible(newVal) {
this.internalVisible = newVal;
if (newVal) {
this.initDialogData();
}
},
payableOrders: {
handler(newVal) {
if (this.dialogVisible) {
this.initDialogData();
}
},
deep: true
}
},
methods: {
initDialogData() {
// Initialize form fields
if (this.payableOrders.length > 0) {
const firstVendorName = this.payableOrders[0].vendorName;
const allSameVendor = this.payableOrders.every(order => order.vendorName === firstVendorName);
this.form.vendorName = allSameVendor ? firstVendorName : '多个制造商';
this.form.estimatedPaymentTime = this.payableOrders[0].estimatedPaymentTime || null; // Use first order's estimated time as default
} else {
this.form.vendorName = '';
this.form.estimatedPaymentTime = null;
}
this.form.paymentBillType = '1'; // Default
// Initialize payableOrdersWithPlans
this.payableOrdersWithPlans = this.payableOrders.map(order => ({
...order,
paymentPlans: order.paymentPlans || [], // Retain existing plans if any, otherwise empty
totalPriceWithTax: order.totalPriceWithTax || 0, // Ensure numeric for calculations
unpaidAmount: order.unpaidAmount || 0,
paidAmount: order.paidAmount || 0, // Ensure numeric for calculations
}));
},
handleClose() {
this.dialogVisible = false;
this.resetForm();
},
handleConfirm() {
// Validate main form fields
if (!this.form.paymentBillType) {
this.$modal.msgError('请选择付款单类型');
return;
}
if (!this.form.estimatedPaymentTime) {
this.$modal.msgError('请选择预计付款时间');
return;
}
// Validate each payable order's payment plans
for (const order of this.payableOrdersWithPlans) {
if (!order.paymentPlans || order.paymentPlans.length === 0) {
this.$modal.msgError(`应付单 ${order.payableBillCode} 至少需要一条付款计划`);
return;
}
const totalPlannedAmountForOrder = order.paymentPlans.reduce((sum, plan) => sum + (plan.planAmount || 0), 0);
if (Math.abs(totalPlannedAmountForOrder - order.unpaidAmount) > 0.01) { // Compare against unpaidAmount
this.$modal.msgError(`应付单 ${order.payableBillCode} 的计划付款总金额 (${totalPlannedAmountForOrder.toFixed(2)}) 与其未付款金额 (${order.unpaidAmount.toFixed(2)}) 不符,请检查。`);
return;
}
for (const plan of order.paymentPlans) {
if (!plan.planPaymentDate) {
this.$modal.msgError(`应付单 ${order.payableBillCode} 的付款计划中预计付款时间不能为空。`);
return;
}
if (plan.planAmount === null || plan.planAmount === undefined || plan.planAmount <= 0) {
this.$modal.msgError(`应付单 ${order.payableBillCode} 的付款计划中预计付款金额必须大于0。`);
return;
}
if (plan.planRate === null || plan.planRate === undefined) {
this.$modal.msgError(`应付单 ${order.payableBillCode} 的付款计划中应付比例不能为空。`);
return;
}
}
}
// Construct the final data to be emitted to the parent
const mergedPaymentData = {
paymentBillType: this.form.paymentBillType,
estimatedPaymentTime: this.form.estimatedPaymentTime,
// Collect all payable orders with their updated payment plans
payableOrders: this.payableOrdersWithPlans.map(order => ({
id: order.id,
payableBillCode: order.payableBillCode,
paymentPlans: order.paymentPlans.map(plan => ({
planPaymentDate: plan.planPaymentDate,
planAmount: plan.planAmount,
planRate: plan.planRate,
remark: plan.remark,
})),
})),
totalMergePaymentAmount: this.totalPlannedAmount, // Total amount for the merged bill
};
this.$emit('confirm', mergedPaymentData);
this.dialogVisible = false;
},
handleCancel() {
this.dialogVisible = false;
this.resetForm();
},
resetForm() {
this.form = {
paymentBillType: '1',
vendorName: '',
estimatedPaymentTime: null,
};
this.payableOrdersWithPlans = [];
this.currentPayableOrderIndexForPlan = -1;
this.loadingPaymentPlans = false;
},
handleOpenPaymentPlanSelector(row, index) {
this.planTitle = `选择付款计划 - ${row.payableBillCode}`;
this.choosePayable = row;
this.currentPayableOrderIndexForPlan = index;
this.isPaymentPlanSelectorOpen = true;
console.log(this.choosePayable.id)
},
handlePaymentPlanConfirm(updatedPlans) {
// Update the payment plans for the specific order
if (this.currentPayableOrderIndexForPlan !== -1) {
this.$set(this.payableOrdersWithPlans[this.currentPayableOrderIndexForPlan], 'paymentPlans', updatedPlans);
}
this.isPaymentPlanSelectorOpen = false;
this.currentPayableOrderIndexForPlan = -1;
},
calculateOrderCurrentPaymentAmount(orderId) {
const order = this.payableOrdersWithPlans.find(o => o.id === orderId);
if (order && order.paymentPlans) {
return order.paymentPlans.reduce((sum, plan) => sum + (plan.planAmount || 0), 0);
}
return 0;
},
calculateOrderCurrentPaymentRate(orderId) {
const order = this.payableOrdersWithPlans.find(o => o.id === orderId);
if (order && order.paymentPlans && order.unpaidAmount > 0) {
const currentAmount = this.calculateOrderCurrentPaymentAmount(orderId);
return (currentAmount / order.unpaidAmount * 100);
}
return 0;
},
},
};
</script>
<style scoped>
.dialog-body {
max-height: 70vh;
overflow-y: auto;
padding-right: 10px; /* To prevent scrollbar from overlapping content */
}
.total-info {
margin-top: 20px;
text-align: right;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,286 @@
<template>
<div class="dialog-body">
<el-divider content-position="left">付款计划</el-divider>
<el-button v-if="isEditing" type="primary" size="mini" @click="handleSavePaymentPlan"
style="margin-bottom: 10px;">
保存付款计划
</el-button>
<el-button v-else type="primary" size="mini" @click="isEditing=true"
style="margin-bottom: 10px;">
编辑
</el-button>
<el-table :data="paymentPlans" border @selection-change="selectPlan">
<el-table-column type="selection" width="50" align="center"/>
<el-table-column label="序号" type="index" width="50" align="center"></el-table-column>
<el-table-column label="预计付款时间" align="center" width="200">
<template slot-scope="scope">
<el-date-picker v-model="scope.row.planPaymentDate" type="date" style="width: 180px"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择日期"
:disabled="!isEditing || scope.row.status === 'paid'"></el-date-picker>
</template>
</el-table-column>
<el-table-column label="预计付款金额" align="center" width="230">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.planAmount"
:precision="2"
:step="100"
:min="0.01"
:max="totalPriceWithTax"
@change="handleAmountChange(scope.row)"
:disabled="!isEditing || scope.row.status === 'paid'"
></el-input-number>
</template>
</el-table-column>
<el-table-column label="应付比例(%)" align="center" width="230">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.planRate"
:precision="2"
:step="1"
:min="0.01"
:max="100"
@change="handleRateChange(scope.row)"
:disabled="!isEditing || scope.row.status === 'paid'"
></el-input-number>
</template>
</el-table-column>
<el-table-column label="备注" align="center">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" placeholder="请输入备注"
:disabled="!isEditing || scope.row.status === 'paid'"></el-input>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150" fixed="right">
<template slot-scope="scope">
<el-button v-if="isEditing"
size="mini"
type="text"
icon="el-icon-plus"
@click="handleAddPaymentPlanRow"
>增加下行
</el-button>
<el-button v-if="isEditing"
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDeletePaymentPlanRow(scope.$index)"
:disabled="paymentPlans.length === 1 || scope.row.status === 'paid'"
>删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="total-info">
<span>应付单未付款金额: <el-tag type="info">{{ totalUnpaidAmount.toFixed(2) }}</el-tag></span>
<span style="margin-left: 20px;">计划付款总金额: <el-tag type="success">{{
totalPlannedAmount.toFixed(2)
}}</el-tag></span>
</div>
</div>
</template>
<script>
import {getPaymentPlan, updatePaymentPlan} from "@/api/finance/payable";
export default {
name: "PaymentPlanSelector",
props: {
payableData: {
type: Object,
default: () => {
}
},
isInitEdit: {
type: Boolean,
default: false
}
},
data() {
return {
selectedPlan:[],
isEditing: false,
loading: false,
paymentPlans: [],
};
},
computed: {
title() {
return `选择付款计划 - ${this.payableData.payableBillCode}`;
},
totalPlannedAmount() {
return this.paymentPlans.reduce((sum, plan) => sum + (plan.planAmount || 0), 0);
},
totalUnpaidAmount() {
return this.payableData.unpaidAmount || 0;
},
totalPriceWithTax() {
return this.payableData.totalPriceWithTax || 0;
}
},
watch: {
'payableData.id': {
handler(newVal, oldVal) {
if (newVal) {
this.fetchPaymentPlans(newVal)
}
},
immediate: true
},
isInitEdit: {
handler(newVal) {
if (newVal) {
this.isEditing = newVal
}
},
immediate: true
}
},
methods: {
selectPlan( selection){
this.selectedPlan=selection
},
fetchPaymentPlans(payableId) {
if (this.payableData && payableId) {
getPaymentPlan(payableId).then(response => {
this.paymentPlans = response.data.map(item => ({
...item,
// Add a default status if not present, similar to EditForm.vue
status: item.status || 'pending'
}));
if (this.paymentPlans.length === 0) {
this.initDefaultPaymentPlan();
}
})
} else {
this.initDefaultPaymentPlan();
}
},
initDefaultPaymentPlan() {
// Default to a single plan covering the unpaid amount if no initial plans
this.paymentPlans = [{
planPaymentDate: null,
planAmount: this.totalUnpaidAmount,
planRate: 100,
remark: '',
status: 'pending' // Default status
}];
},
handleSavePaymentPlan() {
if (!this.validatePaymentPlans()) {
return;
}
if (!this.validatePaymentPlanTotals()) {
return;
}
for (let i = 0; i < this.paymentPlans.length; i++) {
const plan = this.paymentPlans[i];
if (!plan.planPaymentDate) {
this.$modal.msgError(`${i + 1} 行付款计划的预计付款时间不能为空。`);
return;
}
if (plan.planAmount === null || plan.planAmount === undefined || plan.planAmount === '' || plan.planAmount < 0) {
this.$modal.msgError(`${i + 1} 行付款计划的预计付款金额不能为空。`);
return;
}
if (plan.planRate === null || plan.planRate === undefined || plan.planRate === '') {
this.$modal.msgError(`${i + 1} 行付款计划的应付比例不能为空。`);
return;
}
}
updatePaymentPlan(this.payableData.id, this.paymentPlans).then(() => {
this.$modal.msgSuccess("保存成功");
this.fetchPaymentPlans(this.payableData.id); // Re-fetch using the correct method and ID
});
},
handleAddPaymentPlanRow() {
this.paymentPlans.push({
planPaymentDate: null,
planAmount: 0,
planRate: 0,
remark: '',
status: 'pending' // Default status
});
},
handleDeletePaymentPlanRow(index) {
if (this.paymentPlans.length === 1) {
this.$modal.msgError("至少需要保留一条付款计划。");
return;
}
// Only allow deletion if not 'paid' or status is not set (newly added)
if (this.paymentPlans[index].status === 'paid') {
this.$modal.msgError("已付款的计划不能删除。");
return;
}
this.paymentPlans.splice(index, 1);
},
handleAmountChange(row) {
if (this.totalPriceWithTax === 0) {
row.planRate = 0;
return;
}
row.planRate = this.$calc.mul((this.$calc.div(row.planAmount, this.totalPriceWithTax,4)), 100);
},
handleRateChange(row) {
row.planAmount = this.$calc.div(this.$calc.mul(this.totalPriceWithTax , row.planRate),100);
},
validatePaymentPlanTotals() {
const totalAmount = this.paymentPlans.reduce((sum, plan) => sum + (plan.planAmount || 0), 0);
const totalRate = this.paymentPlans.reduce((sum, plan) => sum + (plan.planRate || 0), 0);
if (totalAmount !== this.totalPriceWithTax) {
this.$modal.msgError(`预计付款金额之和应该等于应付总金额[${this.totalPriceWithTax}]`);
return false;
}
return true;
},
validatePaymentPlans() {
if (this.paymentPlans.length === 0) {
this.$modal.msgError("请至少添加一条付款计划。");
return false;
}
const totalPlannedAmount = this.paymentPlans.reduce((sum, plan) => sum + (plan.planAmount || 0), 0);
for (let i = 0; i < this.paymentPlans.length; i++) {
const plan = this.paymentPlans[i];
if (!plan.planPaymentDate) {
this.$modal.msgError(`${i + 1} 行付款计划的预计付款时间不能为空。`);
return false;
}
if (plan.planAmount === null || plan.planAmount === undefined || plan.planAmount <= 0) {
this.$modal.msgError(`${i + 1} 行付款计划的预计付款金额必须大于0。`);
return false;
}
if (plan.planRate === null || plan.planRate === undefined || plan.planRate === '') {
this.$modal.msgError(`${i + 1} 行付款计划的应付比例不能为空。`);
return false;
}
}
return true;
}
}
};
</script>
<style scoped>
.dialog-body {
max-height: 70vh;
overflow-y: auto;
padding-right: 10px; /* To prevent scrollbar from overlapping content */
}
.total-info {
margin-top: 20px;
text-align: right;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,339 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="项目编号" prop="projectCode">
<el-input
v-model="queryParams.projectCode"
placeholder="请输入项目编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="queryParams.projectName"
placeholder="请输入项目名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="应付单编号" prop="payableBillCode">
<el-input
v-model="queryParams.payableBillCode"
placeholder="请输入应付单编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="制造商名称" prop="vendorName">
<el-input
v-model="queryParams.vendorName"
placeholder="请输入制造商名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="合同编号" prop="orderCode">
<el-input
v-model="queryParams.orderCode"
placeholder="请输入合同编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="出入库单号" prop="inventoryCode">
<el-input
v-model="queryParams.inventoryCode"
placeholder="请输入出入库单号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="产品类型" prop="productType">
<el-select v-model="queryParams.productType" placeholder="请选择产品类型" clearable>
<el-option
v-for="dict in dict.type.product_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="付款状态" prop="paymentStatus">
<el-select v-model="queryParams.paymentStatus" placeholder="请选择付款状态" clearable>
<el-option
v-for="dict in dict.type.payment_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="生成时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd hh:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item label="预计付款时间">
<el-date-picker
v-model="estimatedPaymentDateRange"
style="width: 240px"
value-format="yyyy-MM-dd hh:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5" >
<el-button type="primary" plain icon="el-icon-plus" @click="handleMergeAndInitiatePayment" v-hasPermi="['finance:payable:mergePayment']">
合并并发起付款单
</el-button>
</el-col>
<el-col :span="1.5" >
<el-button type="primary" plain icon="el-icon-plus" v-hasPermi="['inventory:inner:add']">
合并并发起收票单
</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="payableList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" />
<el-table-column label="项目编号" align="center" prop="projectCode" width="120" />
<el-table-column label="项目名称" align="center" prop="projectName" width="150" />
<el-table-column label="应付单编号" align="center" prop="payableBillCode" width="150" />
<el-table-column label="生成时间" align="center" prop="createTime" width="180"/>
<el-table-column label="预计付款时间" align="center" prop="estimatedPaymentTime" width="180"/>
<el-table-column label="预计付款金额" align="center" prop="totalPriceWithTax" width="120" />
<el-table-column label="该制造商是否有预付单" align="center" prop="hasAdvancePayment" width="150" />
<el-table-column label="预付金额" align="center" prop="advancePaymentAmount" width="120" />
<el-table-column label="制造商名称" align="center" prop="vendorName" 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="productType" width="120">
<template slot-scope="scope">
<dict-tag :options="dict.type.product_type" :value="scope.row.productType"/>
</template>
</el-table-column>
<el-table-column label="含税总价" align="center" prop="totalPriceWithTax" width="120" />
<el-table-column label="未税总价" align="center" prop="totalPriceWithoutTax" width="120" />
<el-table-column label="税额" align="center" prop="taxAmount" width="120" />
<el-table-column label="付款状态" align="center" prop="paymentStatus" width="120">
<template slot-scope="scope">
<dict-tag :options="dict.type.payment_status" :value="scope.row.paymentStatus"/>
</template>
</el-table-column>
<el-table-column label="生成付款单" align="center" width="120">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
@click="handleGeneratePayment(scope.row)"
>生成付款单</el-button>
</template>
</el-table-column>
<el-table-column label="收票状态" align="center" prop="invoiceStatus" width="120">
<template slot-scope="scope">
<dict-tag :options="dict.type.invoice_status" :value="scope.row.invoiceStatus"/>
</template>
</el-table-column>
<el-table-column label="生成收票单" align="center" width="120">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
@click="handleGenerateInvoice(scope.row)"
>生成收票单</el-button>
</template>
</el-table-column>
<el-table-column label="未付款金额" align="center" prop="unpaidAmount" width="120" />
<el-table-column label="付款中金额" align="center" prop="payingAmount" width="120" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160" fixed="right">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['finance:payable:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['finance:payable:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 修改弹窗 -->
<edit-form :visible.sync="open" :data="selectedRow" />
<!-- 合并付款单弹窗 -->
<merge-payment-dialog :visible.sync="isMergePaymentDialogOpen" :payable-orders="selectedPayableRows" @confirm="confirmMergePayment" />
</div>
</template>
<script>
import { listPayable } from "@/api/finance/payable";
import EditForm from './components/EditForm.vue';
import MergePaymentDialog from './components/MergePaymentDialog.vue';
export default {
name: "Payable",
components: { EditForm, MergePaymentDialog },
dicts: ['product_type', 'payment_status', 'invoice_status'],
data() {
return {
// (other data properties)
//
open: false,
//
selectedRow: {},
//
loading: true,
//
showSearch: true,
//
total: 0,
//
payableList: [],
//
dateRange: [],
//
estimatedPaymentDateRange: [],
//
queryParams: {
pageNum: 1,
pageSize: 10,
projectCode: null,
projectName: null,
payableBillCode: null,
vendorName: null,
orderCode: null,
inventoryCode: null,
productType: null,
paymentStatus: null,
createTimeStart: null,
createTimeEnd: null,
estimatedPaymentTimeStart: null,
estimatedPaymentTimeEnd: null
},
//
selectedPayableRows: [],
//
isMergePaymentDialogOpen: false
};
},
created() {
this.getList();
},
methods: {
/** 查询采购应付单列表 */
getList() {
this.loading = true;
if (null != this.dateRange && '' != this.dateRange) {
this.queryParams.createTimeStart = this.dateRange[0];
this.queryParams.createTimeEnd = this.dateRange[1];
}
if (null != this.estimatedPaymentDateRange && '' != this.estimatedPaymentDateRange) {
this.queryParams.estimatedPaymentTimeStart = this.estimatedPaymentDateRange[0];
this.queryParams.estimatedPaymentTimeEnd = this.estimatedPaymentDateRange[1];
}
listPayable(this.queryParams).then(response => {
this.payableList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = [];
this.estimatedPaymentDateRange = [];
this.resetForm("queryForm");
this.queryParams.createTimeStart=null;
this.queryParams.createTimeEnd=null;
this.queryParams.estimatedPaymentTimeStart=null;
this.queryParams.estimatedPaymentTimeEnd=null;
this.handleQuery();
},
/** 修改按钮操作 */
handleUpdate(row) {
this.selectedRow = row;
this.open = true;
},
/** 删除按钮操作 */
handleDelete(row) {
this.$modal.confirm('是否确认删除采购应付单编号为"' + row.payableBillCode + '"的数据项?').then(function() {
return Promise.resolve();
}).then(() => {
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 生成付款单按钮操作 */
handleGeneratePayment(row) {
console.log("handleGeneratePayment", row);
},
/** 生成收票单按钮操作 */
handleGenerateInvoice(row) {
console.log("handleGenerateInvoice", row);
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.selectedPayableRows = selection;
},
/** 合并并发起付款单按钮操作 */
handleMergeAndInitiatePayment() {
if (this.selectedPayableRows.length === 0) {
this.$modal.msgWarning("请选择至少一条应付单进行合并操作");
return;
}
let vendorLength = new Set(this.selectedPayableRows.map(item=>item.vendorCode)).size;
if (vendorLength > 1) {
this.$modal.msgWarning("请选择同一家供应商的应付单进行合并操作");
return;
}
this.isMergePaymentDialogOpen = true;
},
/** 确认合并付款单操作 */
confirmMergePayment(paymentData) {
console.log("Merged payment data received:", paymentData);
// TODO: Call backend API to initiate merged payment
// this.$modal.msgSuccess("");
this.isMergePaymentDialogOpen = false;
this.getList(); // Refresh the list
}
}
};
</script>

View File

@ -28,7 +28,7 @@
<!-- 操作按钮栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['inventory:outer:export']"></el-button>
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['inventory:outer:export']"></el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>

View File

@ -534,15 +534,15 @@ export default {
/** 分配数据权限操作 */
handleDataScope(row) {
this.reset()
const deptTreeSelect = this.getDeptTree(row.roleId)
// const deptTreeSelect = this.getDeptTree(row.roleId)
getRole(row.roleId).then(response => {
this.form = response.data
this.openDataScope = true
this.$nextTick(() => {
deptTreeSelect.then(res => {
this.$refs.dept.setCheckedKeys(res.checkedKeys)
})
})
// this.$nextTick(() => {
// deptTreeSelect.then(res => {
// this.$refs.dept.setCheckedKeys(res.checkedKeys)
// })
// })
})
this.title = "分配数据权限"
},

View File

@ -0,0 +1,72 @@
flowable:
database-schema-update: false
ruoyi:
excelTemplate: /home/application/excelTemplate
profile: /home/application/uploadPath
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://121.199.168.157:3306/unis_pms?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowMultiQueries=true
username: root
password: unis@db
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
unis:
order:
# 执行单截止时间
endHour: 96
mail:
enabled: false

View File

@ -0,0 +1,35 @@
package com.ruoyi.sip.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.sip.oms.domain.OmsPayablePaymentPlan;
import com.ruoyi.sip.oms.service.IOmsPayablePaymentPlanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/finance/payable/plan")
public class OmsPayablePlanController extends BaseController {
@Autowired
private IOmsPayablePaymentPlanService paymentPlanService;
@GetMapping("/{payableBillId}")
public AjaxResult getPaymentPlan(@PathVariable("payableBillId") Long payableBillId) {
return AjaxResult.success(paymentPlanService.selectOmsPayablePaymentPlanListByPayableBillId(payableBillId));
}
@PostMapping("/{payableBillId}")
public AjaxResult updatePaymentPlan(@PathVariable("payableBillId") Long payableBillId, @RequestBody List<OmsPayablePaymentPlan> paymentPlanList) {
paymentPlanService.updatePaymentPlans(payableBillId, paymentPlanList);
return AjaxResult.success();
}
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.sip.oms.domain;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class OmsPayablePaymentPlan extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 主键 */
private Long id;
/** 应付单ID */
private Long payableBillId;
/** 计划付款时间 */
private Date planPaymentDate;
/** 计划付款金额 */
private BigDecimal planAmount;
/** 计划付款比例 */
private BigDecimal planRate;
}

View File

@ -0,0 +1,63 @@
package com.ruoyi.sip.oms.mapper;
import java.util.List;
import com.ruoyi.sip.oms.domain.OmsPayablePaymentPlan;
import org.apache.ibatis.annotations.Param;
public interface OmsPayablePaymentPlanMapper {
/**
* ID
*
* @param payableBillId ID
* @return
*/
public List<OmsPayablePaymentPlan> selectOmsPayablePaymentPlanListByPayableBillId(Long payableBillId);
/**
*
*
* @param omsPayablePaymentPlan
* @return
*/
public int insertOmsPayablePaymentPlan(OmsPayablePaymentPlan omsPayablePaymentPlan);
/**
*
*
* @param omsPayablePaymentPlan
* @return
*/
public int updateOmsPayablePaymentPlan(OmsPayablePaymentPlan omsPayablePaymentPlan);
/**
*
*
* @param omsPayablePaymentPlanList
* @return
*/
public int batchOmsPayablePaymentPlan(List<OmsPayablePaymentPlan> omsPayablePaymentPlanList);
/**
* ID
*
* @param payableBillId ID
* @return
*/
public int deleteOmsPayablePaymentPlanByPayableBillId(Long payableBillId);
/**
* ID
*
* @param id ID
* @return
*/
public int deleteOmsPayablePaymentPlanById(Long id);
/**
* IDID
*
* @param payableBillId ID
* @return ID
*/
public List<Long> selectOmsPayablePaymentPlanIdsByPayableBillId(Long payableBillId);
}

View File

@ -0,0 +1,22 @@
package com.ruoyi.sip.oms.service;
import java.util.List;
import com.ruoyi.sip.oms.domain.OmsPayablePaymentPlan;
public interface IOmsPayablePaymentPlanService {
/**
* ID
*
* @param payableBillId ID
* @return
*/
public List<OmsPayablePaymentPlan> selectOmsPayablePaymentPlanListByPayableBillId(Long payableBillId);
/**
*
*
* @param payableBillId ID
* @param paymentPlanList
*/
public void updatePaymentPlans(Long payableBillId, List<OmsPayablePaymentPlan> paymentPlanList);
}

View File

@ -0,0 +1,58 @@
package com.ruoyi.sip.oms.service.impl;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.ruoyi.common.utils.ShiroUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.sip.oms.mapper.OmsPayablePaymentPlanMapper;
import com.ruoyi.sip.oms.domain.OmsPayablePaymentPlan;
import com.ruoyi.sip.oms.service.IOmsPayablePaymentPlanService;
@Service
public class OmsPayablePaymentPlanServiceImpl implements IOmsPayablePaymentPlanService {
@Autowired
private OmsPayablePaymentPlanMapper omsPayablePaymentPlanMapper;
@Override
public List<OmsPayablePaymentPlan> selectOmsPayablePaymentPlanListByPayableBillId(Long payableBillId) {
return omsPayablePaymentPlanMapper.selectOmsPayablePaymentPlanListByPayableBillId(payableBillId);
}
@Override
@Transactional
public void updatePaymentPlans(Long payableBillId, List<OmsPayablePaymentPlan> paymentPlanList) {
// 获取数据库中现有付款计划的ID
List<Long> existingPlanIds = omsPayablePaymentPlanMapper.selectOmsPayablePaymentPlanIdsByPayableBillId(payableBillId);
Set<Long> existingPlanIdSet = existingPlanIds.stream().collect(Collectors.toSet());
// 用于存放前端传入的有效非新增或已删除的计划ID
Set<Long> incomingPlanIdSet = paymentPlanList.stream()
.filter(plan -> plan.getId() != null)
.map(OmsPayablePaymentPlan::getId)
.collect(Collectors.toSet());
for (OmsPayablePaymentPlan plan : paymentPlanList) {
plan.setPayableBillId(payableBillId);
if (plan.getId() == null) {
// 新增付款计划
plan.setCreateBy(ShiroUtils.getLoginName());
omsPayablePaymentPlanMapper.insertOmsPayablePaymentPlan(plan);
} else if (existingPlanIdSet.contains(plan.getId())) {
// 更新现有付款计划
omsPayablePaymentPlanMapper.updateOmsPayablePaymentPlan(plan);
}
// 如果plan.getId()不为null但不在existingPlanIdSet中说明是前端新添加但带有id的脏数据或者是非法数据这里不处理
}
// 删除数据库中存在但前端未提交的付款计划
existingPlanIdSet.removeAll(incomingPlanIdSet);
for (Long idToDelete : existingPlanIdSet) {
omsPayablePaymentPlanMapper.deleteOmsPayablePaymentPlanById(idToDelete);
}
}
}