Merge branch 'test' of https://git.lionquexpress.com/wuweihua/BMS into test
This commit is contained in:
+7
-1
@@ -173,10 +173,16 @@ public class ArBillController extends BaseController {
|
||||
AjaxResult result = success();
|
||||
result.put("rows", pageData.getRows());
|
||||
result.put("total", pageData.getTotal());
|
||||
result.put("totalAmount", billService.sumPreviewBill(query));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 按需汇总生成预览范围内的应收金额。 */
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||
@PostMapping("/generate/preview/amount")
|
||||
public AjaxResult previewAmount(@RequestBody ArBillGenerateQuery query) {
|
||||
return success(billService.sumPreviewBill(query));
|
||||
}
|
||||
|
||||
/** 按生成预览使用的相同筛选条件提交异步导出任务。 */
|
||||
@Log(title = "应收账单生成订单", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.bms.receivable;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.bms.common.config.BmsConfig;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.redis.RedisCache;
|
||||
import com.bms.price.domain.PrcOrderBill;
|
||||
import com.bms.receivable.domain.vo.ArBillGenerateQuery;
|
||||
import com.bms.receivable.security.ArBillScopeResolver;
|
||||
import com.bms.receivable.service.IArBillService;
|
||||
import com.bms.receivable.task.ArBillGeneratePreviewExportRunner;
|
||||
import com.bms.web.controller.receivable.ArBillController;
|
||||
|
||||
class ArBillGeneratePreviewTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void previewDoesNotCalculateAmountUntilAmountEndpointIsCalled() {
|
||||
IArBillService service = mock(IArBillService.class);
|
||||
ArBillController controller = new ArBillController();
|
||||
ReflectionTestUtils.setField(controller, "billService", service);
|
||||
ArBillGenerateQuery query = new ArBillGenerateQuery();
|
||||
when(service.previewBill(query)).thenReturn(List.of(new PrcOrderBill()));
|
||||
when(service.sumPreviewBill(query)).thenReturn(new BigDecimal("12.34"));
|
||||
|
||||
try {
|
||||
AjaxResult preview = controller.preview(query);
|
||||
assertFalse(preview.containsKey("totalAmount"));
|
||||
verify(service, never()).sumPreviewBill(query);
|
||||
} finally {
|
||||
PageHelper.clearPage();
|
||||
}
|
||||
|
||||
assertEquals(new BigDecimal("12.34"), controller.previewAmount(query).get("data"));
|
||||
verify(service).sumPreviewBill(query);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportEndsWithTheSumOfTheExportedAmounts() throws Exception {
|
||||
String previousProfile = BmsConfig.getProfile();
|
||||
try {
|
||||
new BmsConfig().setProfile(tempDir.toString());
|
||||
IArBillService service = mock(IArBillService.class);
|
||||
ArBillGenerateQuery query = new ArBillGenerateQuery();
|
||||
when(service.previewBill(same(query), isNull())).thenReturn(List.of(
|
||||
bill("A", "0.10"), bill("B", "0.20"), bill("C", "-0.05"), bill("D", null)));
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor() {
|
||||
@Override
|
||||
public void execute(Runnable task) {
|
||||
task.run();
|
||||
}
|
||||
};
|
||||
ArBillGeneratePreviewExportRunner runner = new ArBillGeneratePreviewExportRunner(
|
||||
service, mock(RedisCache.class), mock(ArBillScopeResolver.class), executor);
|
||||
for (String dateType : List.of("BIZ_DATE", "PICKUP_DATE")) {
|
||||
query.setDateType(dateType);
|
||||
String taskId = runner.submit(query, 1L);
|
||||
Path file = tempDir.resolve("download/arBillGeneratePreviewExport/" + taskId + ".xlsx");
|
||||
|
||||
try (InputStream in = Files.newInputStream(file); Workbook workbook = WorkbookFactory.create(in)) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
Row header = sheet.getRow(0);
|
||||
Row lastRow = sheet.getRow(sheet.getLastRowNum());
|
||||
assertEquals("合计", lastRow.getCell(column(header, "订单号")).getStringCellValue());
|
||||
assertEquals(0.25, lastRow.getCell(column(header, "应收金额")).getNumericCellValue(), 0.000001);
|
||||
assertEquals(5, sheet.getLastRowNum());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
new BmsConfig().setProfile(previousProfile);
|
||||
}
|
||||
}
|
||||
|
||||
private static PrcOrderBill bill(String orderNo, String amount) {
|
||||
PrcOrderBill bill = new PrcOrderBill();
|
||||
bill.setOrderNo(orderNo);
|
||||
bill.setIncomeTotal(amount == null ? null : new BigDecimal(amount));
|
||||
return bill;
|
||||
}
|
||||
|
||||
private static int column(Row header, String name) {
|
||||
for (int i = 0; i < header.getLastCellNum(); i++) {
|
||||
if (name.equals(header.getCell(i).getStringCellValue())) return i;
|
||||
}
|
||||
throw new AssertionError("缺少导出列: " + name);
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -1,6 +1,7 @@
|
||||
package com.bms.receivable.task;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
@@ -75,10 +76,18 @@ public class ArBillGeneratePreviewExportRunner {
|
||||
put(taskId, userId, "RUNNING", 5, "正在查询订单", null, 0, fileName, null);
|
||||
List<PrcOrderBill> bills = billService.previewBill(query, scope);
|
||||
put(taskId, userId, "RUNNING", 40, "正在生成 Excel", null, bills.size(), fileName, null);
|
||||
List<ArBillGeneratePreviewExportRow> rows = new ArrayList<>(bills.size());
|
||||
List<ArBillGeneratePreviewExportRow> rows = new ArrayList<>(bills.size() + 1);
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
for (PrcOrderBill bill : bills) {
|
||||
rows.add(ArBillGeneratePreviewExportRow.from(bill));
|
||||
if (bill.getIncomeTotal() != null) {
|
||||
totalAmount = totalAmount.add(bill.getIncomeTotal());
|
||||
}
|
||||
}
|
||||
ArBillGeneratePreviewExportRow totalRow = new ArBillGeneratePreviewExportRow();
|
||||
totalRow.setOrderNo("合计");
|
||||
totalRow.setIncomeTotal(totalAmount);
|
||||
rows.add(totalRow);
|
||||
ExcelUtil<ArBillGeneratePreviewExportRow> util = new ExcelUtil<>(ArBillGeneratePreviewExportRow.class);
|
||||
if (ArBillDateType.PICKUP_DATE == ArBillDateType.of(query.getDateType())) {
|
||||
util.hideColumn("bizDate");
|
||||
|
||||
@@ -694,14 +694,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
#{excludedBillId}
|
||||
</foreach>
|
||||
</if>
|
||||
<choose>
|
||||
<when test="dateType != null and dateType == 'PICKUP_DATE'">
|
||||
order by p.pickup_date, p.order_no, p.bill_id
|
||||
</when>
|
||||
<otherwise>
|
||||
order by p.biz_date, p.order_no, p.bill_id
|
||||
</otherwise>
|
||||
</choose>
|
||||
order by p.bill_id
|
||||
</select>
|
||||
|
||||
<select id="sumPrcOrderBillIncomeByCustomerPeriod" resultType="java.math.BigDecimal">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
AND bill_id IN
|
||||
<foreach collection="sourceIds" item="sourceId" open="(" separator="," close=")">#{sourceId}</foreach>
|
||||
</select>
|
||||
<!-- c 固定为客户表别名;无部门层级扩展,不按角色管理 data_scope 过滤。 -->
|
||||
<!-- c 固定为客户表别名;销售经理可见本部门及下级部门,不按角色管理 data_scope 过滤。 -->
|
||||
<sql id="customerScope">
|
||||
<choose>
|
||||
<when test="scope != null and scope.all"/>
|
||||
@@ -22,7 +22,14 @@
|
||||
WHERE CAST(su.user_id AS CHAR) = c.sales_user_id AND su.del_flag = '0'
|
||||
AND (1 = 0
|
||||
<if test="scope.self">OR su.user_id = #{scope.userId}</if>
|
||||
<if test="scope.department">OR su.dept_id = #{scope.deptId}</if>
|
||||
<if test="scope.department">
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM sys_dept dept
|
||||
WHERE dept.dept_id = su.dept_id
|
||||
AND (dept.dept_id = #{scope.deptId}
|
||||
OR find_in_set(#{scope.deptId}, dept.ancestors))
|
||||
)
|
||||
</if>
|
||||
)
|
||||
)
|
||||
</when>
|
||||
|
||||
@@ -58,6 +58,15 @@ export function previewArBill(query) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getArBillPreviewAmount(query) {
|
||||
return request({
|
||||
url: '/ar/bill/generate/preview/amount',
|
||||
method: 'post',
|
||||
data: query,
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
export function previewExcludedArBill(query) {
|
||||
return request({
|
||||
url: '/ar/bill/generate/exclusion-preview',
|
||||
|
||||
@@ -41,7 +41,11 @@
|
||||
<span v-else class="payee-empty">当前币种尚未配置公司默认收款账户</span>
|
||||
</div>
|
||||
<div class="preview-toolbar">
|
||||
<span v-if="previewSearched" class="preview-summary">可选 {{ previewTotal }} 条订单,已选 <b>{{ displaySelectedCount }}</b> 条,应收合计 <b>{{ money(displaySelectedAmount) }}</b></span>
|
||||
<span v-if="previewSearched" class="preview-summary">
|
||||
可选 {{ previewTotal }} 条订单,已选 <b>{{ displaySelectedCount }}</b> 条
|
||||
<template v-if="previewAmountVisible">,应收合计 <b>{{ money(displaySelectedAmount) }}</b></template>
|
||||
<el-button link type="primary" :loading="previewAmountLoading" :disabled="previewLoading || !previewTotal" @click="queryPreviewAmount">查询应收合计</el-button>
|
||||
</span>
|
||||
<span v-else class="preview-summary">请选择客户、币种和账单截止日后查询可生成的计费订单</span>
|
||||
<el-button v-if="previewSearched && previewTotal > 0" type="warning" plain :icon="Download" :loading="previewExportLoading" :disabled="previewLoading || previewExportLoading" @click="exportPreview">导出查询结果</el-button>
|
||||
<el-button type="primary" plain :loading="previewLoading" :disabled="!bankAccount" @click="queryGeneratePreview">查询</el-button>
|
||||
@@ -104,7 +108,7 @@
|
||||
@current-change="handlePreviewPageChange"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="generate-footer-summary">已选 {{ displaySelectedCount }} 条订单 · {{ money(displaySelectedAmount) }}</span>
|
||||
<span class="generate-footer-summary">已选 {{ displaySelectedCount }} 条订单<template v-if="previewAmountVisible"> · {{ money(displaySelectedAmount) }}</template></span>
|
||||
<el-button type="primary" :loading="generateLoading" :disabled="!bankAccount || !previewSearched || !displaySelectedCount" @click="submitGenerate">生成账单</el-button>
|
||||
<el-button @click="dialogOpen = false">取消</el-button>
|
||||
</template>
|
||||
@@ -163,7 +167,7 @@
|
||||
<el-input v-model="billNameForm.billName" maxlength="100" show-word-limit placeholder="请输入账单名称" clearable @keyup.enter="confirmGenerate" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="bill-name-summary">已选择 {{ displaySelectedCount }} 条订单,应收合计 {{ money(displaySelectedAmount) }}</div>
|
||||
<div class="bill-name-summary">已选择 {{ displaySelectedCount }} 条订单<template v-if="previewAmountVisible">,应收合计 {{ money(displaySelectedAmount) }}</template></div>
|
||||
<template #footer>
|
||||
<el-button @click="billNameOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="generateLoading" @click="confirmGenerate">确认生成</el-button>
|
||||
@@ -173,7 +177,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="GenerateBillDialog">
|
||||
import { listArBillGenerateCustomerOptions, getDefaultArBankAccount, previewArBill, previewExcludedArBill, generateArBill } from '@/api/ar/bill'
|
||||
import { listArBillGenerateCustomerOptions, getDefaultArBankAccount, previewArBill, getArBillPreviewAmount, previewExcludedArBill, generateArBill } from '@/api/ar/bill'
|
||||
import { Download, Refresh, Search, Select } from '@element-plus/icons-vue'
|
||||
import { AR_BILL_SELECTION_MODE } from '@/constants/ar/enums'
|
||||
import { exportAsync } from '@/utils/excelExport'
|
||||
@@ -222,6 +226,9 @@ const previewSearched = ref(false)
|
||||
const previewRows = ref([])
|
||||
const previewTotal = ref(0)
|
||||
const previewTotalAmount = ref(0)
|
||||
const previewAmountVisible = ref(false)
|
||||
const previewAmountLoading = ref(false)
|
||||
let previewAmountRequestSeq = 0
|
||||
const previewPageNum = ref(1)
|
||||
// 主预览列表固定每页 5 条;Map 负责保存跨页勾选结果。
|
||||
const previewPageSize = 5
|
||||
@@ -267,7 +274,7 @@ const allSelected = computed(() => selectionMode.value === AR_BILL_SELECTION_MOD
|
||||
const displaySelectedCount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
|
||||
? Math.max(previewTotal.value - confirmedExcludeCount.value, 0) : selectedRows.value.length)
|
||||
const displaySelectedAmount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
|
||||
? Math.max(Number(previewTotalAmount.value || 0) - confirmedExcludeAmount.value, 0) : selectedAmount.value)
|
||||
? previewTotalAmount.value - confirmedExcludeAmount.value : selectedAmount.value)
|
||||
|
||||
watch(() => props.modelValue, visible => {
|
||||
if (visible) {
|
||||
@@ -323,6 +330,9 @@ function resetPreview() {
|
||||
previewRows.value = []
|
||||
previewTotal.value = 0
|
||||
previewTotalAmount.value = 0
|
||||
previewAmountRequestSeq += 1
|
||||
previewAmountVisible.value = false
|
||||
previewAmountLoading.value = false
|
||||
previewPageNum.value = 1
|
||||
selectedRows.value = []
|
||||
selectedRowMap.clear()
|
||||
@@ -366,7 +376,7 @@ function buildPreviewQuery(pageNum = previewPageNum.value, pageSize = previewPag
|
||||
return { ...(previewQuery.value || buildGenerateQuery()), pageNum, pageSize }
|
||||
}
|
||||
|
||||
// 查询按钮是预览入口,后端返回当前页、总数和全量金额合计。
|
||||
// 查询按钮是预览入口,后端返回当前页和总数。
|
||||
function queryGeneratePreview() {
|
||||
generateFormRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
@@ -383,7 +393,6 @@ function fetchPreview(initializeSelectionMode = false) {
|
||||
syncingPageSelection = true
|
||||
previewRows.value = res.rows || []
|
||||
previewTotal.value = Number(res.total) || 0
|
||||
previewTotalAmount.value = Number(res.totalAmount) || 0
|
||||
// 仅首次查询默认全选;翻页请求保留用户当前的选择模式。
|
||||
if (initializeSelectionMode) {
|
||||
selectionMode.value = previewTotal.value > 0
|
||||
@@ -397,6 +406,25 @@ function fetchPreview(initializeSelectionMode = false) {
|
||||
})
|
||||
}
|
||||
|
||||
async function queryPreviewAmount() {
|
||||
if (!previewQuery.value || !previewTotal.value || previewAmountLoading.value) return
|
||||
const requestSeq = ++previewAmountRequestSeq
|
||||
previewAmountVisible.value = false
|
||||
previewAmountLoading.value = true
|
||||
try {
|
||||
const res = await getArBillPreviewAmount({ ...previewQuery.value })
|
||||
if (requestSeq !== previewAmountRequestSeq) return
|
||||
if (res.data == null || !Number.isFinite(Number(res.data))) {
|
||||
proxy.$modal.msgError('应收合计查询失败,请重试')
|
||||
return
|
||||
}
|
||||
previewTotalAmount.value = Number(res.data)
|
||||
previewAmountVisible.value = true
|
||||
} finally {
|
||||
if (requestSeq === previewAmountRequestSeq) previewAmountLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePreviewPageChange(page) {
|
||||
previewPageNum.value = page
|
||||
fetchPreview()
|
||||
|
||||
Reference in New Issue
Block a user