feat(finance): 新增收款单管理功能

- 新增收款单新增表单页面,支持预收和应收两种类型
- 实现收款单详情查看抽屉组件
- 添加收款单列表查询、筛选和分页功能
- 集成收款单新增、提交、退回和红冲操作
- 支持收款计划选择和金额计算逻辑
- 完善收款单据关联数据展示和处理
- 提供收款单附件管理和审批状态跟踪
- 增加代理商选择器和订单数据联动加载
- 实现收款单据类型和状态字典显示
- 添加收款单据验证规则和提交前检查
dev_1.0.1
chenhao 2025-12-19 13:44:22 +08:00
parent 1d85e92997
commit c257cdc5cd
22 changed files with 2278 additions and 71 deletions

View File

@ -0,0 +1,75 @@
import request from '@/utils/request'
import {tansParams} from "@/utils/ruoyi"
// 查询收款单列表
export function listReceive(query) {
return request({
url: '/finance/receipt/list',
method: 'get',
data: tansParams(query)
})
}
// 查询收款单详细
export function getReceive(id) {
return request({
url: '/finance/receipt/' + id,
method: 'get'
})
}
// 查询收款单附件
export function getReceiveAttachments(id, params) {
return request({
url: `/finance/receipt/attachment/${id}`,
method: 'get',
params
})
}
// 上传收款单附件
export function uploadReceiveAttachment(data) {
return request({
url: '/finance/receipt/uploadReceipt',
method: 'post',
headers: {
'Content-Type': 'multipart/form-data'
},
data: data,
needLoading: true
});
}
// 申请红冲
export function redRush(id) {
return request({
url: '/finance/receipt/applyRefund/' + id,
method: 'get'
})
}
// 退回收款单
export function returnReceive(id) {
return request({
url: '/finance/receipt/returnReceivable/' + id,
method: 'delete'
})
}
// 新增收款单 (Calls Receivable Merge Logic)
export function mergeReceivable(data) {
return request({
url: '/finance/receivable/mergeAndInitiateReceipt',
method: 'post',
data: data,
needLoading: true
})
}
export function addReceipt(data) {
return request({
url: '/finance/receipt/insert',
method: 'post',
data: data,
needLoading: true
})
}

View File

@ -0,0 +1,63 @@
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
/**
* 导出指定DOM元素为PDF
* @param {HTMLElement} element - 要导出的DOM元素
* @param {string} fileName - 导出的文件名
* @returns {Promise<void>}
*/
export async function exportElementToPDF(element, fileName) {
const disabledElements = [];
try {
// 移除所有输入框的 disabled 属性以便在PDF中正确显示
element.querySelectorAll('input:disabled, textarea:disabled').forEach(el => {
disabledElements.push(el);
el.disabled = false;
});
// 使用html2canvas捕获内容
const canvas = await html2canvas(element, {
scale: 2, // 提高清晰度
useCORS: true, // 允许跨域图片
logging: false, // 关闭日志
backgroundColor: '#F8F5F0' // 设置背景色
});
// 计算PDF页面尺寸
const imgWidth = 210; // A4纸宽度mm
const pageHeight = 297; // A4纸高度mm
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
// 创建PDF
const pdf = new jsPDF('p', 'mm', 'a4');
let position = 0;
// 将canvas转换为图片
const imgData = canvas.toDataURL('image/jpeg');
// 添加第一页
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
// 如果内容超过一页,添加更多页
while (heightLeft > 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
// 保存PDF
pdf.save(fileName);
} catch (error) {
console.error('PDF导出失败:', error);
throw error;
} finally {
// 恢复之前移除的 disabled 属性
disabledElements.forEach(el => {
el.disabled = true;
});
}
}

View File

@ -0,0 +1,482 @@
<template>
<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-row>
<el-col :span="24">
<el-form-item label="代理商" prop="partnerName">
<el-input v-model="form.partnerName" placeholder="请选择代理商" readonly @click.native="showPartnerSelector = true">
<el-button slot="append" icon="el-icon-search" @click="showPartnerSelector = true"></el-button>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="是否预收" prop="receiveBillType">
<el-radio-group v-model="form.receiveBillType">
<el-radio label="PRE_RECEIPT"></el-radio>
<el-radio label="FROM_RECEIVABLE"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="预计收款时间" prop="receiptTime">
<el-date-picker
v-model="form.receiptTime"
type="date"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择日期"
></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row v-if="form.receiveBillType === 'PRE_RECEIPT'">
<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>
<div v-if="form.receiveBillType === 'FROM_RECEIVABLE' && form.partnerCode" class="table-container">
<h4>应收单列表</h4>
<el-table
ref="receivableTable"
:data="receivableList"
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="receivableBillCode" width="150"/>
<el-table-column label="预计收款时间" align="center" prop="planReceiptDate" 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="collectionStatus" width="120">
<template slot-scope="scope">
<dict-tag :options="dict.type.collection_status" :value="scope.row.collectionStatus"/>
</template>
</el-table-column> -->
<el-table-column label="含税总价" align="center" prop="totalPriceWithTax" width="120"/>
<el-table-column label="未收款金额" align="center" prop="unreceivedAmount" width="120"/>
<el-table-column label="本次收款金额">
<template slot-scope="scope">
{{ calculateOrderCurrentReceiptAmount(scope.row).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="本次收款比例" align="center" width="120">
<template slot-scope="scope">
{{ calculateOrderCurrentReceiptRate(scope.row) }}%
</template>
</el-table-column>
<el-table-column label="已收款金额" align="center" prop="receivedAmount" 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="handleOpenReceiptPlanSelector(scope.row, scope.$index)"
>选择
</el-button>
</template>
</el-table-column>
</el-table>
<div class="total-info">
<span style="margin-left: 20px;">计划收款总金额: <el-tag type="success">{{
totalPlannedAmount.toFixed(2)
}}</el-tag></span>
<span>计划收款比例: <el-tag type="info">{{ totalSelectedReceivableAmount ? this.$calc.mul(this.$calc.div(totalPlannedAmount,totalSelectedReceivableAmount,4),100) : 0 }}%</el-tag></span>
</div>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="loadTableData"
/>
</div>
<div v-if="form.receiveBillType === 'PRE_RECEIPT' && form.partnerCode" class="table-container">
<h4>订单列表</h4>
<el-table
ref="orderTable"
:data="orderList"
border
style="width: 100%"
@selection-change="handleSelectionChange"
@select="handleSelect"
@select-all="handleSelectAll"
max-height="400"
row-key="id"
>
<el-table-column type="selection" width="55" reserve-selection></el-table-column>
<el-table-column prop="projectCode" label="项目编号"></el-table-column>
<el-table-column prop="projectName" label="项目名称"></el-table-column>
<el-table-column prop="createTime" label="下单时间"></el-table-column>
<el-table-column label="订单状态" prop="orderStatus">
<template slot-scope="scope">
<dict-tag :options="dict.type.order_status" :value="scope.row.orderStatus"/>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="loadTableData"
/>
</div>
<div v-if="!form.partnerCode" style="text-align: center; color: #909399; padding: 20px;">
请先选择代理商
</div>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</div>
<!-- Receipt Plan Selector Dialog -->
<el-dialog :title="planTitle" :visible.sync="isReceiptPlanSelectorOpen" width="70%"
@close="isReceiptPlanSelectorOpen=false" append-to-body>
<receipt-plan
ref="planSelector"
:receivable-data="chooseReceivable"
:selected-plans="chooseReceivable.receiptPlans"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="isReceiptPlanSelectorOpen=false"> </el-button>
<el-button type="primary" @click="handleChooseConfirm"> </el-button>
</div>
</el-dialog>
<select-partner :visible.sync="showPartnerSelector" @partner-selected="handlePartnerSelected" />
</el-dialog>
</template>
<script>
import {listReceivable} from "@/api/finance/receivable";
import {listOrder} from "@/api/project/order";
import ReceiptPlan from "@/views/finance/receivable/components/ReceiptPlan.vue";
import SelectPartner from "@/views/system/partner/selectPartner.vue";
export default {
name: "AddForm",
components: {ReceiptPlan, SelectPartner},
props: {
visible: {
type: Boolean,
default: false
}
},
dicts: ['order_status', 'receive_status', 'collection_status'],
data() {
return {
internalVisible: this.visible,
receivableList: [], // List for Standard/Receivable Bills
orderList: [], // List for Pre-receipt/Sales Orders
selectedRows: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10
},
form: {
receiveBillType: 'FROM_RECEIVABLE',
partnerName: null,
partnerCode: null,
remark: null,
totalPriceWithTax: 0,
actualReceiveTime: null
},
showPartnerSelector: false,
rules: {
partnerName: [{required: true, message: "代理商不能为空", trigger: "change"}],
actualReceiveTime: [{required: true, message: "实际收款时间不能为空", trigger: "change"}],
receiveBillType: [{required: true, message: "请选择是否预收", trigger: "change"}],
totalPriceWithTax: [{required: false, message: "预收金额不能为空", trigger: "blur"}]
},
// Plan Selector Data
planTitle: '',
isReceiptPlanSelectorOpen: false,
chooseReceivable: {},
currentReceivableOrderIndexForPlan: -1,
};
},
computed: {
totalPlannedAmount() {
if (this.form.receiveBillType === 'FROM_RECEIVABLE') {
// Calculate based on selected rows and their plans/defaults
return this.selectedRows.reduce((sum, row) => {
return sum + this.calculateOrderCurrentReceiptAmount(row);
}, 0);
}
return 0;
},
totalSelectedReceivableAmount() {
if (this.form.receiveBillType === 'FROM_RECEIVABLE') {
return this.selectedRows.reduce((sum, row) => {
return sum + (row.totalPriceWithTax || 0);
}, 0);
}
return 0;
},
},
watch: {
visible(newVal) {
this.internalVisible = newVal;
if (newVal) {
this.resetForm();
}
},
internalVisible(newVal) {
this.$emit("update:visible", newVal);
},
'form.receiveBillType': function (val) {
// Toggle validation for Pre-receipt Amount
if (val === 'PRE_RECEIPT') {
this.rules.totalPriceWithTax[0].required = true;
} else {
this.rules.totalPriceWithTax[0].required = false;
}
this.queryParams.pageNum = 1;
this.selectedRows = [];
this.loadTableData();
}
},
methods: {
handlePartnerSelected(partner) {
this.form.partnerName = partner.partnerName;
this.form.partnerCode = partner.partnerCode;
this.queryParams.pageNum = 1;
this.selectedRows = [];
this.loadTableData();
},
loadTableData() {
this.receivableList = [];
this.orderList = [];
const query = {
pageNum: this.queryParams.pageNum,
pageSize: this.queryParams.pageSize
};
if (this.form.receiveBillType === 'FROM_RECEIVABLE' && this.form.partnerCode) {
query.partnerCode = this.form.partnerCode;
listReceivable(query).then(res => {
this.receivableList = (res.rows || []).map(item => {
const receiptPlans = item.receiptPlans ? [...item.receiptPlans] : [];
// If needed, add logic to initialize default plan similar to payment
if (receiptPlans.length === 0 && item.lastReceiptPlanId) {
receiptPlans.push({
id: item.lastReceiptPlanId,
planAmount: item.planAmount,
planReceiptDate: item.planReceiptDate,
planRate: this.$calc.mul(this.$calc.div(item.planAmount, item.totalPriceWithTax, 4), 100)
});
}
return {
...item,
receiptPlans: receiptPlans,
}
});
this.total = res.total;
});
} else if (this.form.receiveBillType === 'PRE_RECEIPT' && this.form.partnerCode) {
// Filter Sales Orders for the agent (partner)
query.partnerCode = this.form.partnerCode;
query.orderStatus = '2'; // Example status
listOrder(query).then(res => {
this.orderList = res.rows || [];
this.total = res.total;
});
}
this.$nextTick(() => {
if (this.$refs.receivableTable) {
this.$refs.receivableTable.clearSelection()
}
if (this.$refs.orderTable) {
this.$refs.orderTable.clearSelection()
}
})
},
handleSelect(selection, row) {
if (this.form.receiveBillType === 'PRE_RECEIPT') {
this.$refs.orderTable.clearSelection();
const isSelected = selection.some(item => item.id === row.id); // Use ID for uniqueness
if (isSelected) {
this.$refs.orderTable.toggleRowSelection(row, true);
}
}
},
handleSelectAll(selection) {
if (this.form.receiveBillType === 'PRE_RECEIPT') {
this.$refs.orderTable.clearSelection();
this.$modal.msgWarning("预收单只能选择一个订单");
}
},
handleSelectionChange(selection) {
this.selectedRows = selection;
},
// --- Receipt Plan Logic ---
handleOpenReceiptPlanSelector(row, index) {
this.planTitle = `选择收款计划 - ${row.receivableBillCode}`;
this.chooseReceivable = row;
this.currentReceivableOrderIndexForPlan = index;
this.isReceiptPlanSelectorOpen = true;
},
handleChooseConfirm() {
if (!this.$refs.planSelector) {
this.$modal.msgError('无法获取计划选择器组件');
return;
}
const selectedPlans = this.$refs.planSelector.selectedPlan || [];
// Update the plans for the specific order
if (this.currentReceivableOrderIndexForPlan !== -1) {
const row = this.receivableList[this.currentReceivableOrderIndexForPlan];
this.$set(row, 'receiptPlans', [...selectedPlans]);
}
this.isReceiptPlanSelectorOpen = false;
this.$modal.msgSuccess(`已更新收款计划选择,共 ${selectedPlans.length}`);
},
calculateOrderCurrentReceiptAmount(order) {
if (order && order.receiptPlans && order.receiptPlans.length > 0) {
return order.receiptPlans.reduce((sum, plan) => sum + (plan.planAmount || 0), 0);
}
return 0;
},
calculateOrderCurrentReceiptRate(order) {
if (order && order.receiptPlans && order.receiptPlans.length > 0 && order.totalPriceWithTax) {
const currentAmount = this.calculateOrderCurrentReceiptAmount(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) {
if (this.form.receiveBillType === 'FROM_RECEIVABLE') {
if (this.selectedRows.length === 0) {
this.$message.warning("请选择至少一条应收单");
return;
}
// Process selected rows
const processedReceivableOrders = this.selectedRows.map(order => {
let finalPlans = order.receiptPlans;
// Add validation if needed: check if plans exist
if (!finalPlans || finalPlans.length === 0) {
// warning handled in previous logic, maybe add here
}
return {
id: order.id,
receivableBillCode: order.receivableBillCode,
taxRate: order.taxRate,
// Map plans
receiptPlans: (finalPlans || []).map(plan => ({
id: plan.id,
planReceiptDate: plan.planReceiptDate,
planAmount: plan.planAmount,
planRate: plan.planRate,
remark: plan.remark
}))
};
});
// Ensure every selected order has plans?
if (processedReceivableOrders.some(o => o.receiptPlans.length === 0)) {
this.$message.warning("选中的应收单必须包含收款计划");
return;
}
const submitData = {
receiptBillType: 'FROM_RECEIVABLE',
receiptTime: this.form.receiptTime,
receivableBills: processedReceivableOrders,
totalMergeReceiptAmount: this.totalPlannedAmount
};
this.$emit("submit", submitData);
} else {
// Pre-receipt logic
if (this.selectedRows.length !== 1) {
this.$modal.msgWarning("请选择一笔订单");
return;
}
if ((this.form.totalPriceWithTax || 0) <= 0) {
this.$message.warning("预收金额需要大于0");
return;
}
let order = this.selectedRows[0] ?? {};
const submitData = {
receiptBillType: 'PRE_RECEIPT',
receiptTime: this.form.receiptTime,
orderCode: order.orderCode, // Ensure orderCode is available in orderList items
partnerCode: this.form.partnerCode,
partnerName: this.form.partnerName,
projectCode: order.projectCode,
projectName: order.projectName,
totalPriceWithTax: this.form.totalPriceWithTax
};
console.log(submitData)
this.$emit("submit", submitData);
}
}
});
},
resetForm() {
if (this.$refs.form) {
this.$refs.form.resetFields();
}
this.form = {
receiveBillType: 'FROM_RECEIVABLE',
partnerName: null,
partnerCode: null,
remark: null,
totalPriceWithTax: 0,
actualReceiveTime: null
};
this.receivableList = [];
this.orderList = [];
this.selectedRows = [];
this.queryParams = {
pageNum: 1,
pageSize: 10
};
this.total = 0;
}
}
};
</script>
<style scoped>
.table-container {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,163 @@
<template>
<el-drawer
title="收款单详情"
:visible.sync="visible"
direction="rtl"
size="70%"
@close="handleClose"
>
<div class="dialog-body" v-if="detail">
<div class="section">
<el-divider content-position="left">销售-收款单</el-divider>
<div class="details-container">
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item">销售-收款单编号: {{ detail.receiptBillCode }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">收款时间: {{ detail.receiptTime }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">进货商名称: {{ detail.partnerName }}</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item">含税总价(): {{ detail.totalPriceWithTax }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">未税总价(): {{ detail.totalPriceWithoutTax }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">税额(): {{ detail.taxAmount }}</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item">收款单类型:
<dict-tag :options="dict.type.receipt_bill_type" :value="detail.receiptBillType"/></div>
</el-col>
<el-col :span="8">
<div class="detail-item">对方支付方式: {{ detail.receiptMethod|| '-' }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">收款图/回执单:
<dict-tag :options="dict.type.approve_status" :value="detail.approveStatus"/>
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item">收款金额:{{detail.totalPriceWithTax}}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">备注: {{ detail.remark|| '-' }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">上传人姓名:
<dict-tag :options="dict.type.approve_status" :value="detail.approveStatus"/>
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item">账户名称: {{ detail.receiptAccountName }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">银行账号: {{ detail.receiptBankNumber|| '-' }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">银行开户行:{{detail.receiptBankOpenAddress}}
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item">银行行号: {{ detail.bankNumber }}</div>
</el-col>
<el-col :span="8">
<div class="detail-item">收款状态: <dict-tag :options="dict.type.receipt_bill_status" :value="detail.receiptStatus"/></div>
</el-col>
<el-col :span="8">
<div class="detail-item">审批节点: {{ detail.approveNode|| '-' }}</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div class="detail-item">审批状态:
<dict-tag :options="dict.type.approve_status" :value="detail.approveStatus"/>
</div>
</el-col>
<el-col :span="8">
<div class="detail-item">审批通过时间: {{ detail.approveTime || '-'}}</div>
</el-col>
</el-row>
</div>
</div>
<div class="section">
<el-divider content-position="left">销售-应收单</el-divider>
<el-table :data="detail.detailDTOList">
<el-table-column type="index" label="序号" width="50"></el-table-column>
<el-table-column property="projectCode" label="项目编号"></el-table-column>
<el-table-column property="projectName" label="项目名称"></el-table-column>
<el-table-column property="receivableBillCode" label="应收单编号"></el-table-column>
<el-table-column property="totalPriceWithTax" label="含税总价(元)"></el-table-column>
<el-table-column property="receiptAmount" label="本次收款金额"></el-table-column>
<el-table-column property="receiptRate" label="本次收款比例(%)"></el-table-column>
</el-table>
</div>
</div>
</el-drawer>
</template>
<script>
export default {
name: "DetailDrawer",
props: {
visible: {
type: Boolean,
default: false,
},
detail: {
type: Object,
default: () => null,
},
},
dicts:['receive_bill_type','approve_status','receive_status'],
methods: {
handleClose() {
this.$emit("update:visible", false);
},
},
};
</script>
<style scoped>
.dialog-body {
max-height: calc(100vh - 50px);
overflow-y: auto;
padding: 0 20px 20px 20px;
}
.details-container {
border: 1px solid #EBEEF5;
padding: 20px;
border-radius: 4px;
}
.detail-item {
display: flex;
border: 1px solid #EBEEF5;
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
font-size: 14px;
align-items: center;
}
.section {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,486 @@
<template>
<el-dialog :title="titleText" :visible.sync="dialogVisible" width="900px" @close="handleClose">
<div v-if="loading" class="loading-spinner">
<i class="el-icon-loading"></i>
</div>
<div v-else class="receipt-dialog-body">
<div v-if="canUpload" class="upload-btn-container">
<el-button type="primary" icon="el-icon-upload" @click="openUploadDialog">{{ titleText }}</el-button>
</div>
<el-timeline v-if="attachments.length > 0">
<el-timeline-item
v-for="attachment in attachments"
:key="attachment.id"
:timestamp="parseTime(attachment.createTime, '{y}-{m}-{d} {h}:{i}:{s}')"
placement="top"
>
<el-card>
<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>
</div>
<div class="detail-item">
<span class="item-label">{{ titleText }}</span>
<div class="item-value">
<div class="image-wrapper">
<el-image
v-if="!isPdf(attachment.filePath)"
:src="getImageUrl(attachment.filePath)"
:preview-src-list="previewList"
style="width: 200px; height: 150px;"
fit="contain"
></el-image>
<div v-else-if="pdfUrls[attachment.filePath]" class="pdf-thumbnail-container" @click="openPdfPreview(pdfUrls[attachment.filePath])">
<iframe :src="pdfUrls[attachment.filePath]" width="100%" height="150px" frameborder="0"></iframe>
<div class="pdf-hover-overlay">
<i class="el-icon-zoom-in"></i>
</div>
</div>
<div v-if="attachment.delFlag === '2'" class="void-overlay"></div>
</div>
<el-button
size="mini"
type="primary"
class="download-btn"
icon="el-icon-download"
@click="downloadFile(attachment)"
>下载{{ titleText }}</el-button>
</div>
</div>
<div class="detail-item">
<span class="item-label">收款总额</span>
<span class="item-value">{{ receiptData.totalAmount }}</span>
</div>
<div class="detail-item">
<span class="item-label">备注</span>
<span class="item-value">{{ attachment.remark }}</span>
</div>
</div>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
<el-empty v-else :description="'暂无' + titleText"></el-empty>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
<!-- PDF Preview Dialog -->
<el-dialog
:visible.sync="pdfPreviewVisible"
width="80%"
top="5vh"
append-to-body
custom-class="pdf-preview-dialog"
>
<iframe :src="currentPdfUrl" width="100%" height="600px" frameborder="0"></iframe>
</el-dialog>
<!-- Upload Dialog -->
<el-dialog
:title="'上传' + titleText"
:visible.sync="uploadDialogVisible"
width="70vw"
append-to-body
@close="closeUploadDialog"
custom-class="upload-receipt-dialog"
>
<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>
<el-form-item label="收款附件" prop="file" required>
<div style="display: flex; flex-direction: column; align-items: flex-start;">
<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="small" type="primary" icon="el-icon-upload2">{{ uploadForm.file ? '重新上传' : '点击上传' }}</el-button>
</el-upload>
<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>
<el-form-item label="备注">
<el-input
type="textarea"
v-model="uploadForm.remark"
:rows="4"
placeholder="此处备注描述..."
></el-input>
</el-form-item>
</el-form>
</el-col>
<el-col :span="12">
<div class="upload-preview-container" style="height: 70vh;">
<div v-if="previewUrl" class="preview-content">
<img v-if="!isPreviewPdf" :src="previewUrl" class="preview-image" />
<iframe v-else :src="previewUrl" width="100%" height="100%" frameborder="0"></iframe>
</div>
<div v-else class="preview-placeholder">
<div class="placeholder-icon">
<i class="el-icon-picture"></i>
</div>
<div class="placeholder-text">点击图片进入预览</div>
</div>
</div>
</el-col>
</el-row>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitNewUpload"></el-button>
<el-button @click="closeUploadDialog"></el-button>
</span>
</el-dialog>
</el-dialog>
</template>
<script>
import { getReceiveAttachments, uploadReceiveAttachment } from "@/api/finance/receive";
import request from '@/utils/request';
export default {
name: "ReceiveDialog",
props: {
visible: {
type: Boolean,
default: false,
},
receiptData: {
type: Object,
default: () => null,
},
dicts: {
type: Object,
default: () => ({})
}
},
dicts:['finance_invoice_type'],
data() {
return {
loading: false,
attachments: [],
// Upload Dialog Data
uploadDialogVisible: false,
uploadForm: {
ticketType: '',
remark: '',
file: null
},
rules: {
ticketType: [
{ required: true, message: "请选择票据类型", trigger: "change" }
],
},
previewUrl: '',
isPreviewPdf: false,
// PDF Preview Data
pdfUrls: {},
pdfPreviewVisible: false,
currentPdfUrl: '',
};
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit("update:visible", val);
},
},
previewList() {
return this.attachments
.filter(att => !this.isPdf(att.filePath))
.map(att => this.getImageUrl(att.filePath));
},
canUpload() {
if (!this.attachments || this.attachments.length === 0) {
return true;
}
return this.attachments.every(att => att.delFlag === '2');
},
titleText() {
return '收款附件';
}
},
watch: {
visible(val) {
if (val && this.receiptData) {
this.fetchAttachments();
}
},
},
methods: {
fetchAttachments() {
if (!this.receiptData.id) return;
this.loading = true;
getReceiveAttachments(this.receiptData.id, { type: 'ticket' })
.then(response => {
const data = response.data || [];
data.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
this.attachments = data;
this.loadPdfPreviews();
this.loading = false;
})
.catch(() => {
this.attachments = [];
this.loading = false;
});
},
loadPdfPreviews() {
this.attachments.forEach(att => {
if (this.isPdf(att.filePath) && !this.pdfUrls[att.filePath]) {
request({
url: '/common/download/resource',
method: 'get',
params: { resource: att.filePath },
responseType: 'blob'
}).then(res => {
const blob = new Blob([res.data], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
this.$set(this.pdfUrls, att.filePath, url);
}).catch(console.error);
}
});
},
openPdfPreview(url) {
if (!url) return;
this.currentPdfUrl = url;
this.pdfPreviewVisible = true;
},
getImageUrl(resource) {
return process.env.VUE_APP_BASE_API + "/common/download/resource?resource=" + resource;
},
isPdf(filePath) {
return filePath && filePath.toLowerCase().endsWith('.pdf');
},
downloadFile(attachment) {
const link = document.createElement('a');
link.href = this.getImageUrl(attachment.filePath);
link.download = attachment.fileName || 'receive_attachment';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
handleClose() {
this.attachments = [];
// Clean up object URLs
Object.values(this.pdfUrls).forEach(url => URL.revokeObjectURL(url));
this.pdfUrls = {};
},
// New Upload Dialog Methods
openUploadDialog() {
this.uploadForm = {
remark: '',
file: null
};
if (this.$refs.uploadForm) {
this.$refs.uploadForm.clearValidate();
}
this.previewUrl = '';
this.isPreviewPdf = false;
this.uploadDialogVisible = true;
},
closeUploadDialog() {
this.uploadDialogVisible = false;
this.uploadForm.file = null;
this.previewUrl = '';
},
handleFileChange(file) {
const isLt2M = file.size / 1024 / 1024 < 2;
const isAcceptedType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.raw.type);
if (!isAcceptedType) {
this.$message.error('上传文件只能是 JPG/PNG/PDF 格式!');
return;
}
if (!isLt2M) {
this.$message.error('上传文件大小不能超过 2MB!');
return;
}
this.uploadForm.file = file.raw;
this.isPreviewPdf = file.raw.type === 'application/pdf';
this.previewUrl = URL.createObjectURL(file.raw);
},
handleFileRemove() {
this.uploadForm.file = null;
this.previewUrl = '';
},
submitNewUpload() {
this.$refs.uploadForm.validate(valid => {
if (valid) {
if (!this.uploadForm.file) {
this.$message.warning("请选择要上传的文件");
return;
}
const formData = new FormData();
formData.append("file", this.uploadForm.file);
formData.append("id", this.receiptData.id);
formData.append("remark", this.uploadForm.remark);
formData.append("ticketType", this.uploadForm.ticketType);
uploadReceiveAttachment(formData)
.then(response => {
this.$message.success("上传成功");
this.closeUploadDialog();
this.fetchAttachments();
})
.catch(error => {
this.$message.error("上传失败");
});
}
});
},
},
};
</script>
<style scoped>
.receipt-dialog-body {
max-height: 60vh;
overflow-y: auto;
}
.loading-spinner {
text-align: center;
font-size: 24px;
padding: 20px;
}
.receipt-card-content {
display: flex;
}
.receipt-details {
flex-grow: 1;
}
.detail-item {
display: flex;
margin-bottom: 12px;
font-size: 14px;
}
.item-label {
width: 110px;
color: #606266;
text-align: right;
margin-right: 15px;
flex-shrink: 0;
}
.item-value {
color: #303133;
}
.image-wrapper {
position: relative;
width: 200px;
min-height: 150px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #DCDFE6;
border-radius: 4px;
margin-bottom: 10px;
}
.void-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-30deg);
color: red;
font-size: 48px;
font-weight: bold;
opacity: 0.7;
pointer-events: none;
}
.download-btn {
display: block;
}
.upload-btn-container {
margin-bottom: 20px;
}
/* New Dialog Styles */
.upload-preview-container {
width: 100%;
height: 300px;
background-color: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.preview-content {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.preview-placeholder {
text-align: center;
color: #909399;
}
.placeholder-icon {
font-size: 64px;
margin-bottom: 10px;
color: #c0c4cc;
}
.placeholder-text {
font-size: 14px;
}
.pdf-thumbnail-container {
position: relative;
width: 100%;
height: 150px;
cursor: pointer;
}
.pdf-hover-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s;
color: #fff;
font-size: 24px;
pointer-events: auto;
}
.pdf-thumbnail-container:hover .pdf-hover-overlay {
opacity: 1;
}
</style>

View File

@ -0,0 +1,346 @@
<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="receiveBillCode">
<el-input
v-model="queryParams.receiveBillCode"
placeholder="请输入收款单编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="客户名称" prop="customerName">
<el-input
v-model="queryParams.customerName"
placeholder="请输入客户名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="应收单编号" prop="receivableBillCode">
<el-input
v-model="queryParams.receivableBillCode"
placeholder="请输入应收单编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="收款单类型" prop="receiveBillType">
<el-select v-model="queryParams.receiveBillType" placeholder="请选择收款单类型" clearable>
<el-option v-for="dict in dict.type.receive_bill_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="审批状态" prop="approveStatus">
<el-select v-model="queryParams.approveStatus" placeholder="请选择审批状态" clearable>
<el-option label="待提交" value="0" />
<el-option label="审批中" value="1" />
<el-option label="已审批" value="2" />
<el-option label="已驳回" value="3" />
</el-select>
</el-form-item>
<el-form-item label="收款状态" prop="receiveStatus">
<el-select v-model="queryParams.receiveStatus" placeholder="请选择收款状态" clearable>
<el-option label="待收款" value="0" />
<el-option label="已收款" value="1" />
</el-select>
</el-form-item>
<el-form-item label="审批节点" prop="approveNode">
<el-input
v-model="queryParams.approveNode"
placeholder="请输入审批节点"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="审批通过时间">
<el-date-picker
v-model="dateRangeApproval"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item label="预计收款时间">
<el-date-picker
v-model="dateRangeEstimated"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item label="实际收款时间">
<el-date-picker
v-model="dateRangeActual"
style="width: 240px"
value-format="yyyy-MM-dd"
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"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="receiveList">
<el-table-column label="收款单编号" align="center" prop="receiptBillCode" />
<el-table-column label="预计收款时间" align="center" prop="receiptTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.receiptTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="进货商名称" align="center" prop="partnerName" />
<el-table-column label="含税总价(元)" align="center" prop="totalPriceWithTax" />
<el-table-column label="收款单类型" align="center" prop="receiptBillType" >
<template slot-scope="scope">
<dict-tag :options="dict.type.receipt_bill_type" :value="scope.row.receiptBillType"/>
</template>
</el-table-column>
<el-table-column label="预收单剩余额度" align="center" prop="remainingAmount" />
<el-table-column label="收款状态" align="center" prop="receiveStatus" >
<template slot-scope="scope">
<dict-tag :options="dict.type.receipt_bill_status" :value="scope.row.receiptStatus"/>
</template>
</el-table-column>
<el-table-column label="审批状态" align="center" prop="approveStatus" >
<template slot-scope="scope">
<dict-tag :options="dict.type.approve_status" :value="scope.row.approveStatus"/>
</template>
</el-table-column>
<el-table-column label="审批通过时间" align="center" prop="approveTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.approveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="审批节点" align="center" prop="approveNode" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleDetail(scope.row)"
>查看详情</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-document"
@click="handleReceipt(scope.row)"
>附件</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-document"
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>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 详情抽屉 -->
<detail-drawer :visible.sync="detailOpen" :detail="detailData"></detail-drawer>
<!-- 新增弹窗 -->
<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>
</div>
</template>
<script>
import {listReceive, getReceive, redRush, mergeReceivable, returnReceive, addReceipt} from "@/api/finance/receive";
import { addDateRange } from "@/utils/ruoyi";
import DetailDrawer from "./components/DetailDrawer.vue";
import AddForm from "./components/AddForm.vue";
import ReceiveDialog from "./components/ReceiveDialog.vue";
export default {
name: "Receive",
components: {
DetailDrawer,
AddForm,
ReceiveDialog
},
dicts:['receipt_bill_type','approve_status','receipt_bill_status'],
data() {
return {
//
loading: true,
//
showSearch: true,
//
total: 0,
//
receiveList: [],
//
queryParams: {
pageNum: 1,
pageSize: 10,
projectCode: null,
projectName: null,
receiveBillCode: null,
customerName: null,
receivableBillCode: null,
receiveBillType: null,
approveStatus: null,
receiveStatus: null,
approveNode: null,
},
//
dateRangeApproval: [],
dateRangeEstimated: [],
dateRangeActual: [],
//
detailOpen: false,
detailData: null,
//
addOpen: false,
//
receiptOpen: false,
currentRow: {}
};
},
created() {
this.getList();
},
methods: {
addDateRange,
getList() {
this.loading = true;
let query = { ...this.queryParams };
query = this.addDateRange(query, this.dateRangeApproval, 'ApproveTime');
query = this.addDateRange(query, this.dateRangeEstimated, 'PlanReceiveTime');
query = this.addDateRange(query, this.dateRangeActual, 'ActualReceiveTime');
listReceive(query).then(response => {
this.receiveList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRangeApproval = [];
this.dateRangeEstimated = [];
this.dateRangeActual = [];
this.resetForm("queryForm");
this.handleQuery();
},
/** 新增按钮操作 */
handleAdd() {
this.addOpen = true;
},
/** 新增提交 */
handleAddSubmit(form) {
if (form.paymentBillType==='FROM_RECEIVABLE'){
mergeReceivable(form).then(response => {
this.$modal.msgSuccess("新增成功");
this.addOpen = false;
this.getList();
}).catch(error => {
console.error("发起收款单失败", error);
this.$modal.msgError("新增失败");
});
}else{
addReceipt(form).then(response => {
this.$modal.msgSuccess("新增成功");
this.addOpen = false;
this.getList();
}).catch(error => {
console.error("新增收款单失败", error);
this.$modal.msgError("新增失败");
});
}
},
/** 详情按钮操作 */
handleDetail(row) {
getReceive(row.id).then(response => {
this.detailData = response.data;
this.detailData.approveNode = row.approveNode;
this.detailOpen = true;
});
},
/** 收款附件按钮操作 */
handleReceipt(row) {
this.currentRow = row;
this.receiptOpen = true;
},
/** 退回按钮操作 */
handleRedRush(row) {
this.$modal.confirm('是否确认收款单编号为"' + row.receiveBillCode + '"的数据项进行红冲,并提交财务审批?').then(function() {
return redRush(row.id);
}).then(() => {
this.getList();
this.$modal.msgSuccess("申请成功");
}).catch(() => {});
},
handleReturn(row) {
this.$modal.confirm('是否确认退回收款单编号为"' + row.receiptBillCode + '"的数据项?').then(function() {
return returnReceive(row.id);
}).then(() => {
this.getList();
this.$modal.msgSuccess("退回成功");
}).catch(() => {});
}
}
};
</script>

View File

@ -0,0 +1,90 @@
<template>
<el-drawer
title="核销详情"
:visible.sync="visible"
direction="rtl"
size="60%"
@close="handleClose"
>
<div class="drawer-body" v-if="detail">
<div class="section">
<el-divider content-position="left">收票单信息</el-divider>
<el-table :data="[detail]" border stripe>
<el-table-column type="index" label="序号" width="50" align="center"></el-table-column>
<el-table-column prop="writeOffAmount" label="本次核销含税总价(元)" align="center"></el-table-column>
<el-table-column prop="vendorName" label="制造商名称" align="center" show-overflow-tooltip></el-table-column>
<el-table-column prop="ticketBillCode" label="采购收票单编号" align="center" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="收票单生成时间" align="center" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.ticketBill.createTime) }}</span>
</template>
</el-table-column>
<!-- <el-table-column prop="receiptBillType" label="收票单类型" align="center">-->
<!-- <template slot-scope="scope">-->
<!-- <dict-tag :options="dict.type.ticket_bill_type" :value="scope.row.ticketBill.ticketBillType"/>-->
<!-- </template>-->
<!-- </el-table-column>-->
</el-table>
</div>
<div class="section">
<el-divider content-position="left">应付单明细</el-divider>
<el-table :data="detail.detailList" border stripe>
<el-table-column type="index" label="序号" width="50" align="center"></el-table-column>
<el-table-column prop="paymentAmount" label="本次核销金额(元)" align="center"></el-table-column>
<el-table-column prop="vendorName" label="制造商名称" align="center"></el-table-column>
<el-table-column prop="payableBillCode" label="采购应付单编号" align="center" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="应付单生成时间" align="center"></el-table-column>
</el-table>
</div>
</div>
</el-drawer>
</template>
<script>
export default {
name: "WriteOffDetailDrawer",
props: {
visible: {
type: Boolean,
default: false,
},
detail: {
type: Object,
default: () => ({}),
},
},
dicts: ['finance_write_off_type'],
methods: {
handleClose() {
this.$emit("update:visible", false);
},
},
};
</script>
<style scoped>
.drawer-body {
padding: 20px;
height: calc(100vh - 80px);
overflow-y: auto;
}
.details-container {
border: 1px solid #EBEEF5;
padding: 15px;
border-radius: 4px;
background-color: #f8f8f9;
}
.detail-item {
margin-bottom: 12px;
font-size: 14px;
color: #606266;
line-height: 1.5;
}
.section {
margin-bottom: 25px;
}
</style>

View File

@ -0,0 +1,234 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="120px">
<el-form-item label="核销序号" prop="writeOffCode">
<el-input
v-model="queryParams.writeOffCode"
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="receiptBillCode">
<el-input
v-model="queryParams.receiptBillCode"
placeholder="请输入采购收票单编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="制造商名称" prop="vendorName">
<el-select v-model="queryParams.vendorName" placeholder="请选择制造商名称" clearable filterable>
<el-option
v-for="item in vendorOptions"
:key="item.vendorName"
:label="item.vendorName"
:value="item.vendorName"
/>
</el-select>
</el-form-item>
<el-form-item label="核销类型" prop="writeOffType">
<el-select v-model="queryParams.writeOffType" placeholder="请选择核销类型" clearable>
<el-option
v-for="dict in dict.type.finance_write_off_type"
: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"
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="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>反核销</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="writeOffList" @selection-change="handleSelectionChange" row-key="id">
<el-table-column type="selection" width="55" align="center" :reserve-selection="true"/>
<el-table-column label="核销序号" align="center" prop="writeOffCode" />
<el-table-column label="核销人" align="center" prop="createByName" />
<el-table-column label="核销时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="采购收票单编号" align="center" prop="ticketBillCode" />
<el-table-column label="制造商名称" align="center" prop="vendorName" />
<el-table-column label="核销类型" align="center" prop="writeOffType">
<template slot-scope="scope">
<dict-tag :options="dict.type.finance_write_off_type" :value="scope.row.writeOffType"/>
</template>
</el-table-column>
<el-table-column label="核销含税总价(元)" align="center" prop="writeOffAmount" />
<el-table-column label="核销未税总价(元)" align="center" prop="writeOffAmountWithoutTax" />
<el-table-column label="核销税额(元)" align="center" prop="writeOffTaxAmount" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleDetail(scope.row)"
>查看详情</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>反核销</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"
/>
<!-- 详情抽屉 -->
<write-off-detail-drawer :visible.sync="open" :detail="form" />
</div>
</template>
<script>
import { listTicketWriteOff, getTicketWriteoff, delTicketWriteoff } from "@/api/finance/writeoff";
import { listVendor } from "@/api/base/vendor";
import WriteOffDetailDrawer from "./WriteOffDetailDrawer";
export default {
name: "WriteOffTicketRecord",
components: {
WriteOffDetailDrawer
},
dicts: ['finance_write_off_type'],
data() {
return {
//
loading: true,
//
ids: [],
//
multiple: true,
//
showSearch: true,
//
total: 0,
//
writeOffList: [],
//
vendorOptions: [],
//
title: "",
//
open: false,
//
dateRange: [],
//
queryParams: {
pageNum: 1,
pageSize: 10,
writeOffCode: null,
payableBillCode: null,
receiptBillCode: null,
vendorName: null,
writeOffType: null,
},
//
form: {}
};
},
created() {
this.getList();
this.getVendorList();
},
methods: {
/** 查询列表 */
getList() {
this.loading = true;
listTicketWriteOff(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.writeOffList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 查询制造商列表 */
getVendorList() {
listVendor().then(response => {
this.vendorOptions = response.rows;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = [];
this.resetForm("queryForm");
this.handleQuery();
},
/** 查看详情 */
handleDetail(row) {
this.loading = true;
getTicketWriteoff(row.id).then(response => {
this.form = response.data;
this.open = true;
this.title = "核销详情";
this.loading = false;
});
},
//
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.multiple = !selection.length
},
/** 反核销按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal.confirm('是否确认反核销核销序号为"' + ids + '"的数据项?').then(function() {
return delTicketWriteoff(ids);
}).then(() => {
this.getList();
this.$modal.msgSuccess("反核销成功");
}).catch(() => {});
}
}
};
</script>

View File

@ -1,10 +1,13 @@
package com.ruoyi.sip.controller;
import com.ruoyi.common.annotation.Log;
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.OmsReceiptBill;
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;
@ -18,7 +21,7 @@ import java.util.List;
*/
@RestController
@RequestMapping("omsReceiptBill")
@RequestMapping("/finance/receipt")
public class OmsReceiptBillController extends BaseController {
@Autowired
@ -67,4 +70,25 @@ public class OmsReceiptBillController extends BaseController {
public AjaxResult batchRemove(@PathVariable("ids") Long[] ids) {
return AjaxResult.success(omsReceiptBillService.batchRemove(ids));
}
@RequiresPermissions("finance:receipt:return")
@Log(title = "退回付款单", businessType = BusinessType.UPDATE)
@DeleteMapping("/returnReceivable/{id}")
@ResponseBody
public AjaxResult returnReceivable(@PathVariable("id") Long id)
{
try {
// 验证付款单ID
if (id == null) {
return AjaxResult.error("付款单ID不能为空");
}
// 调用服务层方法处理退回逻辑
return omsReceiptBillService.returnPaymentBill(id);
} catch (Exception e) {
logger.error("退回付款单失败", e);
return AjaxResult.error("操作失败:" + e.getMessage());
}
}
}

View File

@ -2,8 +2,10 @@ package com.ruoyi.sip.domain;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.ruoyi.sip.domain.dto.ReceiptDetailDTO;
import lombok.Data;
import lombok.Getter;
@ -52,11 +54,17 @@ public class OmsReceiptBill {
*/
private String orderCode;
/**
*
*/
private BigDecimal remainingAmount;
/**
*
*/
private BigDecimal totalPriceWithTax;
/**
*
*/
@ -152,6 +160,7 @@ public class OmsReceiptBill {
* 退 0:退 1:退
*/
private String refundStatus;
private List<ReceiptDetailDTO> detailDTOList;
@Getter
public enum ReceiptBillTypeEnum {

View File

@ -53,7 +53,7 @@ public class OmsReceivableBill extends BaseEntity
@Excel(name = "产品类型")
private String productType;
private String productCode;
private BigDecimal remainingAmount;
/** 含税总价 */
@Excel(name = "含税总价")
private BigDecimal totalPriceWithTax;

View File

@ -34,6 +34,7 @@ public class OmsReceivableReceiptDetail extends BaseEntity
/** 应收单ID */
@Excel(name = "应收单ID")
private Long receivableBillId;
private List<Long> receivableBillIdList;
/** 实际收款时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ -64,6 +65,8 @@ public class OmsReceivableReceiptDetail extends BaseEntity
private OmsFinAttachment finAttachment;
private Date actualReceiptTime;
private String receiptStatus;
private BigDecimal paymentAmountWithoutTax;
private BigDecimal paymentAmountTax;
@Getter
public enum ReceivableDetailTypeEnum {

View File

@ -0,0 +1,38 @@
package com.ruoyi.sip.domain.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* DTO
*
* @author ruoyi
* @date 2024-12-08
*/
@Data
public class ReceiptDetailDTO {
/** 项目编号 */
private String projectCode;
/** 项目名称 */
private String projectName;
private String productType;
/** 采购应付单编号 */
private String receivableBillCode;
/** 应付单含税总价 */
private BigDecimal totalPriceWithTax;
/** 本次付款金额 */
private BigDecimal receiptAmount;
/** 本次付款比例 */
private BigDecimal receiptRate;
private String partnerName;
private String partnerCode;
private Date createTime;
}

View File

@ -48,4 +48,7 @@ public interface OmsReceiptBillMapper {
int selectMaxCodeByPrefix(String codePrefix);
List<OmsReceiptBill> listRemainingAmountByPartnerCodeList(List<String> collect);
void clearRelationReceivable(String receiptBillCode);
}

View File

@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
import java.util.List;
import com.ruoyi.sip.domain.OmsReceivableReceiptDetail;
import com.ruoyi.sip.domain.dto.ReceiptDetailDTO;
/**
* Mapper
@ -62,4 +63,6 @@ public interface OmsReceivableReceiptDetailMapper
List<OmsReceivableReceiptDetail> list(OmsReceivableReceiptDetail omsReceivableReceiptDetail);
List<OmsReceivableReceiptDetail> selectByPaymentPlanIds(List<Long> allReceiptPlanIds);
List<ReceiptDetailDTO> listReceivableByReceiptBillCode(List<String> receiptBillCode);
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.sip.service;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.sip.domain.OmsReceiptBill;
import java.util.List;
@ -42,6 +43,9 @@ public interface IOmsReceiptBillService {
*/
int batchRemove(Long[] ids);
List<OmsReceiptBill> listRemainingAmountByPartnerCodeList(List<String> collect);
AjaxResult returnPaymentBill(Long id);
}

View File

@ -4,6 +4,7 @@ import java.util.List;
import com.ruoyi.sip.domain.OmsPayablePaymentDetail;
import com.ruoyi.sip.domain.OmsReceivableReceiptDetail;
import com.ruoyi.sip.domain.dto.ReceiptDetailDTO;
/**
* Service
@ -66,4 +67,8 @@ public interface IOmsReceivableReceiptDetailService
List<OmsReceivableReceiptDetail> selectReceiptPlanByIds(List<Long> allReceiptPlanIds);
List<OmsReceivableReceiptDetail> listByReceivableBillIdList(List<Long> idList);
List<ReceiptDetailDTO> listReceivableByReceiptBillCode(String receiptBillCode);
List<OmsReceivableReceiptDetail> listByReceiptBillCode(String receiptBillCode);
}

View File

@ -1,18 +1,28 @@
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 com.ruoyi.common.core.domain.AjaxResult;
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.sip.domain.dto.ReceiptDetailDTO;
import com.ruoyi.sip.mapper.OmsReceiptBillMapper;
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 javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author ch
@ -25,7 +35,10 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService {
@Resource
private OmsReceiptBillMapper omsReceiptBillMapper;
@Autowired
private IOmsReceivableReceiptDetailService omsReceivableReceiptDetailService;
@Autowired
private IOmsReceivableBillService receivableBillService;
/**
*
@ -41,7 +54,10 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService {
@Override
public OmsReceiptBill queryById(Long id) {
return omsReceiptBillMapper.queryById(id);
OmsReceiptBill receiptBill = omsReceiptBillMapper.queryById(id);
List<ReceiptDetailDTO> receiptDetailDTOS = omsReceivableReceiptDetailService.listReceivableByReceiptBillCode(receiptBill.getReceiptBillCode());
receiptBill.setDetailDTOList(receiptDetailDTOS);
return receiptBill;
}
@ -87,6 +103,47 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService {
return omsReceiptBillMapper.batchRemove(ids);
}
@Override
public List<OmsReceiptBill> listRemainingAmountByPartnerCodeList(List<String> collect) {
return omsReceiptBillMapper.listRemainingAmountByPartnerCodeList(collect);
}
@Override
public AjaxResult returnPaymentBill(Long id) {
try {
// 1. 验证付款单是否存在
OmsReceiptBill receiptBill = omsReceiptBillMapper.queryById(id);
if (receiptBill == null) {
return AjaxResult.error("付款单不存在");
}
// 2. 检查付款单类型只有FROM_PAYABLE类型的付款单才能退回
if (!OmsReceiptBill.ReceiptBillTypeEnum.FROM_RECEIVABLE.getCode().equals(receiptBill.getReceiptBillType())) {
return AjaxResult.error("只有由应收单合并生成的收款单才能执行退回操作");
}
List<OmsReceivableReceiptDetail> detailList = omsReceivableReceiptDetailService.listByReceiptBillCode(receiptBill.getReceiptBillCode());
// 3. 清楚关联
omsReceiptBillMapper.clearRelationReceivable(receiptBill.getReceiptBillCode());
// 6. 删除付款单记录
int result = omsReceiptBillMapper.deleteById(id);
if (result <= 0) {
throw new RuntimeException("删除付款单失败");
}
if (CollUtil.isNotEmpty(detailList)) {
receivableBillService.updateReceiptAmount(detailList.stream().map(OmsReceivableReceiptDetail::getReceivableBillId).distinct().collect(Collectors.toList()));
}
return AjaxResult.success("付款单退回成功!");
} catch (Exception e) {
throw new RuntimeException("退回付款单操作失败:" + e.getMessage(), e);
}
}
}

View File

@ -1,9 +1,10 @@
package com.ruoyi.sip.service.impl;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Comparator;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import cn.hutool.core.collection.CollUtil;
@ -11,10 +12,10 @@ import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.sip.domain.*;
import com.ruoyi.sip.domain.dto.MergedInvoiceDataDto;
import com.ruoyi.sip.domain.dto.MergedReceviableReceiptDataDto;
import com.ruoyi.sip.domain.dto.ReceivableOrderReceiptDto;
import com.ruoyi.sip.service.*;
@ -75,7 +76,17 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
@Override
public List<OmsReceivableBill> selectOmsReceivableBillList(OmsReceivableBill omsReceivableBill)
{
return omsReceivableBillMapper.selectOmsReceivableBillList(omsReceivableBill);
List<OmsReceivableBill> receivableBills = omsReceivableBillMapper.selectOmsReceivableBillList(omsReceivableBill);
if (CollUtil.isNotEmpty(receivableBills)) {
PageUtils.clearPage();
List<OmsReceiptBill> receiptBills = receiptBillService.listRemainingAmountByPartnerCodeList(receivableBills.stream().map(OmsReceivableBill::getPartnerCode).collect(Collectors.toList()));
Map<String, BigDecimal> decimalMap = receiptBills.stream().filter(item -> item.getRemainingAmount() != null)
.collect(Collectors.toMap(OmsReceiptBill::getPartnerCode, OmsReceiptBill::getRemainingAmount, BigDecimal::add));
for (OmsReceivableBill payableBill : receivableBills) {
payableBill.setReceivedAmount(decimalMap.getOrDefault(payableBill.getPartnerCode(), BigDecimal.ZERO));
}
}
return receivableBills;
}
@ -142,13 +153,13 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
// Fetch bills once
for (ReceivableOrderReceiptDto order : dto.getReceivableBills()) {
for (OmsReceivableReceiptPlan plan : order.getReceiptPlans()) {
// 计算每个 plan 的未税金额 = planAmount / (1 + 税率),保留两位小数
BigDecimal taxRate = order.getTaxRate();
if (taxRate == null || taxRate.compareTo(BigDecimal.ZERO) < 0) {
// 如果税率为空或小于0则默认为0.13
taxRate = new BigDecimal(defaultTax);
}
for (OmsReceivableReceiptPlan plan : order.getReceiptPlans()) {
// 计算每个 plan 的未税金额 = planAmount / (1 + 税率),保留两位小数
// 计算未税金额 = planAmount / (1 + 税率)
BigDecimal divisor = BigDecimal.ONE.add(taxRate);
BigDecimal planWithoutTax = plan.getPlanAmount().divide(divisor, 2, java.math.RoundingMode.HALF_UP);
@ -192,6 +203,11 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
// 遍历所有计划并检查是否有已开票的记录
for (ReceivableOrderReceiptDto receivableOrderDto : dto.getReceivableBills()) {
BigDecimal taxRate = receivableOrderDto.getTaxRate();
if (taxRate == null || taxRate.compareTo(BigDecimal.ZERO) < 0) {
// 如果税率为空或小于0则默认为0.13
taxRate = new BigDecimal(defaultTax);
}
for (OmsReceivableReceiptPlan plan : receivableOrderDto.getReceiptPlans()) {
// 检查是否存在已开票的记录
OmsReceivableReceiptDetail existingDetail = existingDetailsMap.get(plan.getId());
@ -210,6 +226,8 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
detail.setReceiptAmount(plan.getPlanAmount());
detail.setReceiptRate(plan.getPlanRate());
detail.setReceiptTime(plan.getPlanReceiptDate());
detail.setPaymentAmountWithoutTax(plan.getPlanAmount().divide(BigDecimal.ONE.add(taxRate), 2, java.math.RoundingMode.HALF_UP));
detail.setPaymentAmountTax(detail.getReceiptAmount().subtract(detail.getPaymentAmountWithoutTax()));
detail.setRemark(plan.getRemark());
detail.setCreateBy(ShiroUtils.getUserId().toString());
detail.setReceivableDetailType(OmsReceivableReceiptDetail.ReceivableDetailTypeEnum.NORMAL_RECEIPT.getCode());
@ -287,23 +305,40 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
}
List<OmsReceivableReceiptDetail> omsReceivableReceiptDetailList = detailService.listByReceivableBillIdList(idList);
Map<Long, Map<String, BigDecimal>> planMap = omsReceivableReceiptDetailList.stream().collect(Collectors.groupingBy(OmsReceivableReceiptDetail::getReceivableBillId,
Collectors.groupingBy(
item -> item.getReceiptStatus() == null ? OmsReceiptBill.ReceiptStatusEnum.WAIT_PAYMENT.getCode() : item.getReceiptStatus(),
Collectors.reducing(
BigDecimal.ZERO,
detail -> (detail.getReceiptAmount()!=null) ? detail.getReceiptAmount() : BigDecimal.ZERO,
BigDecimal::add
))));
Map<Long, Map<Long, OmsReceivableReceiptDetail>> planMap = omsReceivableReceiptDetailList.stream().collect(Collectors.groupingBy(
OmsReceivableReceiptDetail::getReceivableBillId,
Collectors.toMap(
OmsReceivableReceiptDetail::getReceiptPlanId,
Function.identity(),
(d1, d2) -> d1.getCreateTime().after(d2.getCreateTime()) ? d1 : d2
)
));
for (OmsReceivableBill bill : receivableBills) {
Map<String, BigDecimal> amountMap = planMap.get(bill.getId());
if (CollUtil.isNotEmpty(amountMap)) {
//已付金额 = 已付款金额-退款金额
bill.setReceivedAmount(amountMap.getOrDefault(OmsReceiptBill.ReceiptStatusEnum.PAYMENT.getCode(), BigDecimal.ZERO)
.subtract(amountMap.getOrDefault(OmsReceiptBill.ReceiptStatusEnum.REFUNDED.getCode(), BigDecimal.ZERO)));
//未付金额=总金额-已付金额-付款中金额
bill.setUnreceivedAmount(bill.getTotalPriceWithTax().subtract(bill.getReceivedAmount())
.subtract(amountMap.getOrDefault(OmsReceiptBill.ReceiptStatusEnum.WAIT_PAYMENT.getCode(), BigDecimal.ZERO)));
Map<Long, OmsReceivableReceiptDetail> paymentDetailMap =
planMap.getOrDefault(bill.getId(), Collections.emptyMap());
if (CollUtil.isNotEmpty(paymentDetailMap)) {
Map<String, BigDecimal> amountMap =
paymentDetailMap.values().stream()
.filter(d -> d.getReceiptAmount() != null)
.collect(Collectors.toMap(
OmsReceivableReceiptDetail::getReceiptStatus,
OmsReceivableReceiptDetail::getReceiptAmount,
BigDecimal::add
));
// 4. 金额计算(集中处理,逻辑更清晰)
BigDecimal paidAmount = amountMap
.getOrDefault(OmsReceiptBill.ReceiptStatusEnum.PAYMENT.getCode(), BigDecimal.ZERO)
.add(amountMap.getOrDefault(
OmsReceiptBill.ReceiptStatusEnum.REFUNDED.getCode(), BigDecimal.ZERO));
BigDecimal waitPayAmount =
amountMap.getOrDefault(
OmsReceiptBill.ReceiptStatusEnum.WAIT_PAYMENT.getCode(), BigDecimal.ZERO);
bill.setReceivedAmount(paidAmount);
bill.setUnreceivedAmount(bill.getTotalPriceWithTax() .subtract(paidAmount)
.subtract(waitPayAmount));
}
OmsReceivableReceiptPlan lastReceiptPlan = receiptPlanService.firstUnPayPlan(bill.getId());
bill.setLastReceiptPlanId(lastReceiptPlan == null ? -1 : lastReceiptPlan.getId());
@ -364,11 +399,11 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
}
/**
* SKY+YYMMdd+
* XYS+YYMMdd+
* @return
*/
private String generateReceivableBillCode() {
String prefix = "SKY";
String prefix = "XYS";
// 查询当天已有的最大序列号
String codePrefix = prefix + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATE_PATTERN);
int maxSequence = omsReceivableBillMapper.selectMaxCodeByPrefix(codePrefix);

View File

@ -1,5 +1,6 @@
package com.ruoyi.sip.service.impl;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -9,6 +10,8 @@ import java.util.stream.Collectors;
import cn.hutool.core.collection.CollUtil;
import com.ruoyi.sip.domain.OmsFinAttachment;
import com.ruoyi.sip.domain.OmsPayablePaymentDetail;
import com.ruoyi.sip.domain.dto.PaymentBillPayableDetailDTO;
import com.ruoyi.sip.domain.dto.ReceiptDetailDTO;
import com.ruoyi.sip.service.IOmsFinAttachmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -130,9 +133,27 @@ public class OmsReceivableReceiptDetailServiceImpl implements IOmsReceivableRece
public List<OmsReceivableReceiptDetail> listByReceivableBillIdList(List<Long> idList) {
if (CollUtil.isNotEmpty(idList)){
OmsReceivableReceiptDetail omsReceivableReceiptDetail = new OmsReceivableReceiptDetail();
omsReceivableReceiptDetail.setIdList(idList);
return omsReceivableReceiptDetailMapper.selectOmsReceivableReceiptDetailList(omsReceivableReceiptDetail);
omsReceivableReceiptDetail.setReceivableBillIdList(idList);
return omsReceivableReceiptDetailMapper.list(omsReceivableReceiptDetail);
}
return Collections.emptyList();
}
@Override
public List<ReceiptDetailDTO> listReceivableByReceiptBillCode(String receiptBillCode) {
List<ReceiptDetailDTO> receiptDetailDTOS = omsReceivableReceiptDetailMapper.listReceivableByReceiptBillCode(Collections.singletonList(receiptBillCode));
for (ReceiptDetailDTO detailDTO : receiptDetailDTOS) {
detailDTO.setReceiptRate(detailDTO.getReceiptAmount()
.divide(detailDTO.getTotalPriceWithTax(),4,java.math.RoundingMode.HALF_UP)
.multiply(new BigDecimal("100")));
}
return receiptDetailDTOS;
}
@Override
public List<OmsReceivableReceiptDetail> listByReceiptBillCode(String receiptBillCode) {
OmsReceivableReceiptDetail omsReceivableReceiptDetail = new OmsReceivableReceiptDetail();
omsReceivableReceiptDetail.setReceiptBillCode(receiptBillCode);
return omsReceivableReceiptDetailMapper.list(omsReceivableReceiptDetail);
}
}

View File

@ -31,109 +31,115 @@
<result property="receiptBankOpenAddress" column="receipt_bank_open_address"/>
<result property="bankNumber" column="bank_number"/>
<result property="refundStatus" column="refund_status"/>
<result property="remainingAmount" column="remaining_amount"/>
</resultMap>
<!-- 基本字段 -->
<sql id="Base_Column_List">
id, receipt_bill_code, receipt_bill_type, receipt_time, actual_receipt_time, partner_code, order_code, total_price_with_tax, total_price_without_tax, tax_amount, create_by, create_time, update_by, update_time, remark, del_flag, project_code, project_name, receipt_status, approve_status, approve_node, approve_time, receipt_method, receipt_account_name, receipt_bank_number, receipt_bank_open_address, bank_number, refund_status
<sql id="selectReceiptBillVo">
select
t1.id, t1.receipt_bill_code, t1.receipt_bill_type, t1.receipt_time, t1.actual_receipt_time, t1.partner_code,
t1.order_code, t1.total_price_with_tax, t1.total_price_without_tax, t1.tax_amount, t1.create_by, t1.create_time, t1.update_by, t1.update_time, t1.remark, t1.del_flag, t1.project_code, t1.project_name, t1.receipt_status, t1.approve_status, t1.approve_node, t1.approve_time, t1.receipt_method, t1.receipt_account_name, t1.receipt_bank_number, t1.receipt_bank_open_address, t1.bank_number, t1.refund_status,
t1.remaining_amount,t2.partner_name
from oms_receipt_bill t1
left join partner_info t2 on t1.partner_code=t2.partner_code
</sql>
<!--通过实体作为筛选条件查询-->
<select id="queryAll" resultMap="OmsReceiptBillMap">
select
<include refid="Base_Column_List"/>
from oms_receipt_bill
<include refid="selectReceiptBillVo"/>
<where>
<if test="id != null">
and id = #{id}
and t1.id = #{id}
</if>
<if test="receiptBillCode != null and receiptBillCode != ''">
and receipt_bill_code = #{receiptBillCode}
and t1.receipt_bill_code = #{receiptBillCode}
</if>
<if test="receiptBillType != null and receiptBillType != ''">
and receipt_bill_type = #{receiptBillType}
and t1.receipt_bill_type = #{receiptBillType}
</if>
<if test="receiptTime != null">
and receipt_time = #{receiptTime}
and t1.receipt_time = #{receiptTime}
</if>
<if test="actualReceiptTime != null">
and actual_receipt_time = #{actualReceiptTime}
and t1.actual_receipt_time = #{actualReceiptTime}
</if>
<if test="partnerCode != null and partnerCode != ''">
and partner_code = #{partnerCode}
and t1.partner_code = #{partnerCode}
</if>
<if test="orderCode != null and orderCode != ''">
and order_code = #{orderCode}
and t1.order_code = #{orderCode}
</if>
<if test="totalPriceWithTax != null">
and total_price_with_tax = #{totalPriceWithTax}
and t1.total_price_with_tax = #{totalPriceWithTax}
</if>
<if test="totalPriceWithoutTax != null">
and total_price_without_tax = #{totalPriceWithoutTax}
and t1.total_price_without_tax = #{totalPriceWithoutTax}
</if>
<if test="taxAmount != null">
and tax_amount = #{taxAmount}
and t1.tax_amount = #{taxAmount}
</if>
<if test="createBy != null and createBy != ''">
and create_by = #{createBy}
and t1.create_by = #{createBy}
</if>
<if test="createTime != null">
and create_time = #{createTime}
and t1.create_time = #{createTime}
</if>
<if test="updateBy != null and updateBy != ''">
and update_by = #{updateBy}
and t1.update_by = #{updateBy}
</if>
<if test="updateTime != null">
and update_time = #{updateTime}
and t1.update_time = #{updateTime}
</if>
<if test="remark != null and remark != ''">
and remark = #{remark}
and t1.remark = #{remark}
</if>
<if test="delFlag != null and delFlag != ''">
and del_flag = #{delFlag}
and t1.del_flag = #{delFlag}
</if>
<if test="projectCode != null and projectCode != ''">
and project_code = #{projectCode}
and t1.project_code = #{projectCode}
</if>
<if test="projectName != null and projectName != ''">
and project_name = #{projectName}
and t1.project_name = #{projectName}
</if>
<if test="receiptStatus != null and receiptStatus != ''">
and receipt_status = #{receiptStatus}
and t1.receipt_status = #{receiptStatus}
</if>
<if test="approveStatus != null and approveStatus != ''">
and approve_status = #{approveStatus}
and t1.approve_status = #{approveStatus}
</if>
<if test="approveNode != null and approveNode != ''">
and approve_node = #{approveNode}
and t1.approve_node = #{approveNode}
</if>
<if test="approveTime != null">
and approve_time = #{approveTime}
and t1.approve_time = #{approveTime}
</if>
<if test="receiptMethod != null and receiptMethod != ''">
and receipt_method = #{receiptMethod}
and t1.receipt_method = #{receiptMethod}
</if>
<if test="receiptAccountName != null and receiptAccountName != ''">
and receipt_account_name = #{receiptAccountName}
and t1.receipt_account_name = #{receiptAccountName}
</if>
<if test="receiptBankNumber != null and receiptBankNumber != ''">
and receipt_bank_number = #{receiptBankNumber}
and t1.receipt_bank_number = #{receiptBankNumber}
</if>
<if test="receiptBankOpenAddress != null and receiptBankOpenAddress != ''">
and receipt_bank_open_address = #{receiptBankOpenAddress}
and t1.receipt_bank_open_address = #{receiptBankOpenAddress}
</if>
<if test="bankNumber != null and bankNumber != ''">
and bank_number = #{bankNumber}
and t1.bank_number = #{bankNumber}
</if>
<if test="refundStatus != null and refundStatus != ''">
and refund_status = #{refundStatus}
and t1.refund_status = #{refundStatus}
</if>
</where>
</select>
<!--根据ID查详情-->
<select id="queryById" parameterType="Integer" resultMap="OmsReceiptBillMap">
<select id="queryById" parameterType="Long" resultMap="OmsReceiptBillMap">
SELECT id,
receipt_bill_code,
receipt_bill_type,
@ -161,7 +167,8 @@
receipt_bank_number,
receipt_bank_open_address,
bank_number,
refund_status
refund_status,
remaining_amount
FROM oms_receipt_bill
WHERE id = #{id}
LIMIT 1
@ -171,6 +178,18 @@
FROM oms_receipt_bill
WHERE receipt_bill_code LIKE CONCAT(#{prefix}, '%')
</select>
<select id="listRemainingAmountByPartnerCodeList" resultType="com.ruoyi.sip.domain.OmsReceiptBill">
SELECT
sum( remaining_amount ) remaining_amount,partner_code
FROM
oms_receipt_bill
where receipt_status='2' and partner_code in
<foreach item="item" collection="list" separator="," open="(" close=")" index="index">
#{item}
</foreach>
GROUP BY
partner_code
</select>
<!--新增所有列-->
@ -258,6 +277,9 @@
<if test="refundStatus != null and refundStatus != ''">
refund_status,
</if>
<if test="remainingAmount != null">
remaining_amount,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="receiptBillCode != null and receiptBillCode != ''">
@ -341,6 +363,9 @@
<if test="refundStatus != null and refundStatus != ''">
#{refundStatus},
</if>
<if test="remainingAmount != null">
#{remainingAmount},
</if>
</trim>
</insert>
@ -429,6 +454,9 @@
<if test="refundStatus != null and refundStatus != ''">
refund_status = #{refundStatus},
</if>
<if test="remainingAmount != null">
remaining_amount = #{remainingAmount},
</if>
</trim>
WHERE id = #{id}
</update>
@ -447,6 +475,9 @@
#{id}
</foreach>
</delete>
<delete id="clearRelationReceivable">
delete from oms_receivable_receipt_detail where receipt_bill_code=#{code}
</delete>
</mapper>

View File

@ -18,10 +18,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateTime" column="update_time" />
<result property="updateBy" column="update_by" />
<result property="receivableDetailType" column="receivable_detail_type" />
<result property="paymentAmountWithoutTax" column="payment_amount_without_tax" />
<result property="paymentAmountTax" column="payment_amount_tax" />
</resultMap>
<sql id="selectOmsReceivableReceiptDetailVo">
select id, receipt_plan_id, receivable_bill_id, receipt_time, receipt_amount, receipt_rate, receipt_bill_code, remark, create_time, create_by, update_time, receivable_detail_type
select id, receipt_plan_id, receivable_bill_id, receipt_time, receipt_amount, receipt_rate, receipt_bill_code, remark, create_time, create_by, update_time, receivable_detail_type, payment_amount_without_tax, payment_amount_tax
from oms_receivable_receipt_detail
</sql>
@ -51,6 +53,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
FROM
oms_receivable_receipt_detail t1
LEFT JOIN oms_receipt_bill t2 ON t1.receipt_bill_code = t2.receipt_bill_code
<where>
<if test="receiptPlanId != null ">and t1.receipt_plan_id = #{receiptPlanId}</if>
<if test="idList != null and idList.size>0 ">and t1.id in
<foreach collection="idList" separator="," close=")" open="(" item="item">#{item}</foreach>
</if>
<if test="receiptBillCode != null ">and t1.receipt_bill_code = #{receiptBillCode}</if>
<if test="receivableBillId != null ">and t1.receivable_bill_id = #{receivableBillId}</if>
<if test="receivableBillIdList != null and receivableBillIdList.size>0 ">and t1.receivable_bill_id in
<foreach collection="receivableBillIdList" separator="," close=")" open="(" item="item">
#{item}
</foreach>
</if>
</where>
</select>
<select id="selectByPaymentPlanIds" resultType="com.ruoyi.sip.domain.OmsReceivableReceiptDetail">
@ -66,6 +81,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
ORDER BY t1.create_time DESC
</select>
<select id="listReceivableByReceiptBillCode" resultType="com.ruoyi.sip.domain.dto.ReceiptDetailDTO">
select t1.receipt_amount, t2.receivable_bill_code, t4.project_name, t4.project_code, t2.total_price_with_tax
from (SELECT sum(receipt_amount) receipt_amount,
receivable_bill_id
FROM oms_receivable_receipt_detail
WHERE write_off_id is null and receipt_bill_code in
<foreach item="item" collection="list" separator="," open="(" close=")" index="">
#{item}
</foreach>
group by receivable_bill_id) t1
left join oms_receivable_bill t2 on t1.receivable_bill_id = t2.id
left join project_order_info t3 on t2.order_code = t3.order_code
left join project_info t4 on t3.project_id = t4.id;
</select>
<insert id="insertOmsReceivableReceiptDetail" parameterType="com.ruoyi.sip.domain.OmsReceivableReceiptDetail" useGeneratedKeys="true" keyProperty="id">
insert into oms_receivable_receipt_detail
@ -82,6 +111,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="updateBy != null and updateBy != ''">update_by,</if>
<if test="paymentAmountWithoutTax != null">payment_amount_without_tax,</if>
<if test="paymentAmountTax != null">payment_amount_tax,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="receiptPlanId != null">#{receiptPlanId},</if>
@ -96,6 +127,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="updateBy != null and updateBy != ''">#{updateBy},</if>
<if test="paymentAmountWithoutTax != null">#{paymentAmountWithoutTax},</if>
<if test="paymentAmountTax != null">#{paymentAmountTax},</if>
</trim>
</insert>
@ -112,6 +145,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="remark != null and remark != ''">remark = #{remark},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="paymentAmountWithoutTax != null">payment_amount_without_tax = #{paymentAmountWithoutTax},</if>
<if test="paymentAmountTax != null">payment_amount_tax = #{paymentAmountTax},</if>
</trim>
where id = #{id}
</update>