feat(finance): 新增收款申请付款与退款功能

- 新增申请付款对话框组件,支持文件上传与预览
- 新增申请退款对话框组件,用于处理退款请求
- 修改收款详情抽屉,增加附件预览与下载功能
- 优化付款单编辑表单,统一文件预览组件
- 在收款管理页面添加申请付款与退款操作按钮
- 后端新增收款申请、退款及附件上传接口
- 数据库映射文件更新,增加原始账单ID字段
- 实现收款单据服务层逻辑,包括退款状态管理
- 增加全局文件预览组件引用与相关方法
- 更新权限控制与业务流程处理逻辑
dev_1.0.1
chenhao 2025-12-19 16:40:17 +08:00
parent c257cdc5cd
commit 1111d285c5
16 changed files with 844 additions and 87 deletions

View File

@ -73,3 +73,24 @@ export function addReceipt(data) {
needLoading: true
})
}
export function applyReceipt(data) {
return request({
url: '/finance/receipt/applyReceipt',
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: data,
needLoading: true
})
}
// 申请退款
export function submitRefund(data) {
return request({
url: '/finance/receipt/applyRefund',
method: 'post',
data: data,
needLoading: true
})
}

View File

@ -164,13 +164,7 @@
</el-tabs>
</div>
<el-dialog :visible.sync="pdfPreviewVisible" width="80%" append-to-body top="5vh" title="PDF预览">
<iframe :src="currentPdfUrl" width="100%" height="600px" frameborder="0"></iframe>
</el-dialog>
<el-dialog :visible.sync="imagePreviewVisible" width="60%" append-to-body top="5vh" title="图片预览">
<img :src="currentImageUrl" style="width: 100%;max-height: 60vh" />
</el-dialog>
<GlobalFilePreview ref="filePreview" />
</div>
<!-- <div slot="footer" class="dialog-footer">-->
<!-- <el-button @click="handleClose"> </el-button>-->
@ -185,7 +179,7 @@ import PaymentPlan from './PaymentPlan.vue';
import ReceivingTicketPlan from './ReceivingTicketPlan.vue';
import { getPayable } from "@/api/finance/payable";
import ReceiptPlan from "@/views/finance/receivable/components/ReceiptPlan.vue";
import request from '@/utils/request';
import GlobalFilePreview from '@/components/GlobalFilePreview';
export default {
name: "EditForm",
@ -193,7 +187,8 @@ export default {
components: {
ReceiptPlan,
PaymentPlan,
ReceivingTicketPlan
ReceivingTicketPlan,
GlobalFilePreview
},
props: {
visible: {
@ -210,10 +205,6 @@ export default {
internalVisible: this.visible, // Local copy of the visible prop
activeTab: 'details',
formData: {},
pdfPreviewVisible: false,
currentPdfUrl: '',
imagePreviewVisible: false,
currentImageUrl: ''
};
},
watch: {
@ -239,40 +230,11 @@ export default {
this.formData = res.data;
});
},
isPdf(filePath) {
return filePath && filePath.toLowerCase().endsWith('.pdf');
},
getImageUrl(resource) {
return process.env.VUE_APP_BASE_API + "/common/download/resource?resource=" + resource;
},
handlePreview(attachment) {
if (!attachment) return;
if (this.isPdf(attachment.filePath)) {
request({
url: '/common/download/resource',
method: 'get',
params: { resource: attachment.filePath },
responseType: 'blob'
}).then(res => {
const blob = new Blob([res.data], { type: 'application/pdf' });
this.currentPdfUrl = URL.createObjectURL(blob);
this.pdfPreviewVisible = true;
});
} else {
this.currentImageUrl = this.getImageUrl(attachment.filePath);
this.imagePreviewVisible = true;
}
this.$refs.filePreview.handlePreview(attachment);
},
downloadFile(attachment){
if (attachment){
const link = document.createElement('a');
link.href = process.env.VUE_APP_BASE_API + "/common/download/resource?resource=" +attachment.filePath;
link.download = attachment.fileName || 'receipt';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
this.$refs.filePreview.downloadFile(attachment);
},
getSummaries(param) {
const { columns, data } = param;

View File

@ -0,0 +1,298 @@
<template>
<el-dialog title="申请付款" :visible.sync="visible" width="1000px" append-to-body :before-close="handleClose"
custom-class="apply-payment-dialog">
<el-row :gutter="20">
<!-- Left Side: Form Data -->
<el-col :span="12">
<div class="form-tip">请选择客户的付款方式并确认客户打款的账户信息提交至财务审批</div>
<el-form ref="form" :model="form" label-width="120px" size="small">
<el-form-item label="付款方式" prop="receiptMethod">
<el-select v-model="form.receiptMethod" placeholder="请选择付款方式" style="width: 100%">
<el-option
v-for="dict in dicts.payment_method"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="账户名称" prop="receiptAccountName">
<el-input v-model="form.receiptAccountName" placeholder="请输入账户名称"/>
</el-form-item>
<el-form-item label="银行账号" prop="payBankNumber">
<el-input v-model="form.receiptBankNumber" placeholder="请输入银行账号"/>
</el-form-item>
<el-form-item label="银行开户行" prop="receiptBankOpenAddress">
<el-input v-model="form.receiptBankOpenAddress" placeholder="请输入银行开户行"/>
</el-form-item>
<el-form-item label="银行行号" prop="bankNumber">
<el-input v-model="form.bankNumber" placeholder="请输入银行行号"/>
</el-form-item>
<!-- New Field: Client Payment Image Upload -->
<el-form-item label="客户付款图" prop="file">
<el-upload
ref="upload"
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:show-file-list="false"
accept=".jpg,.jpeg,.png,.pdf"
>
<el-button size="mini" type="primary" icon="el-icon-upload2">{{
form.file ? '重新上传' : '点击上传'
}}
</el-button>
<div slot="tip" class="el-upload__tip">支持JPG/PNG/PDF格式</div>
</el-upload>
<div v-if="form.file" class="file-name-tip">
<i class="el-icon-document"></i> {{ form.fileName }}
</div>
</el-form-item>
<el-form-item label="含税金额" prop="totalPriceWithTax">
<el-input v-model="form.totalPriceWithTax" :disabled="true"/>
</el-form-item>
<!-- New Field: Confirm Receipt Amount -->
<el-form-item label="确认收款金额" prop="confirmAmount">
<el-input v-model="form.confirmAmount" placeholder="请输入确认收款金额"/>
</el-form-item>
<!-- New Field: Remarks -->
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="form.remark" :rows="3" placeholder="请输入备注"/>
</el-form-item>
</el-form>
</el-col>
<!-- Right Side: Preview -->
<el-col :span="12">
<div class="preview-container">
<div v-if="previewUrl" class="preview-content">
<div v-if="isPreviewPdf" class="pdf-preview">
<iframe :src="previewUrl" width="100%" height="100%" frameborder="0"></iframe>
</div>
<div v-else class="image-preview">
<img :src="previewUrl" alt="预览图片"/>
</div>
</div>
<div v-else class="preview-placeholder">
<div class="placeholder-icon">
<i class="el-icon-picture-outline"></i>
</div>
<div class="placeholder-text">上传文件后在此处预览</div>
</div>
</div>
</el-col>
</el-row>
<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 {applyReceipt} from "@/api/finance/receive";
export default {
name: "ApplyPaymentDialog",
props: {
visible: {
type: Boolean,
default: false
},
receiptData: {
type: Object,
default: () => ({})
},
dicts: {
type: Object,
default: () => ({})
}
},
data() {
return {
form: {
receiptMethod: null,
receiptAccountName: null,
receiptBankNumber: null,
receiptBankOpenAddress: null,
bankNumber: null,
totalPriceWithTax: null,
confirmAmount: null,
remark: null,
file: null,
fileName: '',
id: this.receiptData.id
},
previewUrl: '',
isPreviewPdf: false
};
},
watch: {
visible(val) {
if (val) {
this.reset();
// Initialize form with receiptData
this.form = {
id: this.receiptData.id,
receiptMethod: this.receiptData.receiptMethod,
receiptAccountName: this.receiptData.receiptAccountName,
receiptBankNumber: this.receiptData.receiptBankNumber,
receiptBankOpenAddress: this.receiptData.receiptBankOpenAddress,
bankNumber: this.receiptData.bankNumber,
totalPriceWithTax: this.receiptData.totalPriceWithTax,
confirmAmount: null,
remark: null,
file: null,
fileName: ''
};
}
}
},
methods: {
reset() {
this.form = {
receiptMethod: null,
receiptAccountName: null,
receiptBankNumber: null,
receiptBankOpenAddress: null,
bankNumber: null,
totalPriceWithTax: null,
confirmAmount: null,
remark: null,
file: null,
fileName: ''
};
this.previewUrl = '';
this.isPreviewPdf = false;
if (this.$refs.form) {
this.$refs.form.resetFields();
}
},
handleClose() {
this.$emit("update:visible", false);
},
handleFileChange(file) {
const isLt10M = file.size / 1024 / 1024 < 10;
const isAcceptedType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.raw.type);
if (!isAcceptedType) {
this.$modal.msgError('上传文件只能是 JPG/PNG/PDF 格式!');
// Remove file from upload list if needed, though we use show-file-list="false"
return;
}
if (!isLt10M) {
this.$modal.msgError('上传文件大小不能超过 10MB!');
return;
}
this.form.file = file.raw;
this.form.fileName = file.name;
this.isPreviewPdf = file.raw.type === 'application/pdf';
this.previewUrl = URL.createObjectURL(file.raw);
},
handleFileRemove() {
this.form.file = null;
this.form.fileName = '';
this.previewUrl = '';
},
handleSubmit() {
this.$refs["form"].validate(valid => {
if (valid) {
// Construct FormData
const formData = new FormData();
// Append regular fields
Object.keys(this.form).forEach(key => {
if (key !== 'file' && key !== 'fileName' && this.form[key] !== null && this.form[key] !== undefined) {
formData.append(key, this.form[key]);
}
});
// Append file if exists
if (this.form.file) {
formData.append("file", this.form.file);
}
// Since applyPaymentApi usually takes JSON, we might need to verify if backend supports FormData
// Assuming we are sending FormData now.
applyReceipt(formData).then(response => {
this.$modal.msgSuccess("申请付款提交成功");
this.$emit("submit");
this.handleClose();
}).catch(error => {
console.error("申请付款提交失败", error);
});
}
});
}
}
};
</script>
<style scoped>
.form-tip {
color: #1890ff;
font-size: 14px;
margin-bottom: 20px;
padding: 8px 16px;
background-color: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 4px;
}
.preview-container {
width: 100%;
height: 500px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #f5f7fa;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.preview-content {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.image-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.pdf-preview {
width: 100%;
height: 100%;
}
.preview-placeholder {
text-align: center;
color: #909399;
}
.placeholder-icon {
font-size: 48px;
margin-bottom: 10px;
}
.placeholder-text {
font-size: 14px;
}
.file-name-tip {
margin-top: 5px;
font-size: 12px;
color: #606266;
}
</style>

View File

@ -0,0 +1,152 @@
<template>
<el-dialog title="申请退款" :visible.sync="visible" width="600px" append-to-body :before-close="handleClose"
custom-class="apply-refund-dialog">
<div class="form-tip">请选择客户的退款方式并确认客户收款的账户信息提交至财务审批</div>
<el-form ref="form" :model="form" label-width="120px" size="small">
<el-form-item label="退款方式" prop="receiptMethod">
<el-select v-model="form.receiptMethod" placeholder="请选择退款方式" style="width: 100%">
<el-option
v-for="dict in dicts.payment_method"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="账户名称" prop="receiptAccountName">
<el-input v-model="form.receiptAccountName" placeholder="请输入账户名称"/>
</el-form-item>
<el-form-item label="银行账号" prop="receiptBankNumber">
<el-input v-model="form.receiptBankNumber" placeholder="请输入银行账号"/>
</el-form-item>
<el-form-item label="银行开户行" prop="receiptBankOpenAddress">
<el-input v-model="form.receiptBankOpenAddress" placeholder="请输入银行开户行"/>
</el-form-item>
<el-form-item label="银行行号" prop="bankNumber">
<el-input v-model="form.bankNumber" placeholder="请输入银行行号"/>
</el-form-item>
<el-form-item label="含税金额" prop="totalPriceWithTax">
<el-input v-model="form.totalPriceWithTax" :disabled="true"/>
</el-form-item>
<!-- Field: Confirm Refund Amount -->
<el-form-item label="确认退款金额" prop="confirmAmount">
<el-input v-model="form.confirmAmount" placeholder="请输入确认退款金额"/>
</el-form-item>
<!-- Field: Remarks -->
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="form.remark" :rows="3" placeholder="请输入备注"/>
</el-form-item>
</el-form>
<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 {submitRefund} from "@/api/finance/receive";
export default {
name: "ApplyRefundDialog",
props: {
visible: {
type: Boolean,
default: false
},
receiptData: {
type: Object,
default: () => ({})
},
dicts: {
type: Object,
default: () => ({})
}
},
data() {
return {
form: {
receiptMethod: null,
receiptAccountName: null,
receiptBankNumber: null,
receiptBankOpenAddress: null,
bankNumber: null,
totalPriceWithTax: null,
confirmAmount: null,
remark: null,
id: this.receiptData.id
}
};
},
watch: {
visible(val) {
if (val) {
this.reset();
// Initialize form with receiptData
this.form = {
id: this.receiptData.id,
receiptMethod: this.receiptData.receiptMethod,
receiptAccountName: this.receiptData.receiptAccountName,
receiptBankNumber: this.receiptData.receiptBankNumber,
receiptBankOpenAddress: this.receiptData.receiptBankOpenAddress,
bankNumber: this.receiptData.bankNumber,
totalPriceWithTax: this.receiptData.totalPriceWithTax,
confirmAmount: null,
remark: null
};
}
}
},
methods: {
reset() {
this.form = {
receiptMethod: null,
receiptAccountName: null,
receiptBankNumber: null,
receiptBankOpenAddress: null,
bankNumber: null,
totalPriceWithTax: null,
confirmAmount: null,
remark: null
};
if (this.$refs.form) {
this.$refs.form.resetFields();
}
},
handleClose() {
this.$emit("update:visible", false);
},
handleSubmit() {
this.$refs["form"].validate(valid => {
if (valid) {
// Send as JSON since no file upload
submitRefund(this.form).then(response => {
this.$modal.msgSuccess("申请退款提交成功");
this.$emit("submit");
this.handleClose();
}).catch(error => {
console.error("申请退款提交失败", error);
});
}
});
}
}
};
</script>
<style scoped>
.form-tip {
color: #1890ff;
font-size: 14px;
margin-bottom: 20px;
padding: 8px 16px;
background-color: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 4px;
}
</style>

View File

@ -43,7 +43,12 @@
</el-col>
<el-col :span="8">
<div class="detail-item">收款图/回执单:
<dict-tag :options="dict.type.approve_status" :value="detail.approveStatus"/>
<span v-if="detail.attachment">
<el-button type="text" size="mini" icon="el-icon-view" @click="handlePreview(detail.attachment)"></el-button>
<el-button type="text" size="mini" icon="el-icon-download"
@click="downloadFile(detail.attachment)">下载</el-button>
</span>
<span v-else>-</span>
</div>
</el-col>
</el-row>
@ -56,7 +61,7 @@
</el-col>
<el-col :span="8">
<div class="detail-item">上传人姓名:
<dict-tag :options="dict.type.approve_status" :value="detail.approveStatus"/>
{{ detail.attachment && detail.attachment.createByName || '-' }}
</div>
</el-col>
</el-row>
@ -108,13 +113,20 @@
<el-table-column property="receiptRate" label="本次收款比例(%)"></el-table-column>
</el-table>
</div>
<GlobalFilePreview ref="filePreview" />
</div>
</el-drawer>
</template>
<script>
import GlobalFilePreview from '@/components/GlobalFilePreview';
export default {
name: "DetailDrawer",
components: {
GlobalFilePreview
},
props: {
visible: {
type: Boolean,
@ -125,11 +137,21 @@ export default {
default: () => null,
},
},
data() {
return {
};
},
dicts:['receive_bill_type','approve_status','receive_status'],
methods: {
handleClose() {
this.$emit("update:visible", false);
},
handlePreview(attachment) {
this.$refs.filePreview.handlePreview(attachment);
},
downloadFile(attachment) {
this.$refs.filePreview.downloadFile(attachment);
}
},
};
</script>

View File

@ -19,8 +19,8 @@
<div class="receipt-card-content">
<div class="receipt-details">
<div class="detail-item">
<span class="item-label">票据类型</span>
<span class="item-value"><dict-tag :options="dict.type.finance_invoice_type" :value="attachment.ticketType"/></span>
<span class="item-label">付款方式</span>
<span class="item-value"><dict-tag :options="dict.type.payment_method" :value="receiptData.receiptMethod"/></span>
</div>
<div class="detail-item">
<span class="item-label">{{ titleText }}</span>
@ -53,7 +53,7 @@
<div class="detail-item">
<span class="item-label">收款总额</span>
<span class="item-value">{{ receiptData.totalAmount }}</span>
<span class="item-value">{{ receiptData.totalPriceWithTax }}</span>
</div>
<div class="detail-item">
<span class="item-label">备注</span>
@ -93,15 +93,8 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form :model="uploadForm" ref="uploadForm" :rules="rules" label-width="120px" size="medium" >
<el-form-item label="票据类型" prop="ticketType" required>
<el-select v-model="uploadForm.ticketType" placeholder="请选择票据类型">
<el-option
v-for="item in dict.type.finance_invoice_type"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
<el-form-item label="票据类型" prop="receiptMethod" >
<dict-tag :options="dict.type.payment_method" :value="receiptData.receiptMethod"/>
</el-form-item>
<el-form-item label="收款附件" prop="file" required>
<div style="display: flex; flex-direction: column; align-items: flex-start;">
@ -119,8 +112,11 @@
<div class="el-upload__tip" style="line-height: 1.5; margin-top: 5px;">支持上传PNGJPGPDF文件格式</div>
</div>
</el-form-item>
<el-form-item label="收款总额">
<span>{{ receiptData.totalAmount }}</span>
<el-form-item label="含税总价">
<span>{{ receiptData.totalPriceWithTax }}</span>
</el-form-item>
<el-form-item label="确认含税总价">
<el-input v-model="uploadForm.totalPriceWithTax" ></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input
@ -170,14 +166,10 @@ export default {
},
receiptData: {
type: Object,
default: () => null,
default: () => {},
},
dicts: {
type: Object,
default: () => ({})
}
},
dicts:['finance_invoice_type'],
dicts:['payment_method'],
data() {
return {
loading: false,
@ -190,9 +182,6 @@ export default {
file: null
},
rules: {
ticketType: [
{ required: true, message: "请选择票据类型", trigger: "change" }
],
},
previewUrl: '',
isPreviewPdf: false,
@ -237,7 +226,7 @@ export default {
fetchAttachments() {
if (!this.receiptData.id) return;
this.loading = true;
getReceiveAttachments(this.receiptData.id, { type: 'ticket' })
getReceiveAttachments(this.receiptData.id, { type: 'receipt' })
.then(response => {
const data = response.data || [];
data.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
@ -342,9 +331,8 @@ export default {
const formData = new FormData();
formData.append("file", this.uploadForm.file);
formData.append("id", this.receiptData.id);
formData.append("relatedBillId", this.receiptData.id);
formData.append("remark", this.uploadForm.remark);
formData.append("ticketType", this.uploadForm.ticketType);
uploadReceiveAttachment(formData)
.then(response => {

View File

@ -165,6 +165,18 @@
icon="el-icon-document"
@click="handleReceipt(scope.row)"
>附件</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-money"
@click="handleApplyPayment(scope.row)"
>申请付款</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-money"
@click="handleApplyRefund(scope.row)"
>申请退款</el-button>
<el-button
size="mini"
type="text"
@ -172,13 +184,6 @@
v-show="scope.row.receiptBillType==='FROM_RECEIVABLE' && scope.row.receiptStatus==='1' &&(scope.row.approveStatus==='0' || scope.row.approveStatus==='3') "
@click="handleReturn(scope.row)"
>退回</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-refresh-left"
v-show="scope.row.approveStatus=='2'"
@click="handleRedRush(scope.row)"
>申请红冲</el-button>
</template>
</el-table-column>
</el-table>
@ -197,6 +202,10 @@
<add-form :visible.sync="addOpen" :dicts="dict.type" @submit="handleAddSubmit"></add-form>
<!-- 收款附件弹窗 -->
<receive-dialog :visible.sync="receiptOpen" :receipt-data="currentRow" :dicts="dict.type"></receive-dialog>
<!-- 申请付款弹窗 -->
<apply-payment-dialog :visible.sync="applyPaymentOpen" :receipt-data="currentRow" :dicts="dict.type" @submit="getList"></apply-payment-dialog>
<!-- 申请退款弹窗 -->
<apply-refund-dialog :visible.sync="applyRefundOpen" :receipt-data="currentRow" :dicts="dict.type" @submit="getList"></apply-refund-dialog>
</div>
</template>
@ -207,15 +216,19 @@ import { addDateRange } from "@/utils/ruoyi";
import DetailDrawer from "./components/DetailDrawer.vue";
import AddForm from "./components/AddForm.vue";
import ReceiveDialog from "./components/ReceiveDialog.vue";
import ApplyPaymentDialog from "./components/ApplyPaymentDialog.vue";
import ApplyRefundDialog from "./components/ApplyRefundDialog.vue";
export default {
name: "Receive",
components: {
DetailDrawer,
AddForm,
ReceiveDialog
ReceiveDialog,
ApplyPaymentDialog,
ApplyRefundDialog
},
dicts:['receipt_bill_type','approve_status','receipt_bill_status'],
dicts:['receipt_bill_type','approve_status','receipt_bill_status', 'payment_method'],
data() {
return {
//
@ -251,7 +264,11 @@ export default {
addOpen: false,
//
receiptOpen: false,
currentRow: {}
currentRow: {},
//
applyPaymentOpen: false,
// 退
applyRefundOpen: false,
};
},
created() {
@ -319,6 +336,16 @@ export default {
this.detailOpen = true;
});
},
/** 申请付款按钮操作 */
handleApplyPayment(row) {
this.currentRow = row;
this.applyPaymentOpen = true;
},
/** 申请退款按钮操作 */
handleApplyRefund(row) {
this.currentRow = row;
this.applyRefundOpen = true;
},
/** 收款附件按钮操作 */
handleReceipt(row) {
this.currentRow = row;
@ -334,7 +361,7 @@ export default {
}).catch(() => {});
},
handleReturn(row) {
this.$modal.confirm('是否确认退回收款单编号为"' + row.receiptBillCode + '"的数据项?').then(function() {
this.$modal.confirm('是否将该笔销售-应收单的:销售-收款单与销售-发票单同时退回至销售-应收单表').then(function() {
return returnReceive(row.id);
}).then(() => {
this.getList();

View File

@ -5,13 +5,19 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.sip.domain.OmsFinAttachment;
import com.ruoyi.sip.domain.OmsPaymentBill;
import com.ruoyi.sip.domain.OmsReceiptBill;
import com.ruoyi.sip.service.IOmsFinAttachmentService;
import com.ruoyi.sip.service.IOmsReceiptBillService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
/**
@ -26,6 +32,8 @@ public class OmsReceiptBillController extends BaseController {
@Autowired
private IOmsReceiptBillService omsReceiptBillService;
@Autowired
private IOmsFinAttachmentService omsFinAttachmentService;
@GetMapping("/list")
@ -91,4 +99,56 @@ public class OmsReceiptBillController extends BaseController {
return AjaxResult.error("操作失败:" + e.getMessage());
}
}
@GetMapping("/attachment/{id}")
@ResponseBody
public AjaxResult viewReceipt(@PathVariable("id") Long id, @RequestParam("type") String type)
{
return AjaxResult.success( omsFinAttachmentService.list(Collections.singletonList(id),type));
}
/**
*
*/
@RequiresPermissions("finance:receipt:uploadReceipt")
@Log(title = "上传回执单", businessType = BusinessType.UPDATE)
@PostMapping("/uploadReceipt")
@ResponseBody
public AjaxResult uploadReceipt(OmsFinAttachment attachment, @RequestParam("file") MultipartFile file)
{
try
{
attachment.setRelatedBillType(OmsFinAttachment.RelatedBillTypeEnum.RECEIPT.getCode());
return omsReceiptBillService.uploadReceipt(attachment, file);
}
catch (Exception e)
{
logger.error("上传回执单失败", e);
return AjaxResult.error("操作失败:" + e.getMessage());
}
}
@RequiresPermissions("finance:receipt:apply")
@Log(title = "申请收款", businessType = BusinessType.UPDATE)
@PostMapping("/applyReceipt")
@ResponseBody
public AjaxResult applyReceipt(OmsReceiptBill omsReceiptBill, @RequestParam("file") MultipartFile file) {
try {
omsReceiptBillService.applyReceipt(omsReceiptBill, file);
} catch (IOException e) {
return AjaxResult.error("操作失败:" + e.getMessage());
}
return AjaxResult.success();
}
/**
* 退
*/
@PostMapping("/applyRefund")
@ResponseBody
public AjaxResult applyRefund(@RequestBody OmsReceiptBill omsReceiptBill) {
try {
return omsReceiptBillService.applyRefund(omsReceiptBill);
} catch (Exception e) {
logger.error("申请退款失败", e);
return AjaxResult.error("操作失败:" + e.getMessage());
}
}
}

View File

@ -160,6 +160,8 @@ public class OmsReceiptBill {
* 退 0:退 1:退
*/
private String refundStatus;
private Long originalBillId;
private OmsFinAttachment attachment;
private List<ReceiptDetailDTO> detailDTOList;
@Getter
public enum ReceiptBillTypeEnum {
@ -182,6 +184,25 @@ public class OmsReceiptBill {
}
}
@Getter
public enum RefundStatusEnum {
/** 应付单生成 */
REFUNDED("REFUND_APPLIED", "已申请退款"),
WAIT_REFUNDED("WAIT_REFUNDED", "未退款")
;
;
private final String code;
private final String desc;
RefundStatusEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}
@Getter
public enum ReceiptStatusEnum {
/** 应付单生成 */

View File

@ -65,4 +65,6 @@ public interface OmsReceivableReceiptDetailMapper
List<OmsReceivableReceiptDetail> selectByPaymentPlanIds(List<Long> allReceiptPlanIds);
List<ReceiptDetailDTO> listReceivableByReceiptBillCode(List<String> receiptBillCode);
void insertBatch(List<OmsReceivableReceiptDetail> addList);
}

View File

@ -1,8 +1,11 @@
package com.ruoyi.sip.service;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.sip.domain.OmsFinAttachment;
import com.ruoyi.sip.domain.OmsReceiptBill;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
/**
@ -46,6 +49,12 @@ public interface IOmsReceiptBillService {
List<OmsReceiptBill> listRemainingAmountByPartnerCodeList(List<String> collect);
AjaxResult returnPaymentBill(Long id);
void applyReceipt(OmsReceiptBill omsReceiptBill, MultipartFile file) throws IOException;
AjaxResult applyRefund(OmsReceiptBill omsReceiptBill);
AjaxResult uploadReceipt(OmsFinAttachment attachment, MultipartFile file) throws IOException;
}

View File

@ -71,4 +71,6 @@ public interface IOmsReceivableReceiptDetailService
List<ReceiptDetailDTO> listReceivableByReceiptBillCode(String receiptBillCode);
List<OmsReceivableReceiptDetail> listByReceiptBillCode(String receiptBillCode);
void applyRefund(String originalReceiptBillCode, String targetReceiptBillCode);
}

View File

@ -3,25 +3,32 @@ package com.ruoyi.sip.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.enums.ApproveStatusEnum;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.sip.domain.OmsPayablePaymentDetail;
import com.ruoyi.sip.domain.OmsPaymentBill;
import com.ruoyi.sip.domain.OmsReceiptBill;
import com.ruoyi.sip.domain.OmsReceivableReceiptDetail;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.sip.domain.*;
import com.ruoyi.sip.domain.dto.ReceiptDetailDTO;
import com.ruoyi.sip.dto.WriteOffRequestDto;
import com.ruoyi.sip.mapper.OmsReceiptBillMapper;
import com.ruoyi.sip.service.IOmsFinAttachmentService;
import com.ruoyi.sip.service.IOmsReceiptBillService;
import com.ruoyi.sip.service.IOmsReceivableBillService;
import com.ruoyi.sip.service.IOmsReceivableReceiptDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
@ -39,6 +46,8 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService {
private IOmsReceivableReceiptDetailService omsReceivableReceiptDetailService;
@Autowired
private IOmsReceivableBillService receivableBillService;
@Autowired
private IOmsFinAttachmentService attachmentService;
/**
*
@ -55,8 +64,14 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService {
@Override
public OmsReceiptBill queryById(Long id) {
OmsReceiptBill receiptBill = omsReceiptBillMapper.queryById(id);
if (receiptBill == null){
return null;
}
List<ReceiptDetailDTO> receiptDetailDTOS = omsReceivableReceiptDetailService.listReceivableByReceiptBillCode(receiptBill.getReceiptBillCode());
receiptBill.setDetailDTOList(receiptDetailDTOS);
List<OmsFinAttachment> list = attachmentService.list(Collections.singletonList(receiptBill.getId()), OmsFinAttachment.RelatedBillTypeEnum.RECEIPT.getCode());
Optional<OmsFinAttachment> first = list.stream().filter(item -> !"2".equals(item.getDelFlag())).max((o1, o2) -> o2.getCreateTime().compareTo(o1.getCreateTime()));
receiptBill.setAttachment(first.orElse(null));
return receiptBill;
}
@ -144,6 +159,143 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService {
}
}
@Override
public void applyReceipt(OmsReceiptBill omsReceiptBill, MultipartFile file) throws IOException {
omsReceiptBill.setApproveStatus(ApproveStatusEnum.WAIT_APPROVE.getCode());
omsReceiptBillMapper.update(omsReceiptBill);
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
SysUser loginUser = ShiroUtils.getSysUser();
OmsFinAttachment attachment = new OmsFinAttachment();
attachment.setRelatedBillType(OmsFinAttachment.RelatedBillTypeEnum.RECEIPT.getCode());
attachment.setRelatedBillId(omsReceiptBill.getId());
attachment.setFileName(file.getOriginalFilename());
attachment.setFilePath(fileName);
attachment.setFileSize(file.getSize());
attachment.setFileType(file.getContentType());
attachment.setCreateBy(loginUser.getUserId().toString());
attachmentService.insertOmsFinAttachment(attachment);
}
@Override
public AjaxResult applyRefund(OmsReceiptBill omsReceiptBill) {
// 1. 验证原始付款单
OmsReceiptBill receiptBill = queryById(omsReceiptBill.getId());
if (receiptBill == null) {
return AjaxResult.error("原始收款单不存在");
}
if (OmsReceiptBill.ReceiptBillTypeEnum.REFUND.getCode().equals(receiptBill.getReceiptBillType())) {
if (!ApproveStatusEnum.WAIT_APPROVE.getCode().equals(receiptBill.getApproveStatus()) ||
!ApproveStatusEnum.APPROVE_REJECT.getCode().equals(receiptBill.getApproveStatus())){
return AjaxResult.error("当前订单已提交审批,请刷新后重试");
}
receiptBill.setApproveStatus(ApproveStatusEnum.WAIT_APPROVE.getCode());
//todo 开启流程
update(receiptBill);
return AjaxResult.success("退款申请成功!");
}
if (!OmsReceiptBill.ReceiptStatusEnum.PAYMENT.getCode().equals(receiptBill.getReceiptStatus())) {
return AjaxResult.error("只有已付款的订单才能申请退款");
}
if (OmsReceiptBill.RefundStatusEnum.REFUNDED.getCode().equals(receiptBill.getRefundStatus())) {
return AjaxResult.error("该付款单已申请过退款,请勿重复操作");
}
// 2. 创建新的退款单
OmsReceiptBill refundBill = new OmsReceiptBill();
// 复制属性并取反金额
refundBill.setTotalPriceWithTax(receiptBill.getTotalPriceWithTax().negate());
refundBill.setTotalPriceWithoutTax(receiptBill.getTotalPriceWithoutTax().negate());
refundBill.setTaxAmount(receiptBill.getTaxAmount().negate());
refundBill.setPartnerCode(receiptBill.getPartnerCode());
refundBill.setOrderCode(receiptBill.getOrderCode());
refundBill.setReceiptAccountName(omsReceiptBill.getReceiptAccountName());
refundBill.setReceiptBankNumber(omsReceiptBill.getReceiptBankNumber());
refundBill.setReceiptBankOpenAddress(omsReceiptBill.getReceiptBankOpenAddress());
refundBill.setBankNumber(omsReceiptBill.getBankNumber());
refundBill.setReceiptBillCode(generateReceiptBillCode());
// 设置新属性
refundBill.setReceiptBillType(OmsReceiptBill.ReceiptBillTypeEnum.REFUND.getCode());
refundBill.setReceiptStatus(OmsReceiptBill.ReceiptStatusEnum.WAIT_PAYMENT.getCode());
refundBill.setApproveStatus(ApproveStatusEnum.WAIT_APPROVE.getCode());
refundBill.setOriginalBillId(receiptBill.getId());
refundBill.setReceiptTime(null);
refundBill.setReceiptMethod(omsReceiptBill.getReceiptMethod());
refundBill.setRemark("退款-关联原付款单:" + omsReceiptBill.getReceiptBillCode());
insert(refundBill);
// 3. 更新原始付款单状态
receiptBill.setRefundStatus(OmsReceiptBill.RefundStatusEnum.REFUNDED.getCode());
update(receiptBill);
//4 创建付款明细
omsReceivableReceiptDetailService.applyRefund(receiptBill.getReceiptBillCode(), refundBill.getReceiptBillCode());
//5. 开始退款审批流程
// todoService.startProcessDeleteBefore(originalBill.getPaymentBillCode(), originalBill.getPaymentBillCode(),
// new HashMap<String, Object>() {{
// put("applyUserName", ShiroUtils.getSysUser().getUserName());
// put("applyUser", ShiroUtils.getUserId());
// }}
// , processConfig.getDefinition().getFinanceRefund());
return AjaxResult.success("退款申请已提交,新的退款单号为:" + refundBill.getReceiptBillCode());
}
@Override
public AjaxResult uploadReceipt(OmsFinAttachment attachment, MultipartFile file) throws IOException {
OmsReceiptBill receiptBill = queryById(attachment.getRelatedBillId());
if (receiptBill == null) {
return AjaxResult.error("收款单不存在");
}
if (file.isEmpty())
{
return AjaxResult.error("上传文件不能为空");
}
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
SysUser loginUser = ShiroUtils.getSysUser();
attachment.setFileName(file.getOriginalFilename());
attachment.setFilePath(fileName);
attachment.setFileSize(file.getSize());
attachment.setFileType(file.getContentType());
attachment.setCreateBy(loginUser.getUserId().toString());
attachmentService.insertOmsFinAttachment(attachment);
receiptBill.setActualReceiptTime(DateUtils.getNowDate());
receiptBill.setReceiptStatus(OmsReceiptBill.ReceiptStatusEnum.PAYMENT.getCode());
update(receiptBill);
if (!receiptBill.getReceiptBillType().equals(OmsReceiptBill.ReceiptBillTypeEnum.PRE_RECEIPT.getCode())) {
//todo 自动核销
// List<OmsPayablePaymentDetail> omsPayablePaymentDetails = omsReceivableReceiptDetailService.listByReceiptCode(receiptBill.getReceiptBillCode());
// WriteOffRequestDto writeOffRequestDto = new WriteOffRequestDto();
// writeOffRequestDto.setReceiptBillId(receiptBill.getId());
// writeOffRequestDto.setDetailList(omsPayablePaymentDetails);
// writeOffRequestDto.setVendorCode(paymentBill.getVendorCode());
// writeOffRequestDto.setRemark(StrUtil.format("{}自动核销数据:{}", DateUtils.getTime(), paymentBill.getPaymentBillCode()));
// // 新增核销记录
// writeOffService.autoWriteOff(writeOffRequestDto);
}
return AjaxResult.success(attachment);
}
}

View File

@ -1,13 +1,16 @@
package com.ruoyi.sip.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.sip.domain.OmsFinAttachment;
import com.ruoyi.sip.domain.OmsPayablePaymentDetail;
import com.ruoyi.sip.domain.dto.PaymentBillPayableDetailDTO;
@ -156,4 +159,28 @@ public class OmsReceivableReceiptDetailServiceImpl implements IOmsReceivableRece
omsReceivableReceiptDetail.setReceiptBillCode(receiptBillCode);
return omsReceivableReceiptDetailMapper.list(omsReceivableReceiptDetail);
}
@Override
public void applyRefund(String originalReceiptBillCode, String targetReceiptBillCode) {
OmsReceivableReceiptDetail detail = new OmsReceivableReceiptDetail();
detail.setReceiptBillCode(originalReceiptBillCode);
List<OmsReceivableReceiptDetail> list = omsReceivableReceiptDetailMapper.list(detail);
if (CollUtil.isNotEmpty(list)){
List<OmsReceivableReceiptDetail> addList=new ArrayList<>();
for (OmsReceivableReceiptDetail existDetail : list) {
OmsReceivableReceiptDetail temp = new OmsReceivableReceiptDetail();
BeanUtil.copyProperties(existDetail,temp);
temp.setId(null);
temp.setReceivableDetailType(OmsReceivableReceiptDetail.ReceivableDetailTypeEnum.REFUND.getCode());
temp.setReceiptBillCode(targetReceiptBillCode);
temp.setCreateBy(ShiroUtils.getUserId().toString());
temp.setReceiptAmount(existDetail.getReceiptAmount().negate());
temp.setReceiptRate(existDetail.getReceiptRate().negate());
temp.setReceiptBillCode(targetReceiptBillCode);
temp.setRemark("退款");
addList.add( temp);
}
omsReceivableReceiptDetailMapper.insertBatch(addList);
}
}
}

View File

@ -32,6 +32,7 @@
<result property="bankNumber" column="bank_number"/>
<result property="refundStatus" column="refund_status"/>
<result property="remainingAmount" column="remaining_amount"/>
<result property="originalBillId" column="original_bill_id"/>
</resultMap>
<!-- 基本字段 -->

View File

@ -131,6 +131,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="paymentAmountTax != null">#{paymentAmountTax},</if>
</trim>
</insert>
<insert id="insertBatch">
insert into oms_receivable_receipt_detail
(receipt_plan_id,receivable_bill_id,receipt_time,receipt_amount,receipt_rate,receipt_bill_code,receivable_detail_type
,remark,create_time,create_by,update_time,update_by,payment_amount_without_tax,payment_amount_tax)
values
<foreach item="item" collection="list" separator="," index="">
(#{item.receiptPlanId},#{item.receivableBillId},#{item.receiptTime},#{item.receiptAmount},#{item.receiptRate},#{item.receiptBillCode},#{item.receivableDetailType}
,#{item.remark},#{item.createTime},#{item.createBy},#{item.updateTime},#{item.updateBy},#{item.paymentAmountWithoutTax},#{item.paymentAmountTax})
</foreach>
</insert>
<update id="updateOmsReceivableReceiptDetail" parameterType="com.ruoyi.sip.domain.OmsReceivableReceiptDetail">
update oms_receivable_receipt_detail