Merge branch 'test' of https://git.lionquexpress.com/wuweihua/BMS
# Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
This commit is contained in:
+10
@@ -24,6 +24,7 @@ import com.bms.common.core.domain.AjaxResult;
|
|||||||
import com.bms.common.core.page.TableDataInfo;
|
import com.bms.common.core.page.TableDataInfo;
|
||||||
import com.bms.common.enums.BusinessType;
|
import com.bms.common.enums.BusinessType;
|
||||||
import com.bms.common.exception.ServiceException;
|
import com.bms.common.exception.ServiceException;
|
||||||
|
import com.bms.price.domain.PayProduct;
|
||||||
import com.bms.price.domain.PaySaveCommand;
|
import com.bms.price.domain.PaySaveCommand;
|
||||||
import com.bms.price.domain.PrcCustomerPriceSet;
|
import com.bms.price.domain.PrcCustomerPriceSet;
|
||||||
import com.bms.price.domain.PrcCustomerPriceSetBatch;
|
import com.bms.price.domain.PrcCustomerPriceSetBatch;
|
||||||
@@ -208,6 +209,15 @@ public class LabelPackageController extends BaseController
|
|||||||
return success(payProductService.selectPayProductDetail(productId));
|
return success(payProductService.selectPayProductDetail(productId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存打单产品明细(重量段/附加费等),按当前 term 覆盖子表 */
|
||||||
|
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||||
|
@Log(title = "打单产品明细", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/product")
|
||||||
|
public AjaxResult updateProduct(@RequestBody PayProduct product)
|
||||||
|
{
|
||||||
|
return toAjax(payProductService.updateDetail(product));
|
||||||
|
}
|
||||||
|
|
||||||
/** 查询打单产品已绑定渠道(默认应收 AR) */
|
/** 查询打单产品已绑定渠道(默认应收 AR) */
|
||||||
@PreAuthorize("@ss.hasPermi('price:label:query')")
|
@PreAuthorize("@ss.hasPermi('price:label:query')")
|
||||||
@GetMapping("/product/{productId}/channels")
|
@GetMapping("/product/{productId}/channels")
|
||||||
|
|||||||
+7
-1
@@ -173,10 +173,16 @@ public class ArBillController extends BaseController {
|
|||||||
AjaxResult result = success();
|
AjaxResult result = success();
|
||||||
result.put("rows", pageData.getRows());
|
result.put("rows", pageData.getRows());
|
||||||
result.put("total", pageData.getTotal());
|
result.put("total", pageData.getTotal());
|
||||||
result.put("totalAmount", billService.sumPreviewBill(query));
|
|
||||||
return result;
|
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)
|
@Log(title = "应收账单生成订单", businessType = BusinessType.EXPORT)
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.bms.receivable;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.nullable;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import com.bms.common.core.domain.entity.SysUser;
|
||||||
|
import com.bms.common.core.domain.model.LoginUser;
|
||||||
|
import com.bms.common.exception.ServiceException;
|
||||||
|
import com.bms.receivable.domain.ArBill;
|
||||||
|
import com.bms.receivable.domain.ArBillLog;
|
||||||
|
import com.bms.receivable.mapper.ArBillDetailMapper;
|
||||||
|
import com.bms.receivable.mapper.ArBillExportMapper;
|
||||||
|
import com.bms.receivable.mapper.ArBillMapper;
|
||||||
|
import com.bms.receivable.security.ArBillScopeResolver;
|
||||||
|
import com.bms.receivable.service.IArBillLogService;
|
||||||
|
import com.bms.receivable.service.impl.ArBillServiceImpl;
|
||||||
|
|
||||||
|
class ArBillConfirmTest {
|
||||||
|
private final ArBillMapper billMapper = mock(ArBillMapper.class);
|
||||||
|
private final ArBillDetailMapper detailMapper = mock(ArBillDetailMapper.class);
|
||||||
|
private final ArBillExportMapper exportMapper = mock(ArBillExportMapper.class);
|
||||||
|
private final ArBillScopeResolver scope = mock(ArBillScopeResolver.class);
|
||||||
|
private final IArBillLogService logService = mock(IArBillLogService.class);
|
||||||
|
private final ArBillServiceImpl service = new ArBillServiceImpl();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ReflectionTestUtils.setField(service, "billMapper", billMapper);
|
||||||
|
ReflectionTestUtils.setField(service, "detailMapper", detailMapper);
|
||||||
|
ReflectionTestUtils.setField(service, "exportMapper", exportMapper);
|
||||||
|
ReflectionTestUtils.setField(service, "billScope", scope);
|
||||||
|
ReflectionTestUtils.setField(service, "logService", logService);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
|
||||||
|
new LoginUser(7L, 1L, new SysUser(), Set.of()), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void clearSecurity() {
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readyDraftCanBeConfirmedWithoutReadingDetails() {
|
||||||
|
ArBill draft = bill("DRAFT", "READY");
|
||||||
|
draft.setTotalAmount(new BigDecimal("123.45"));
|
||||||
|
ArBill confirmed = bill("CONFIRMED", "READY");
|
||||||
|
when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(draft);
|
||||||
|
when(billMapper.updateArBillStatus(11L, "CONFIRMED", null, "finance", "DRAFT")).thenReturn(1);
|
||||||
|
when(billMapper.selectArBillByIdScoped(11L, null)).thenReturn(confirmed);
|
||||||
|
when(exportMapper.selectByBillId(11L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
assertSame(confirmed, service.confirmBill(11L, null, "finance", "request-1"));
|
||||||
|
verify(scope).requireBillAccessible(11L);
|
||||||
|
verifyNoInteractions(detailMapper);
|
||||||
|
verify(logService).record(any(ArBillLog.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unfinishedOrNonDraftBillStillCannotBeConfirmed() {
|
||||||
|
when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(
|
||||||
|
bill("DRAFT", "GENERATING"), bill("CONFIRMED", "READY"));
|
||||||
|
|
||||||
|
ServiceException unfinished = assertThrows(ServiceException.class,
|
||||||
|
() -> service.confirmBill(11L, null, "finance", "request-1"));
|
||||||
|
assertEquals("账单尚未生成完成,暂不能确认", unfinished.getMessage());
|
||||||
|
ServiceException notDraft = assertThrows(ServiceException.class,
|
||||||
|
() -> service.confirmBill(11L, null, "finance", "request-2"));
|
||||||
|
assertEquals("只有草稿账单可以确认,当前状态为 CONFIRMED", notDraft.getMessage());
|
||||||
|
verify(billMapper, never()).updateArBillStatus(eq(11L), any(), nullable(String.class), any(), any());
|
||||||
|
verifyNoInteractions(detailMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inaccessibleBillStillCannotBeConfirmed() {
|
||||||
|
when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(bill("DRAFT", "READY"));
|
||||||
|
doThrow(new ServiceException("账单不存在或无权访问")).when(scope).requireBillAccessible(11L);
|
||||||
|
|
||||||
|
assertThrows(ServiceException.class, () -> service.confirmBill(11L, null, "finance", "request-1"));
|
||||||
|
verify(billMapper, never()).updateArBillStatus(eq(11L), any(), nullable(String.class), any(), any());
|
||||||
|
verifyNoInteractions(detailMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void concurrentStatusChangeStillPreventsConfirmation() {
|
||||||
|
when(billMapper.selectArBillByIdForUpdate(11L)).thenReturn(bill("DRAFT", "READY"));
|
||||||
|
|
||||||
|
ServiceException error = assertThrows(ServiceException.class,
|
||||||
|
() -> service.confirmBill(11L, null, "finance", "request-1"));
|
||||||
|
assertEquals("账单状态已发生变化,请刷新后重试", error.getMessage());
|
||||||
|
verify(logService, never()).record(any(ArBillLog.class));
|
||||||
|
verifyNoInteractions(detailMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ArBill bill(String status, String generationStatus) {
|
||||||
|
ArBill bill = new ArBill();
|
||||||
|
bill.setArBillId(11L);
|
||||||
|
bill.setStatus(status);
|
||||||
|
bill.setGenerationStatus(generationStatus);
|
||||||
|
return bill;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,10 +73,4 @@ public class PaySurcharge
|
|||||||
private BigDecimal weightMax;
|
private BigDecimal weightMax;
|
||||||
|
|
||||||
private Integer sortNo;
|
private Integer sortNo;
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否计费并落库:false 不写入 pay_surcharge;null/缺省视为要计费(兼容应付全量保存)。
|
|
||||||
* 仅作为前端传参,不落库字段。
|
|
||||||
*/
|
|
||||||
private Boolean needBill;
|
|
||||||
}
|
}
|
||||||
|
|||||||
-24
@@ -461,8 +461,6 @@ public class PayProductServiceImpl implements IPayProductService
|
|||||||
{
|
{
|
||||||
product.setExtraLines(new ArrayList<>());
|
product.setExtraLines(new ArrayList<>());
|
||||||
}
|
}
|
||||||
// 计数前先按「是否计费」过滤,与落库行数一致
|
|
||||||
retainBillableSurcharges(product);
|
|
||||||
product.setWeightCount(product.getWeightBands().size());
|
product.setWeightCount(product.getWeightBands().size());
|
||||||
product.setSurchargeCount(product.getSurcharges().size());
|
product.setSurchargeCount(product.getSurcharges().size());
|
||||||
product.setExtraCount(product.getExtraLines().size());
|
product.setExtraCount(product.getExtraLines().size());
|
||||||
@@ -1531,7 +1529,6 @@ public class PayProductServiceImpl implements IPayProductService
|
|||||||
|
|
||||||
private void insertSurcharges(PayProduct product)
|
private void insertSurcharges(PayProduct product)
|
||||||
{
|
{
|
||||||
retainBillableSurcharges(product);
|
|
||||||
List<PaySurcharge> rows = product.getSurcharges();
|
List<PaySurcharge> rows = product.getSurcharges();
|
||||||
if (rows == null || rows.isEmpty())
|
if (rows == null || rows.isEmpty())
|
||||||
{
|
{
|
||||||
@@ -1550,27 +1547,6 @@ public class PayProductServiceImpl implements IPayProductService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 仅保留需要计费的附加费:needBill=false 不入库;null 视为要计费。
|
|
||||||
*/
|
|
||||||
private void retainBillableSurcharges(PayProduct product)
|
|
||||||
{
|
|
||||||
List<PaySurcharge> rows = product.getSurcharges();
|
|
||||||
if (rows == null || rows.isEmpty())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List<PaySurcharge> kept = new ArrayList<PaySurcharge>();
|
|
||||||
for (PaySurcharge fee : rows)
|
|
||||||
{
|
|
||||||
if (fee != null && !Boolean.FALSE.equals(fee.getNeedBill()))
|
|
||||||
{
|
|
||||||
kept.add(fee);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
product.setSurcharges(kept);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按附加费名称映射 prc_surcharge.sc_id;币种不能为空:附加费 -> 价卡 -> EUR。
|
* 按附加费名称映射 prc_surcharge.sc_id;币种不能为空:附加费 -> 价卡 -> EUR。
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -3127,7 +3127,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 业务规则计费重量(VOL_8000 等)。
|
* 业务规则计费重量(THREE_SIDE / VOL_8000)。
|
||||||
* 如需禁用:注释掉 resolveWeightByRule 调用,始终返回 baseWeight。
|
* 如需禁用:注释掉 resolveWeightByRule 调用,始终返回 baseWeight。
|
||||||
*/
|
*/
|
||||||
private BigDecimal applyBizRuleWeight(TmsPriceBillRequest request, BigDecimal baseWeight)
|
private BigDecimal applyBizRuleWeight(TmsPriceBillRequest request, BigDecimal baseWeight)
|
||||||
|
|||||||
@@ -16,11 +16,6 @@ public interface ArBillDetailMapper {
|
|||||||
List<ArBillDetail> selectByBillIdAndSourceIds(@Param("arBillId") Long arBillId,
|
List<ArBillDetail> selectByBillIdAndSourceIds(@Param("arBillId") Long arBillId,
|
||||||
@Param("sourceBillIds") List<String> sourceBillIds);
|
@Param("sourceBillIds") List<String> sourceBillIds);
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询账单明细及费用明细,避免详情页逐层查询。
|
|
||||||
*/
|
|
||||||
List<ArBillDetail> selectByBillIdWithFees(Long arBillId);
|
|
||||||
|
|
||||||
List<ArBillDetail> selectExportPage(@Param("arBillId") Long arBillId,
|
List<ArBillDetail> selectExportPage(@Param("arBillId") Long arBillId,
|
||||||
@Param("lastDetailNo") Integer lastDetailNo, @Param("pageSize") Integer pageSize);
|
@Param("lastDetailNo") Integer lastDetailNo, @Param("pageSize") Integer pageSize);
|
||||||
|
|
||||||
|
|||||||
+1
-26
@@ -985,7 +985,7 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认已完成生成且明细金额校验通过的草稿账单。
|
* 确认已完成生成的草稿账单。
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -1007,11 +1007,6 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
bill.getStatus(),
|
bill.getStatus(),
|
||||||
"只有草稿账单可以确认,当前状态为 " + bill.getStatus(), operator, requestId);
|
"只有草稿账单可以确认,当前状态为 " + bill.getStatus(), operator, requestId);
|
||||||
}
|
}
|
||||||
if (!validateDetails(arBillId, bill.getTotalAmount())) {
|
|
||||||
throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.CONFIRM,
|
|
||||||
bill.getStatus(),
|
|
||||||
"账单费用明细汇总与订单或账单总额不一致", operator, requestId);
|
|
||||||
}
|
|
||||||
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.CONFIRMED.getCode(),
|
if (billMapper.updateArBillStatus(arBillId, ArBillStatus.CONFIRMED.getCode(),
|
||||||
StringUtils.isEmpty(request == null ? null : request.getRemark()) ? bill.getRemark() : request.getRemark(),
|
StringUtils.isEmpty(request == null ? null : request.getRemark()) ? bill.getRemark() : request.getRemark(),
|
||||||
operator, ArBillStatus.DRAFT.getCode()) != 1) {
|
operator, ArBillStatus.DRAFT.getCode()) != 1) {
|
||||||
@@ -1313,26 +1308,6 @@ public class ArBillServiceImpl implements IArBillService {
|
|||||||
return message.length() > 500 ? message.substring(0, 500) : message;
|
return message.length() > 500 ? message.substring(0, 500) : message;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateDetails(Long arBillId, BigDecimal billTotal) {
|
|
||||||
List<ArBillDetail> details = detailMapper.selectByBillIdWithFees(arBillId);
|
|
||||||
BigDecimal detailTotal = BigDecimal.ZERO;
|
|
||||||
for (ArBillDetail detail : details) {
|
|
||||||
BigDecimal feeTotal = BigDecimal.ZERO;
|
|
||||||
List<ArBillDetailFee> fees = detail.getFees();
|
|
||||||
if (fees == null) {
|
|
||||||
fees = new ArrayList<ArBillDetailFee>();
|
|
||||||
}
|
|
||||||
for (ArBillDetailFee fee : fees) {
|
|
||||||
feeTotal = feeTotal.add(nvl(fee.getAmount()));
|
|
||||||
}
|
|
||||||
if (feeTotal.compareTo(nvl(detail.getTotalAmount())) != 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
detailTotal = detailTotal.add(nvl(detail.getTotalAmount()));
|
|
||||||
}
|
|
||||||
return !details.isEmpty() && detailTotal.compareTo(nvl(billTotal)) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArBillDetail toDetail(ArBill bill, PrcOrderBill source, PrcIncomeBill sourceIncomeBill,
|
private ArBillDetail toDetail(ArBill bill, PrcOrderBill source, PrcIncomeBill sourceIncomeBill,
|
||||||
int detailNo, String operator) {
|
int detailNo, String operator) {
|
||||||
ArBillDetail detail = new ArBillDetail();
|
ArBillDetail detail = new ArBillDetail();
|
||||||
|
|||||||
+10
-1
@@ -1,6 +1,7 @@
|
|||||||
package com.bms.receivable.task;
|
package com.bms.receivable.task;
|
||||||
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.LinkOption;
|
import java.nio.file.LinkOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -75,10 +76,18 @@ public class ArBillGeneratePreviewExportRunner {
|
|||||||
put(taskId, userId, "RUNNING", 5, "正在查询订单", null, 0, fileName, null);
|
put(taskId, userId, "RUNNING", 5, "正在查询订单", null, 0, fileName, null);
|
||||||
List<PrcOrderBill> bills = billService.previewBill(query, scope);
|
List<PrcOrderBill> bills = billService.previewBill(query, scope);
|
||||||
put(taskId, userId, "RUNNING", 40, "正在生成 Excel", null, bills.size(), fileName, null);
|
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) {
|
for (PrcOrderBill bill : bills) {
|
||||||
rows.add(ArBillGeneratePreviewExportRow.from(bill));
|
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);
|
ExcelUtil<ArBillGeneratePreviewExportRow> util = new ExcelUtil<>(ArBillGeneratePreviewExportRow.class);
|
||||||
if (ArBillDateType.PICKUP_DATE == ArBillDateType.of(query.getDateType())) {
|
if (ArBillDateType.PICKUP_DATE == ArBillDateType.of(query.getDateType())) {
|
||||||
util.hideColumn("bizDate");
|
util.hideColumn("bizDate");
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public interface IBmsBizRuleService
|
|||||||
int deleteByIds(Long[] ruleIds);
|
int deleteByIds(Long[] ruleIds);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将生效的重量规则(目前为 VOL_8000)应用到计费请求。
|
* 将生效的重量规则(THREE_SIDE / VOL_8000)应用到计费请求。
|
||||||
* @return 规则命中时返回覆盖后的计费重量;无规则适用时返回 null
|
* @return 规则命中时返回覆盖后的计费重量;无规则适用时返回 null
|
||||||
*/
|
*/
|
||||||
BigDecimal resolveWeightByRule(TmsPriceBillRequest request, BigDecimal systemWeight);
|
BigDecimal resolveWeightByRule(TmsPriceBillRequest request, BigDecimal systemWeight);
|
||||||
|
|||||||
+52
-16
@@ -136,8 +136,12 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// VOL_8000 在本地入库 / 计费时应用。THREE_SIDE 仅作为配置
|
// 先三边之和取重,未命中再走 VOL_8000
|
||||||
// (上游已应用该产品规则)。
|
BigDecimal threeSide = resolveThreeSideWeight(request, systemWeight);
|
||||||
|
if (threeSide != null)
|
||||||
|
{
|
||||||
|
return threeSide;
|
||||||
|
}
|
||||||
BigDecimal vol8000 = resolveVol8000(request, systemWeight);
|
BigDecimal vol8000 = resolveVol8000(request, systemWeight);
|
||||||
if (vol8000 != null)
|
if (vol8000 != null)
|
||||||
{
|
{
|
||||||
@@ -146,6 +150,47 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 三边之和取重:命中 THREE_SIDE 规则时,
|
||||||
|
* L+W+H < 上限取本地收货重;>= 上限取已有计费重。
|
||||||
|
*
|
||||||
|
* @param existingChargeWeight 当前已有计费重(未被三边规则覆盖前)
|
||||||
|
* @return 命中规则时返回计费重;未命中返回 null
|
||||||
|
*/
|
||||||
|
private BigDecimal resolveThreeSideWeight(TmsPriceBillRequest request, BigDecimal existingChargeWeight)
|
||||||
|
{
|
||||||
|
List<BmsBizRule> rules = bizRuleMapper.selectActiveByType(RULE_TYPE_THREE_SIDE);
|
||||||
|
if (rules == null || rules.isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (BmsBizRule rule : rules)
|
||||||
|
{
|
||||||
|
if (!matchThreeSideDims(rule, request, existingChargeWeight))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule.getSideSumMax() == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal sum = sideSum(request);
|
||||||
|
if (sum == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 三边之和 < 上限(如 90):取本地收货重;无收货重则保留已有计费重
|
||||||
|
if (sum.compareTo(rule.getSideSumMax()) < 0)
|
||||||
|
{
|
||||||
|
BigDecimal receiveWeight = firstPositive(request.getReceiveWeight());
|
||||||
|
return receiveWeight != null ? receiveWeight : existingChargeWeight;
|
||||||
|
}
|
||||||
|
// 三边之和 >= 上限:保留已有计费重
|
||||||
|
return existingChargeWeight;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VOL_8000:chargeWeight = max(L*W*H/8000, receive_weight)。
|
* VOL_8000:chargeWeight = max(L*W*H/8000, receive_weight)。
|
||||||
* 将体积重(/8000)与收货重比较。
|
* 将体积重(/8000)与收货重比较。
|
||||||
@@ -273,7 +318,10 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
|||||||
return new ArrayList<String>(set);
|
return new ArrayList<String>(set);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean matchThreeSide(BmsBizRule rule, TmsPriceBillRequest request, BigDecimal systemWeight)
|
/**
|
||||||
|
* 三边规则维度匹配(不包含三边和阈值判断)。
|
||||||
|
*/
|
||||||
|
private boolean matchThreeSideDims(BmsBizRule rule, TmsPriceBillRequest request, BigDecimal systemWeight)
|
||||||
{
|
{
|
||||||
if (rule == null)
|
if (rule == null)
|
||||||
{
|
{
|
||||||
@@ -292,19 +340,7 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
BigDecimal checkWeight = firstPositive(request.getReceiveWeight(), systemWeight, request.getWeight());
|
BigDecimal checkWeight = firstPositive(request.getReceiveWeight(), systemWeight, request.getWeight());
|
||||||
if (!inWeightRange(checkWeight, rule.getWeightMin(), rule.getWeightMax()))
|
return inWeightRange(checkWeight, rule.getWeightMin(), rule.getWeightMax());
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (rule.getSideSumMax() != null)
|
|
||||||
{
|
|
||||||
BigDecimal sum = sideSum(request);
|
|
||||||
if (sum == null || sum.compareTo(rule.getSideSumMax()) >= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean dimMatch(String requestCode, String ruleCode, boolean tryProductMap)
|
private boolean dimMatch(String requestCode, String ruleCode, boolean tryProductMap)
|
||||||
|
|||||||
@@ -69,49 +69,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
order by detail_no
|
order by detail_no
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<resultMap id="ArBillDetailWithFeesResult" type="ArBillDetail" extends="ArBillDetailResult">
|
|
||||||
<collection property="fees" ofType="ArBillDetailFee">
|
|
||||||
<id property="detailFeeId" column="fee_detail_id"/>
|
|
||||||
<result property="detailId" column="fee_parent_detail_id"/>
|
|
||||||
<result property="prcIncomeBillFeeId" column="fee_prc_income_bill_fee_id"/>
|
|
||||||
<result property="feeNo" column="fee_no"/>
|
|
||||||
<result property="feeType" column="fee_type"/>
|
|
||||||
<result property="feeName" column="fee_name"/>
|
|
||||||
<result property="scId" column="fee_sc_id"/>
|
|
||||||
<result property="quantity" column="fee_quantity"/>
|
|
||||||
<result property="unit" column="fee_unit"/>
|
|
||||||
<result property="unitPrice" column="fee_unit_price"/>
|
|
||||||
<result property="amount" column="fee_amount"/>
|
|
||||||
<result property="productAmount" column="fee_product_amount"/>
|
|
||||||
<result property="redlineAmount" column="fee_redline_amount"/>
|
|
||||||
<result property="currency" column="fee_currency"/>
|
|
||||||
<result property="calculationFormula" column="fee_calculation_formula"/>
|
|
||||||
<result property="roundMode" column="fee_round_mode"/>
|
|
||||||
<result property="remark" column="fee_remark"/>
|
|
||||||
<result property="createBy" column="fee_create_by"/>
|
|
||||||
<result property="createTime" column="fee_create_time"/>
|
|
||||||
</collection>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<select id="selectByBillIdWithFees" parameterType="long" resultMap="ArBillDetailWithFeesResult">
|
|
||||||
select d.detail_id, d.ar_bill_id, d.prc_order_bill_id, d.detail_no, d.order_no, d.tracking_no, d.transfer_no, d.receive_time,
|
|
||||||
d.product_code, d.product_name, d.goods_description, d.country_code, d.country_name, d.dest_postcode,
|
|
||||||
d.actual_weight, d.receive_weight, d.volume_weight, d.charge_weight, d.freight_rate, d.registration_fee, d.pkg_length, d.pkg_width, d.pkg_height,
|
|
||||||
d.currency, d.total_amount, d.product_amount, d.redline_amount, d.billing_fail_status, d.remark, d.create_by, d.create_time,
|
|
||||||
f.detail_fee_id as fee_detail_id, f.detail_id as fee_parent_detail_id,
|
|
||||||
f.prc_income_bill_fee_id as fee_prc_income_bill_fee_id,
|
|
||||||
f.fee_no, f.fee_type, f.fee_name, f.sc_id as fee_sc_id, f.quantity as fee_quantity,
|
|
||||||
f.unit as fee_unit, f.unit_price as fee_unit_price, f.amount as fee_amount,
|
|
||||||
f.product_amount as fee_product_amount, f.redline_amount as fee_redline_amount,
|
|
||||||
f.currency as fee_currency, f.calculation_formula as fee_calculation_formula,
|
|
||||||
f.round_mode as fee_round_mode, f.remark as fee_remark, f.create_by as fee_create_by,
|
|
||||||
f.create_time as fee_create_time
|
|
||||||
from ar_bill_detail d
|
|
||||||
left join ar_bill_detail_fee f on f.detail_id = d.detail_id
|
|
||||||
where d.ar_bill_id = #{arBillId}
|
|
||||||
order by d.detail_no, d.detail_id, f.fee_no, f.detail_fee_id
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectExportPage" resultMap="ArBillDetailResult">
|
<select id="selectExportPage" resultMap="ArBillDetailResult">
|
||||||
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
|
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, transfer_no, receive_time,
|
||||||
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
|
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
|
||||||
|
|||||||
@@ -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) {
|
export function previewExcludedArBill(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/ar/bill/generate/exclusion-preview',
|
url: '/ar/bill/generate/exclusion-preview',
|
||||||
|
|||||||
@@ -155,6 +155,17 @@ export function getLabelProduct(productId, termId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存打单产品明细(重量段/附加费) */
|
||||||
|
export function updateLabelProduct(data) {
|
||||||
|
return request({
|
||||||
|
url: '/price/label/product',
|
||||||
|
method: 'put',
|
||||||
|
data,
|
||||||
|
headers: { repeatSubmit: false },
|
||||||
|
timeout: 120000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function removeLabelProducts(priceSetId, productIds) {
|
export function removeLabelProducts(priceSetId, productIds) {
|
||||||
return request({
|
return request({
|
||||||
url: '/price/label/' + priceSetId + '/products/' + productIds,
|
url: '/price/label/' + priceSetId + '/products/' + productIds,
|
||||||
|
|||||||
@@ -41,7 +41,11 @@
|
|||||||
<span v-else class="payee-empty">当前币种尚未配置公司默认收款账户</span>
|
<span v-else class="payee-empty">当前币种尚未配置公司默认收款账户</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-toolbar">
|
<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 v-if="!previewAmountVisible" link type="primary" :loading="previewAmountLoading" :disabled="previewLoading || !previewTotal" @click="queryPreviewAmount">查询应收合计</el-button>
|
||||||
|
</span>
|
||||||
<span v-else class="preview-summary">请选择客户、币种和账单截止日后查询可生成的计费订单</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 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>
|
<el-button type="primary" plain :loading="previewLoading" :disabled="!bankAccount" @click="queryGeneratePreview">查询</el-button>
|
||||||
@@ -104,7 +108,7 @@
|
|||||||
@current-change="handlePreviewPageChange"
|
@current-change="handlePreviewPageChange"
|
||||||
/>
|
/>
|
||||||
<template #footer>
|
<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 type="primary" :loading="generateLoading" :disabled="!bankAccount || !previewSearched || !displaySelectedCount" @click="submitGenerate">生成账单</el-button>
|
||||||
<el-button @click="dialogOpen = false">取消</el-button>
|
<el-button @click="dialogOpen = false">取消</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -163,7 +167,7 @@
|
|||||||
<el-input v-model="billNameForm.billName" maxlength="100" show-word-limit placeholder="请输入账单名称" clearable @keyup.enter="confirmGenerate" />
|
<el-input v-model="billNameForm.billName" maxlength="100" show-word-limit placeholder="请输入账单名称" clearable @keyup.enter="confirmGenerate" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</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>
|
<template #footer>
|
||||||
<el-button @click="billNameOpen = false">取消</el-button>
|
<el-button @click="billNameOpen = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="generateLoading" @click="confirmGenerate">确认生成</el-button>
|
<el-button type="primary" :loading="generateLoading" @click="confirmGenerate">确认生成</el-button>
|
||||||
@@ -173,7 +177,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup name="GenerateBillDialog">
|
<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 { Download, Refresh, Search, Select } from '@element-plus/icons-vue'
|
||||||
import { AR_BILL_SELECTION_MODE } from '@/constants/ar/enums'
|
import { AR_BILL_SELECTION_MODE } from '@/constants/ar/enums'
|
||||||
import { exportAsync } from '@/utils/excelExport'
|
import { exportAsync } from '@/utils/excelExport'
|
||||||
@@ -222,6 +226,9 @@ const previewSearched = ref(false)
|
|||||||
const previewRows = ref([])
|
const previewRows = ref([])
|
||||||
const previewTotal = ref(0)
|
const previewTotal = ref(0)
|
||||||
const previewTotalAmount = ref(0)
|
const previewTotalAmount = ref(0)
|
||||||
|
const previewAmountVisible = ref(false)
|
||||||
|
const previewAmountLoading = ref(false)
|
||||||
|
let previewAmountRequestSeq = 0
|
||||||
const previewPageNum = ref(1)
|
const previewPageNum = ref(1)
|
||||||
// 主预览列表固定每页 5 条;Map 负责保存跨页勾选结果。
|
// 主预览列表固定每页 5 条;Map 负责保存跨页勾选结果。
|
||||||
const previewPageSize = 5
|
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
|
const displaySelectedCount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
|
||||||
? Math.max(previewTotal.value - confirmedExcludeCount.value, 0) : selectedRows.value.length)
|
? Math.max(previewTotal.value - confirmedExcludeCount.value, 0) : selectedRows.value.length)
|
||||||
const displaySelectedAmount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
|
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 => {
|
watch(() => props.modelValue, visible => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
@@ -323,6 +330,9 @@ function resetPreview() {
|
|||||||
previewRows.value = []
|
previewRows.value = []
|
||||||
previewTotal.value = 0
|
previewTotal.value = 0
|
||||||
previewTotalAmount.value = 0
|
previewTotalAmount.value = 0
|
||||||
|
previewAmountRequestSeq += 1
|
||||||
|
previewAmountVisible.value = false
|
||||||
|
previewAmountLoading.value = false
|
||||||
previewPageNum.value = 1
|
previewPageNum.value = 1
|
||||||
selectedRows.value = []
|
selectedRows.value = []
|
||||||
selectedRowMap.clear()
|
selectedRowMap.clear()
|
||||||
@@ -366,7 +376,7 @@ function buildPreviewQuery(pageNum = previewPageNum.value, pageSize = previewPag
|
|||||||
return { ...(previewQuery.value || buildGenerateQuery()), pageNum, pageSize }
|
return { ...(previewQuery.value || buildGenerateQuery()), pageNum, pageSize }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询按钮是预览入口,后端返回当前页、总数和全量金额合计。
|
// 查询按钮是预览入口,后端返回当前页和总数。
|
||||||
function queryGeneratePreview() {
|
function queryGeneratePreview() {
|
||||||
generateFormRef.value.validate(valid => {
|
generateFormRef.value.validate(valid => {
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
@@ -383,7 +393,6 @@ function fetchPreview(initializeSelectionMode = false) {
|
|||||||
syncingPageSelection = true
|
syncingPageSelection = true
|
||||||
previewRows.value = res.rows || []
|
previewRows.value = res.rows || []
|
||||||
previewTotal.value = Number(res.total) || 0
|
previewTotal.value = Number(res.total) || 0
|
||||||
previewTotalAmount.value = Number(res.totalAmount) || 0
|
|
||||||
// 仅首次查询默认全选;翻页请求保留用户当前的选择模式。
|
// 仅首次查询默认全选;翻页请求保留用户当前的选择模式。
|
||||||
if (initializeSelectionMode) {
|
if (initializeSelectionMode) {
|
||||||
selectionMode.value = previewTotal.value > 0
|
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) {
|
function handlePreviewPageChange(page) {
|
||||||
previewPageNum.value = page
|
previewPageNum.value = page
|
||||||
fetchPreview()
|
fetchPreview()
|
||||||
|
|||||||
@@ -79,16 +79,26 @@
|
|||||||
<div class="pay-edit-bar">
|
<div class="pay-edit-bar">
|
||||||
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、价格、货币或费用解释" class="pay-filter" @input="pane.feePageNo = 1" />
|
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、价格、货币或费用解释" class="pay-filter" @input="pane.feePageNo = 1" />
|
||||||
<el-button v-if="editable" size="small" type="primary" plain @click="openFeePicker(pane)">添加附加费</el-button>
|
<el-button v-if="editable" size="small" type="primary" plain @click="openFeePicker(pane)">添加附加费</el-button>
|
||||||
<template v-if="billSelect">
|
<el-button
|
||||||
<el-button size="small" @click="setFeesBill(pane, true)">全选计费</el-button>
|
v-if="editable"
|
||||||
<el-button size="small" @click="setFeesBill(pane, false)">取消全选</el-button>
|
size="small"
|
||||||
<span class="pay-bill-tip">勾选后才会写入 pay_surcharge;默认不勾选</span>
|
type="danger"
|
||||||
</template>
|
plain
|
||||||
|
:disabled="!feeCheckedCount(pane)"
|
||||||
|
@click="removeCheckedFees(pane)"
|
||||||
|
>批量删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="pageFees(pane)" border size="small" empty-text="没有附加费">
|
<el-table :data="pageFees(pane)" border size="small" empty-text="没有附加费">
|
||||||
<el-table-column v-if="billSelect" label="是否计费" width="88" align="center" fixed="left">
|
<el-table-column v-if="editable" width="48" align="center" fixed="left">
|
||||||
|
<template #header>
|
||||||
|
<el-checkbox
|
||||||
|
:model-value="isAllFilteredFeesChecked(pane)"
|
||||||
|
:indeterminate="isFeeCheckIndeterminate(pane)"
|
||||||
|
@update:model-value="v => setFilteredFeesChecked(pane, v)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-checkbox :model-value="row.needBill === true" @update:model-value="v => { row.needBill = !!v }" />
|
<el-checkbox :model-value="row._checked === true" @update:model-value="v => { row._checked = !!v }" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="feeName" label="附加费用" min-width="180" show-overflow-tooltip>
|
<el-table-column prop="feeName" label="附加费用" min-width="180" show-overflow-tooltip>
|
||||||
@@ -266,11 +276,6 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
/** 打单套餐:附加费表显示「是否计费」勾选,仅勾选行入库 */
|
|
||||||
billSelect: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
savingRowKey: {
|
savingRowKey: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
default: ''
|
default: ''
|
||||||
@@ -541,16 +546,49 @@ function filteredFees(pane) {
|
|||||||
|
|
||||||
function feeTabLabel(pane) {
|
function feeTabLabel(pane) {
|
||||||
const fees = pane.item.surcharges || []
|
const fees = pane.item.surcharges || []
|
||||||
if (!props.billSelect) return '附加费(' + fees.length + ')'
|
return '附加费(' + fees.length + ')'
|
||||||
const n = fees.filter(f => f && f.needBill === true).length
|
|
||||||
return '附加费(已选 ' + n + '/' + fees.length + ')'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFeesBill(pane, value) {
|
function feeCheckedCount(pane) {
|
||||||
const fees = pane.item.surcharges || []
|
return (pane?.item?.surcharges || []).filter(fee => fee && fee._checked === true).length
|
||||||
for (const fee of fees) {
|
|
||||||
if (fee) fee.needBill = !!value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAllFilteredFeesChecked(pane) {
|
||||||
|
const rows = filteredFees(pane)
|
||||||
|
return rows.length > 0 && rows.every(fee => fee && fee._checked === true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFeeCheckIndeterminate(pane) {
|
||||||
|
const rows = filteredFees(pane)
|
||||||
|
if (!rows.length) return false
|
||||||
|
const n = rows.filter(fee => fee && fee._checked === true).length
|
||||||
|
return n > 0 && n < rows.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFilteredFeesChecked(pane, value) {
|
||||||
|
for (const fee of filteredFees(pane)) {
|
||||||
|
if (fee) fee._checked = !!value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeCheckedFees(pane) {
|
||||||
|
const list = pane?.item?.surcharges
|
||||||
|
if (!list?.length) return
|
||||||
|
const checked = list.filter(fee => fee && fee._checked === true)
|
||||||
|
if (!checked.length) {
|
||||||
|
proxy.$modal.msgWarning('请先勾选要删除的附加费')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await proxy.$modal.confirm(`确认删除所选 ${checked.length} 项附加费?`)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const remain = list.filter(fee => !(fee && fee._checked === true))
|
||||||
|
pane.item.surcharges = remain
|
||||||
|
const maxPage = Math.max(1, Math.ceil(remain.length / pageSize) || 1)
|
||||||
|
if (pane.feePageNo > maxPage) pane.feePageNo = maxPage
|
||||||
|
proxy.$modal.msgSuccess(`已删除 ${checked.length} 项`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
@@ -589,9 +627,7 @@ function configToFeeRow(cfg, product) {
|
|||||||
priceUnit: isPercent ? '%' : (currency + '/ITEM'),
|
priceUnit: isPercent ? '%' : (currency + '/ITEM'),
|
||||||
feeDesc: String(cfg?.scNote || '').trim(),
|
feeDesc: String(cfg?.scNote || '').trim(),
|
||||||
calcRule: toCalcRule(cfg?.chargeType),
|
calcRule: toCalcRule(cfg?.chargeType),
|
||||||
calcParam: '',
|
calcParam: ''
|
||||||
// 从配置添加默认要计费
|
|
||||||
needBill: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,10 +774,6 @@ function statusText(status) {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.pay-bill-tip {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.pay-hint {
|
.pay-hint {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
|
|||||||
@@ -14,6 +14,13 @@
|
|||||||
placeholder="搜索附加费名称、计费方式、币种、国家或备注"
|
placeholder="搜索附加费名称、计费方式、币种、国家或备注"
|
||||||
class="picker-search"
|
class="picker-search"
|
||||||
/>
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="Plus"
|
||||||
|
v-hasPermi="['price:surcharge:add']"
|
||||||
|
@click="openCreate"
|
||||||
|
>新建附加费</el-button>
|
||||||
<span class="picker-meta">共 {{ filteredRows.length }} 项,已选 {{ picked.length }} 项</span>
|
<span class="picker-meta">共 {{ filteredRows.length }} 项,已选 {{ picked.length }} 项</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,17 +50,20 @@
|
|||||||
<el-table-column prop="scNote" label="备注" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="scNote" label="备注" min-width="160" show-overflow-tooltip />
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="picker-hint">勾选后点确定写入当前产品附加费表;已存在相同配置的不会重复添加。</div>
|
<div class="picker-hint">可先「新建附加费」写入配置库;勾选后点确定写入当前产品附加费表;已存在相同配置的不会重复添加。</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="visible = false">取 消</el-button>
|
<el-button @click="visible = false">取 消</el-button>
|
||||||
<el-button type="primary" :disabled="!picked.length" @click="submit">确 定</el-button>
|
<el-button type="primary" :disabled="!picked.length" @click="submit">确 定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<surcharge-form-dialog ref="formDialogRef" @success="onCreated" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { listSurchargeOptions } from '@/api/price/surcharge'
|
import { listSurchargeOptions } from '@/api/price/surcharge'
|
||||||
|
import SurchargeFormDialog from '@/views/price/basic/surcharge/components/SurchargeFormDialog.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Boolean, default: false },
|
modelValue: { type: Boolean, default: false },
|
||||||
@@ -64,6 +74,7 @@ const emit = defineEmits(['update:modelValue', 'confirm'])
|
|||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
|
const formDialogRef = ref()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
@@ -187,9 +198,25 @@ function onOpened() {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
tableRef.value?.clearSelection?.()
|
tableRef.value?.clearSelection?.()
|
||||||
})
|
})
|
||||||
if (!rows.value.length) {
|
|
||||||
loadRows()
|
loadRows()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
formDialogRef.value?.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCreated() {
|
||||||
|
const before = new Set((rows.value || []).map(row => String(row.scId)))
|
||||||
|
await loadRows()
|
||||||
|
// 自动勾选刚新建的配置
|
||||||
|
const next = new Set(pickedIds.value)
|
||||||
|
for (const row of rows.value || []) {
|
||||||
|
if (row?.scId == null) continue
|
||||||
|
const id = String(row.scId)
|
||||||
|
if (!before.has(id) && canSelect(row)) next.add(id)
|
||||||
|
}
|
||||||
|
pickedIds.value = next
|
||||||
|
nextTick(() => restoreSelection())
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
@@ -215,11 +242,12 @@ watch(() => props.modelValue, v => {
|
|||||||
.picker-bar {
|
.picker-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.picker-search {
|
.picker-search {
|
||||||
width: 360px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
.picker-meta {
|
.picker-meta {
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
||||||
<pay-config-detail :product="detailProduct" :editable="false" :bill-select="true" />
|
<pay-config-detail :product="detailProduct" :editable="false" />
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="detailVisible = false">关闭</el-button>
|
<el-button @click="detailVisible = false">关闭</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -187,9 +187,7 @@ async function onFile(uploadFile) {
|
|||||||
const data = res.data || {}
|
const data = res.data || {}
|
||||||
data.products = (data.products || []).map((item, index) => ({
|
data.products = (data.products || []).map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`,
|
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`
|
||||||
// 解析后默认不计费,勾选后才入 pay_surcharge
|
|
||||||
surcharges: (item.surcharges || []).map(fee => ({ ...fee, needBill: fee?.needBill === true }))
|
|
||||||
}))
|
}))
|
||||||
preview.value = data
|
preview.value = data
|
||||||
const picked = data.products.filter(row => canSelect(row))
|
const picked = data.products.filter(row => canSelect(row))
|
||||||
@@ -312,17 +310,16 @@ async function submit(payload = {}) {
|
|||||||
const productsPayload = (payload.products && payload.products.length)
|
const productsPayload = (payload.products && payload.products.length)
|
||||||
? payload.products
|
? payload.products
|
||||||
: selected.value.map(row => ({ ...row, productType: 'AR' }))
|
: selected.value.map(row => ({ ...row, productType: 'AR' }))
|
||||||
const filteredProducts = productsPayload.map(product => ({
|
const productsForSave = productsPayload.map(product => ({
|
||||||
...product,
|
...product,
|
||||||
productType: product.productType || 'AR',
|
productType: product.productType || 'AR'
|
||||||
surcharges: (product.surcharges || []).filter(fee => fee && fee.needBill === true)
|
|
||||||
}))
|
}))
|
||||||
const res = await saveLabelImport({
|
const res = await saveLabelImport({
|
||||||
priceSetId: payload.priceSetId || null,
|
priceSetId: payload.priceSetId || null,
|
||||||
priceSetName: String(payload.priceSetName || '').trim(),
|
priceSetName: String(payload.priceSetName || '').trim(),
|
||||||
priceSetNote: payload.priceSetNote || '',
|
priceSetNote: payload.priceSetNote || '',
|
||||||
overwrite: payload.overwrite != null ? payload.overwrite : overwrite.value,
|
overwrite: payload.overwrite != null ? payload.overwrite : overwrite.value,
|
||||||
products: filteredProducts
|
products: productsForSave
|
||||||
})
|
})
|
||||||
const data = res.data || {}
|
const data = res.data || {}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -22,11 +22,11 @@
|
|||||||
<el-table-column :label="colTerm" min-width="180" align="center">
|
<el-table-column :label="colTerm" min-width="180" align="center">
|
||||||
<template #default="{ row }">{{ termText(row) }}</template>
|
<template #default="{ row }">{{ termText(row) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="colStatus" width="90" align="center">
|
<!-- <el-table-column :label="colStatus" width="90" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="termStatusTag(row.termStatus)" size="small">{{ termStatusText(row.termStatus) }}</el-tag>
|
<el-tag :type="termStatusTag(row.termStatus)" size="small">{{ termStatusText(row.termStatus) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column> -->
|
||||||
<el-table-column prop="weightCount" :label="colWeight" width="80" align="center" />
|
<el-table-column prop="weightCount" :label="colWeight" width="80" align="center" />
|
||||||
<el-table-column prop="surchargeCount" :label="colSc" width="80" align="center" />
|
<el-table-column prop="surchargeCount" :label="colSc" width="80" align="center" />
|
||||||
<el-table-column :label="colBoundChannels" min-width="180" show-overflow-tooltip>
|
<el-table-column :label="colBoundChannels" min-width="180" show-overflow-tooltip>
|
||||||
@@ -34,16 +34,29 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="colAction" width="160" align="center" fixed="right">
|
<el-table-column :label="colAction" width="160" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="openProduct(row)" v-hasPermi="['price:label:query']">查看</el-button>
|
<el-button link type="primary" @click="openProduct(row)" v-hasPermi="['price:label:query', 'price:label:edit']">编辑</el-button>
|
||||||
<el-button link type="primary" @click="openBindList(row)" v-hasPermi="['price:label:edit']">绑定</el-button>
|
<el-button link type="primary" @click="openBindList(row)" v-hasPermi="['price:label:edit']">绑定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
||||||
<pay-config-detail v-loading="detailLoading" :product="detail" :editable="false" />
|
<pay-config-detail
|
||||||
|
v-loading="detailLoading"
|
||||||
|
:product="detail"
|
||||||
|
:editable="canEditDetail"
|
||||||
|
:saving-row-key="savingRowKey"
|
||||||
|
@save-row="saveDetailRow"
|
||||||
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="detailVisible = false">关闭</el-button>
|
<el-button @click="detailVisible = false">关闭</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="canEditDetail"
|
||||||
|
type="primary"
|
||||||
|
:loading="detailSaving"
|
||||||
|
v-hasPermi="['price:label:edit']"
|
||||||
|
@click="saveDetail"
|
||||||
|
>保存明细</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
@@ -125,9 +138,11 @@
|
|||||||
import {
|
import {
|
||||||
listLabelProducts,
|
listLabelProducts,
|
||||||
getLabelProduct,
|
getLabelProduct,
|
||||||
|
updateLabelProduct,
|
||||||
listLabelProductChannels,
|
listLabelProductChannels,
|
||||||
replaceLabelProductChannels
|
replaceLabelProductChannels
|
||||||
} from '@/api/price/label'
|
} from '@/api/price/label'
|
||||||
|
import { checkPermi } from '@/utils/permission'
|
||||||
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
|
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
|
||||||
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
|
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
|
||||||
|
|
||||||
@@ -144,7 +159,10 @@ const rows = ref([])
|
|||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
|
const detailSaving = ref(false)
|
||||||
|
const savingRowKey = ref('')
|
||||||
const detail = ref(null)
|
const detail = ref(null)
|
||||||
|
const canEditDetail = computed(() => checkPermi(['price:label:edit']))
|
||||||
|
|
||||||
const bindListVisible = ref(false)
|
const bindListVisible = ref(false)
|
||||||
const bindListLoading = ref(false)
|
const bindListLoading = ref(false)
|
||||||
@@ -258,9 +276,66 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanMoney(product) {
|
||||||
|
if (!product) return
|
||||||
|
;(product.weightBands || []).forEach(row => {
|
||||||
|
if (row && row.price === '') row.price = null
|
||||||
|
})
|
||||||
|
;(product.surcharges || []).forEach(row => {
|
||||||
|
if (row && row.amount === '') row.amount = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistDetail() {
|
||||||
|
if (!detail.value?.productId) {
|
||||||
|
throw new Error('NO_PRODUCT')
|
||||||
|
}
|
||||||
|
cleanMoney(detail.value)
|
||||||
|
await updateLabelProduct(detail.value)
|
||||||
|
const termId = detail.value.termId
|
||||||
|
const res = await getLabelProduct(detail.value.productId, termId)
|
||||||
|
detail.value = res.data || detail.value
|
||||||
|
await load()
|
||||||
|
emit('changed')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDetail() {
|
||||||
|
if (!detail.value || detailSaving.value || savingRowKey.value) return
|
||||||
|
detailSaving.value = true
|
||||||
|
try {
|
||||||
|
await persistDetail()
|
||||||
|
proxy.$modal.msgSuccess('明细已保存')
|
||||||
|
} catch (e) {
|
||||||
|
// request 拦截器已提示
|
||||||
|
} finally {
|
||||||
|
detailSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDetailRow(payload) {
|
||||||
|
if (!detail.value || savingRowKey.value || detailSaving.value) {
|
||||||
|
payload?.done?.(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
savingRowKey.value = payload?.rowKey || 'row'
|
||||||
|
let ok = false
|
||||||
|
try {
|
||||||
|
await persistDetail()
|
||||||
|
ok = true
|
||||||
|
proxy.$modal.msgSuccess('该行已保存')
|
||||||
|
} catch (e) {
|
||||||
|
ok = false
|
||||||
|
} finally {
|
||||||
|
savingRowKey.value = ''
|
||||||
|
payload?.done?.(ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openProduct(row) {
|
async function openProduct(row) {
|
||||||
detailVisible.value = true
|
detailVisible.value = true
|
||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
|
detailSaving.value = false
|
||||||
|
savingRowKey.value = ''
|
||||||
detail.value = null
|
detail.value = null
|
||||||
try {
|
try {
|
||||||
const res = await getLabelProduct(row.productId, row.termId)
|
const res = await getLabelProduct(row.productId, row.termId)
|
||||||
|
|||||||
@@ -38,11 +38,11 @@
|
|||||||
<el-table-column label="失效时间" min-width="170" align="center">
|
<el-table-column label="失效时间" min-width="170" align="center">
|
||||||
<template #default="{ row }">{{ formatHour(row.expireTime) || '-' }}</template>
|
<template #default="{ row }">{{ formatHour(row.expireTime) || '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="100" align="center">
|
<!-- <el-table-column label="状态" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column> -->
|
||||||
<el-table-column label="产品数" width="90" align="center">
|
<el-table-column label="产品数" width="90" align="center">
|
||||||
<template #default="{ row }">{{ row.productCount || 0 }}</template>
|
<template #default="{ row }">{{ row.productCount || 0 }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|||||||
@@ -100,7 +100,6 @@
|
|||||||
<pay-config-detail
|
<pay-config-detail
|
||||||
:product="detailProduct"
|
:product="detailProduct"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
:bill-select="true"
|
|
||||||
:saving-row-key="savingRowKey"
|
:saving-row-key="savingRowKey"
|
||||||
@save-row="saveDetailRow"
|
@save-row="saveDetailRow"
|
||||||
/>
|
/>
|
||||||
@@ -220,8 +219,7 @@ async function loadEditProducts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
// 库中已落库的附加费默认勾选(保留原计费项)
|
productRows.value = details
|
||||||
productRows.value = markSurchargeBillFlags(details, true)
|
|
||||||
selectedProducts.value = []
|
selectedProducts.value = []
|
||||||
importMeta.sourceFile = details[0]?.sourceFile || ''
|
importMeta.sourceFile = details[0]?.sourceFile || ''
|
||||||
importMeta.supplierName = details[0]?.supplierName || ''
|
importMeta.supplierName = details[0]?.supplierName || ''
|
||||||
@@ -235,23 +233,6 @@ async function openImport() {
|
|||||||
importRef.value?.open()
|
importRef.value?.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
function markSurchargeBillFlags(products, defaultNeedBill) {
|
|
||||||
return (products || []).map(product => ({
|
|
||||||
...product,
|
|
||||||
surcharges: (product.surcharges || []).map(fee => ({
|
|
||||||
...fee,
|
|
||||||
needBill: fee?.needBill === true ? true : (fee?.needBill === false ? false : !!defaultNeedBill)
|
|
||||||
}))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function withBillableSurcharges(products) {
|
|
||||||
return (products || []).map(product => ({
|
|
||||||
...product,
|
|
||||||
surcharges: (product.surcharges || []).filter(fee => fee && fee.needBill === true)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function onProductSelect(rows) {
|
function onProductSelect(rows) {
|
||||||
selectedProducts.value = rows || []
|
selectedProducts.value = rows || []
|
||||||
}
|
}
|
||||||
@@ -297,8 +278,7 @@ async function removeSelectedProducts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onImportConfirm(payload) {
|
function onImportConfirm(payload) {
|
||||||
// 价卡解析行默认不计费,需手动勾选
|
productRows.value = payload?.products || []
|
||||||
productRows.value = markSurchargeBillFlags(payload?.products || [], false)
|
|
||||||
selectedProducts.value = []
|
selectedProducts.value = []
|
||||||
importMeta.sourceFile = payload?.sourceFile || ''
|
importMeta.sourceFile = payload?.sourceFile || ''
|
||||||
importMeta.supplierName = payload?.supplierName || ''
|
importMeta.supplierName = payload?.supplierName || ''
|
||||||
@@ -414,7 +394,7 @@ async function finish() {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
overwrite: editVersionMode.value ? true : importMeta.overwrite,
|
overwrite: editVersionMode.value ? true : importMeta.overwrite,
|
||||||
products: withBillableSurcharges(productRows.value)
|
products: productRows.value
|
||||||
}
|
}
|
||||||
if (editVersionMode.value) {
|
if (editVersionMode.value) {
|
||||||
payload.priceSetId = priceSetId.value
|
payload.priceSetId = priceSetId.value
|
||||||
|
|||||||
Reference in New Issue
Block a user