增加可修改计费重功能
This commit is contained in:
@@ -4,6 +4,7 @@ import java.util.Date;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
@@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import com.bms.common.annotation.Log;
|
import com.bms.common.annotation.Log;
|
||||||
import com.bms.common.core.controller.BaseController;
|
import com.bms.common.core.controller.BaseController;
|
||||||
import com.bms.common.core.domain.AjaxResult;
|
import com.bms.common.core.domain.AjaxResult;
|
||||||
@@ -24,13 +26,18 @@ import com.bms.common.enums.BusinessType;
|
|||||||
import com.bms.common.utils.DateUtils;
|
import com.bms.common.utils.DateUtils;
|
||||||
import com.bms.common.utils.MessageUtils;
|
import com.bms.common.utils.MessageUtils;
|
||||||
import com.bms.common.utils.StringUtils;
|
import com.bms.common.utils.StringUtils;
|
||||||
|
import com.bms.common.utils.poi.ExcelUtil;
|
||||||
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
|
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
|
||||||
|
import com.bms.price.domain.ChargeWeightImportTask;
|
||||||
import com.bms.price.domain.PrcBillingTask;
|
import com.bms.price.domain.PrcBillingTask;
|
||||||
import com.bms.price.domain.PrcOrderBill;
|
import com.bms.price.domain.PrcOrderBill;
|
||||||
import com.bms.price.domain.PrcQuote;
|
import com.bms.price.domain.PrcQuote;
|
||||||
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
|
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
|
||||||
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
|
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
|
||||||
import com.bms.price.domain.vo.BillingTaskSubmitRequest;
|
import com.bms.price.domain.vo.BillingTaskSubmitRequest;
|
||||||
|
import com.bms.price.domain.vo.ChargeWeightImportRow;
|
||||||
|
import com.bms.price.domain.vo.ChargeWeightTemplateRequest;
|
||||||
|
import com.bms.price.domain.vo.UpdateChargeWeightRequest;
|
||||||
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
||||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||||
import com.bms.price.domain.vo.TmsPriceQuoteRequest;
|
import com.bms.price.domain.vo.TmsPriceQuoteRequest;
|
||||||
@@ -312,4 +319,64 @@ public class BillingController extends BaseController
|
|||||||
TmsPriceQuoteResult result = prcOrderBillService.bill(request);
|
TmsPriceQuoteResult result = prcOrderBillService.bill(request);
|
||||||
return success(result);
|
return success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update charge_weight (keep weight), then rebill. */
|
||||||
|
@PreAuthorize("@ss.hasPermi('price:billing:chargeWeightEdit')")
|
||||||
|
@Log(title = "Billing charge weight", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/bill/chargeWeight")
|
||||||
|
public AjaxResult updateChargeWeight(@Validated @RequestBody UpdateChargeWeightRequest request)
|
||||||
|
{
|
||||||
|
return success(prcOrderBillService.updateChargeWeight(request.getBillId(), request.getChargeWeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download import template. fillByFilter=true fills orderNo/customer from list filters.
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('price:billing:chargeWeightEdit')")
|
||||||
|
@PostMapping("/bill/chargeWeight/importTemplate")
|
||||||
|
public void importChargeWeightTemplate(HttpServletResponse response,
|
||||||
|
@RequestBody(required = false) ChargeWeightTemplateRequest request)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
request = new ChargeWeightTemplateRequest();
|
||||||
|
}
|
||||||
|
List<ChargeWeightImportRow> rows = prcOrderBillService.buildChargeWeightTemplateRows(request);
|
||||||
|
ExcelUtil<ChargeWeightImportRow> util = new ExcelUtil<ChargeWeightImportRow>(ChargeWeightImportRow.class);
|
||||||
|
if (rows == null || rows.isEmpty())
|
||||||
|
{
|
||||||
|
util.importTemplateExcel(response, "charge_weight");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
util.exportExcel(response, rows, "charge_weight");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async import charge_weight updates and rebill.
|
||||||
|
* Returns taskId; poll /bill/chargeWeight/import/task/{taskId}.
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('price:billing:chargeWeightEdit')")
|
||||||
|
@Log(title = "Billing charge weight import", businessType = BusinessType.IMPORT)
|
||||||
|
@PostMapping("/bill/chargeWeight/importData")
|
||||||
|
public AjaxResult importChargeWeight(MultipartFile file) throws Exception
|
||||||
|
{
|
||||||
|
ExcelUtil<ChargeWeightImportRow> util = new ExcelUtil<ChargeWeightImportRow>(ChargeWeightImportRow.class);
|
||||||
|
List<ChargeWeightImportRow> rows = util.importExcel(file.getInputStream());
|
||||||
|
String taskId = prcOrderBillService.submitImportChargeWeight(rows);
|
||||||
|
Map<String, Object> payload = new HashMap<String, Object>();
|
||||||
|
payload.put("taskId", taskId);
|
||||||
|
return success(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('price:billing:chargeWeightEdit')")
|
||||||
|
@GetMapping("/bill/chargeWeight/import/task/{taskId}")
|
||||||
|
public AjaxResult getChargeWeightImportTask(@PathVariable String taskId)
|
||||||
|
{
|
||||||
|
ChargeWeightImportTask task = prcOrderBillService.getChargeWeightImportTask(taskId);
|
||||||
|
if (task == null)
|
||||||
|
{
|
||||||
|
return error(MessageUtils.message("price.bill.fee.batch.taskNotExists"));
|
||||||
|
}
|
||||||
|
return success(task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.bms.price.domain;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/** Async charge-weight import progress (Redis). */
|
||||||
|
public class ChargeWeightImportTask implements Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public static final String STATUS_PENDING = "PENDING";
|
||||||
|
public static final String STATUS_RUNNING = "RUNNING";
|
||||||
|
public static final String STATUS_SUCCESS = "SUCCESS";
|
||||||
|
public static final String STATUS_FAIL = "FAIL";
|
||||||
|
|
||||||
|
private String taskId;
|
||||||
|
private String status;
|
||||||
|
private int percent;
|
||||||
|
private String message;
|
||||||
|
private String error;
|
||||||
|
private int total;
|
||||||
|
private int success;
|
||||||
|
private int fail;
|
||||||
|
|
||||||
|
public String getTaskId()
|
||||||
|
{
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTaskId(String taskId)
|
||||||
|
{
|
||||||
|
this.taskId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus()
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status)
|
||||||
|
{
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPercent()
|
||||||
|
{
|
||||||
|
return percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPercent(int percent)
|
||||||
|
{
|
||||||
|
this.percent = percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage()
|
||||||
|
{
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message)
|
||||||
|
{
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getError()
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setError(String error)
|
||||||
|
{
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotal()
|
||||||
|
{
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(int total)
|
||||||
|
{
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSuccess()
|
||||||
|
{
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuccess(int success)
|
||||||
|
{
|
||||||
|
this.success = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFail()
|
||||||
|
{
|
||||||
|
return fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFail(int fail)
|
||||||
|
{
|
||||||
|
this.fail = fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.bms.price.domain.vo;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import com.bms.common.annotation.Excel;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Excel row for batch charge_weight update. */
|
||||||
|
@Data
|
||||||
|
public class ChargeWeightImportRow implements Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Excel(name = "订单号")
|
||||||
|
private String orderNo;
|
||||||
|
|
||||||
|
@Excel(name = "客户")
|
||||||
|
private String customer;
|
||||||
|
|
||||||
|
@Excel(name = "人工修改计费重")
|
||||||
|
private BigDecimal chargeWeight;
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.bms.price.domain.vo;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Download charge-weight import template; optionally fill order/customer by list filters. */
|
||||||
|
@Data
|
||||||
|
public class ChargeWeightTemplateRequest implements Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** true = fill orderNo/customer from current list filters */
|
||||||
|
private boolean fillByFilter;
|
||||||
|
|
||||||
|
private List<String> orderNos;
|
||||||
|
private List<String> trackingNos;
|
||||||
|
private List<String> transferNos;
|
||||||
|
private List<String> customerCodes;
|
||||||
|
private List<String> productCodes;
|
||||||
|
private String creditArrearsStatus;
|
||||||
|
private String beginBizDate;
|
||||||
|
private String endBizDate;
|
||||||
|
}
|
||||||
@@ -35,6 +35,6 @@ public class TmsPriceQuoteRequest implements Serializable
|
|||||||
/** yyyy-MM-dd or yyyy-MM-dd HH:mm:ss; quote defaults today; bill required */
|
/** yyyy-MM-dd or yyyy-MM-dd HH:mm:ss; quote defaults today; bill required */
|
||||||
private String bizDate;
|
private String bizDate;
|
||||||
private Integer tmsId;
|
private Integer tmsId;
|
||||||
/** Header X-Api-Code 优先;Body 兜底 */
|
/** Header X-Api-Code first; body as fallback */
|
||||||
private String apiCode;
|
private String apiCode;
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.bms.price.domain.vo;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Single-bill update for charge_weight (manual override; weight kept). */
|
||||||
|
@Data
|
||||||
|
public class UpdateChargeWeightRequest implements Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@NotBlank(message = "billId is required")
|
||||||
|
private String billId;
|
||||||
|
|
||||||
|
@NotNull(message = "chargeWeight is required")
|
||||||
|
@DecimalMin(value = "0.001", message = "chargeWeight must be > 0")
|
||||||
|
private BigDecimal chargeWeight;
|
||||||
|
}
|
||||||
@@ -4,9 +4,12 @@ import java.math.BigDecimal;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.bms.price.domain.PrcOrderBill;
|
import com.bms.price.domain.PrcOrderBill;
|
||||||
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
|
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
|
||||||
|
import com.bms.price.domain.ChargeWeightImportTask;
|
||||||
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
|
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
|
||||||
import com.bms.price.domain.vo.BatchAddIncomeBillFeePreview;
|
import com.bms.price.domain.vo.BatchAddIncomeBillFeePreview;
|
||||||
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
|
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
|
||||||
|
import com.bms.price.domain.vo.ChargeWeightImportRow;
|
||||||
|
import com.bms.price.domain.vo.ChargeWeightTemplateRequest;
|
||||||
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
||||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||||
import com.bms.price.domain.vo.TmsPriceQuoteResult;
|
import com.bms.price.domain.vo.TmsPriceQuoteResult;
|
||||||
@@ -77,4 +80,35 @@ public interface IPrcOrderBillService
|
|||||||
* @return 更新后的计费结果
|
* @return 更新后的计费结果
|
||||||
*/
|
*/
|
||||||
TmsPriceQuoteResult updateCreditArrearsStatus(String billId);
|
TmsPriceQuoteResult updateCreditArrearsStatus(String billId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update charge_weight (manual override), keep weight, and rebill.
|
||||||
|
* Applies data-scope check.
|
||||||
|
*
|
||||||
|
* @param billId bill id
|
||||||
|
* @param chargeWeight new charge weight (kg)
|
||||||
|
*/
|
||||||
|
TmsPriceQuoteResult updateChargeWeight(String billId, BigDecimal chargeWeight);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update charge_weight without data-scope check (pre-validated async import).
|
||||||
|
*/
|
||||||
|
TmsPriceQuoteResult updateChargeWeightByBillId(String billId, BigDecimal chargeWeight);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit async import of charge_weight updates; returns taskId.
|
||||||
|
*
|
||||||
|
* @param rows excel rows
|
||||||
|
* @return taskId
|
||||||
|
*/
|
||||||
|
String submitImportChargeWeight(List<ChargeWeightImportRow> rows);
|
||||||
|
|
||||||
|
/** Poll Redis progress for charge-weight import task. */
|
||||||
|
ChargeWeightImportTask getChargeWeightImportTask(String taskId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build import-template rows. Empty when fillByFilter=false;
|
||||||
|
* otherwise fill orderNo/customer from list filters (data-scoped).
|
||||||
|
*/
|
||||||
|
List<ChargeWeightImportRow> buildChargeWeightTemplateRows(ChargeWeightTemplateRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
+359
-3
@@ -43,11 +43,16 @@ import com.bms.price.domain.PrcPayBill;
|
|||||||
import com.bms.price.domain.PrcPayBillFee;
|
import com.bms.price.domain.PrcPayBillFee;
|
||||||
import com.bms.price.domain.PrcSurcharge;
|
import com.bms.price.domain.PrcSurcharge;
|
||||||
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
|
import com.bms.price.domain.BatchAddIncomeBillFeeTask;
|
||||||
|
import com.bms.price.domain.ChargeWeightImportTask;
|
||||||
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
|
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
|
||||||
import com.bms.price.domain.vo.BatchAddIncomeBillFeePreview;
|
import com.bms.price.domain.vo.BatchAddIncomeBillFeePreview;
|
||||||
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
|
import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest;
|
||||||
|
import com.bms.price.domain.vo.ChargeWeightImportRow;
|
||||||
|
import com.bms.price.domain.vo.ChargeWeightTemplateRequest;
|
||||||
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
|
||||||
import com.bms.price.task.PrcBatchAddIncomeFeeAsyncRunner;
|
import com.bms.price.task.PrcBatchAddIncomeFeeAsyncRunner;
|
||||||
|
import com.bms.price.task.PrcChargeWeightImportAsyncRunner;
|
||||||
|
import com.bms.price.task.PrcChargeWeightImportAsyncRunner.ChargeWeightImportWorkItem;
|
||||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||||
import com.bms.price.domain.vo.TmsOrderBillStatusResult;
|
import com.bms.price.domain.vo.TmsOrderBillStatusResult;
|
||||||
import com.bms.price.domain.vo.TmsPriceQuoteFeeLine;
|
import com.bms.price.domain.vo.TmsPriceQuoteFeeLine;
|
||||||
@@ -128,8 +133,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PrcBatchAddIncomeFeeAsyncRunner batchAddIncomeFeeAsyncRunner;
|
private PrcBatchAddIncomeFeeAsyncRunner batchAddIncomeFeeAsyncRunner;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PrcChargeWeightImportAsyncRunner chargeWeightImportAsyncRunner;
|
||||||
|
|
||||||
private static final int BATCH_ADD_FEE_MAX = 5000;
|
private static final int BATCH_ADD_FEE_MAX = 5000;
|
||||||
private static final int BATCH_ADD_FEE_PREVIEW_LIMIT = 20;
|
private static final int BATCH_ADD_FEE_PREVIEW_LIMIT = 20;
|
||||||
|
private static final int CHARGE_WEIGHT_IMPORT_MAX = 5000;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -148,9 +157,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
|||||||
}
|
}
|
||||||
Date bizDate = resolveBizDate(request.getBizDate());
|
Date bizDate = resolveBizDate(request.getBizDate());
|
||||||
assertOrderBillEditable(request.getOrderNo());
|
assertOrderBillEditable(request.getOrderNo());
|
||||||
BigDecimal weight = request.getWeight();
|
PrcOrderBill existed = orderBillMapper.selectPrcOrderBillByOrderNo(request.getOrderNo());
|
||||||
|
BigDecimal systemWeight = request.getWeight();
|
||||||
|
BigDecimal calcWeight = resolveCalcWeight(existed, systemWeight);
|
||||||
|
request.setWeight(calcWeight);
|
||||||
TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(request);
|
TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(request);
|
||||||
persistBill(request, weight, bizDate, result);
|
persistBill(request, systemWeight, bizDate, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (RuntimeException e)
|
catch (RuntimeException e)
|
||||||
@@ -1097,6 +1109,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
|||||||
bill.setProductName(result.getProductName());
|
bill.setProductName(result.getProductName());
|
||||||
bill.setCountryCode(result.getCountryCode());
|
bill.setCountryCode(result.getCountryCode());
|
||||||
bill.setCountryName(result.getCountryName());
|
bill.setCountryName(result.getCountryName());
|
||||||
|
// weight = system/original; charge_weight = effective (may be manual override)
|
||||||
bill.setWeight(weight);
|
bill.setWeight(weight);
|
||||||
bill.setBizDate(bizDate);
|
bill.setBizDate(bizDate);
|
||||||
bill.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId()));
|
bill.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId()));
|
||||||
@@ -1115,7 +1128,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
|||||||
bill.setWeightMax(result.getWeightMax());
|
bill.setWeightMax(result.getWeightMax());
|
||||||
bill.setWeightLabel(result.getWeightLabel());
|
bill.setWeightLabel(result.getWeightLabel());
|
||||||
bill.setEffectiveDate(result.getEffectiveDate());
|
bill.setEffectiveDate(result.getEffectiveDate());
|
||||||
bill.setChargeWeight(result.getChargeWeight());
|
bill.setChargeWeight(resolvePersistChargeWeight(weight, result.getChargeWeight()));
|
||||||
bill.setFreightRate(result.getFreightRate());
|
bill.setFreightRate(result.getFreightRate());
|
||||||
bill.setBaseFreightRate(result.getBaseFreightRate());
|
bill.setBaseFreightRate(result.getBaseFreightRate());
|
||||||
bill.setDiscountPercent(result.getDiscountPercent());
|
bill.setDiscountPercent(result.getDiscountPercent());
|
||||||
@@ -1754,6 +1767,349 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public TmsPriceQuoteResult updateChargeWeight(String billId, BigDecimal chargeWeight)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(billId))
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.bill.billIdRequired"));
|
||||||
|
}
|
||||||
|
// data-scope check via list query
|
||||||
|
self.getBill(billId.trim());
|
||||||
|
return self.updateChargeWeightByBillId(billId.trim(), chargeWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public TmsPriceQuoteResult updateChargeWeightByBillId(String billId, BigDecimal chargeWeight)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(billId))
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.bill.billIdRequired"));
|
||||||
|
}
|
||||||
|
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId.trim());
|
||||||
|
if (bill == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
|
||||||
|
}
|
||||||
|
assertOrderBillEditable(bill);
|
||||||
|
if (BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("订单已退费,不可修改计费重");
|
||||||
|
}
|
||||||
|
if (chargeWeight == null || chargeWeight.compareTo(BigDecimal.ZERO) <= 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("人工修改计费重必须大于 0");
|
||||||
|
}
|
||||||
|
BigDecimal cw = chargeWeight.setScale(3, RoundingMode.HALF_UP);
|
||||||
|
TmsPriceBillRequest req = buildBillRequestFromBill(bill);
|
||||||
|
req.setWeight(cw);
|
||||||
|
Date bizDate = bill.getBizDate();
|
||||||
|
if (bizDate == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired"));
|
||||||
|
}
|
||||||
|
TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(req);
|
||||||
|
// keep original system weight; charge_weight becomes the manual override
|
||||||
|
persistBill(req, bill.getWeight(), bizDate, result);
|
||||||
|
return toResult(bill.getBillId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String submitImportChargeWeight(List<ChargeWeightImportRow> rows)
|
||||||
|
{
|
||||||
|
if (rows == null || rows.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException("导入数据不能为空");
|
||||||
|
}
|
||||||
|
java.util.Map<String, ChargeWeightImportRow> dedup = new java.util.LinkedHashMap<String, ChargeWeightImportRow>();
|
||||||
|
int lineNo = 1;
|
||||||
|
int initialFail = 0;
|
||||||
|
StringBuilder preFailTips = new StringBuilder();
|
||||||
|
for (ChargeWeightImportRow row : rows)
|
||||||
|
{
|
||||||
|
lineNo++;
|
||||||
|
if (row == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String orderNo = row.getOrderNo() == null ? "" : row.getOrderNo().trim();
|
||||||
|
if (orderNo.isEmpty())
|
||||||
|
{
|
||||||
|
initialFail++;
|
||||||
|
if (preFailTips.length() < 800)
|
||||||
|
{
|
||||||
|
if (preFailTips.length() > 0)
|
||||||
|
{
|
||||||
|
preFailTips.append("; ");
|
||||||
|
}
|
||||||
|
preFailTips.append("第").append(lineNo).append("行:订单号为空");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dedup.put(orderNo, row);
|
||||||
|
}
|
||||||
|
List<ChargeWeightImportWorkItem> items = new ArrayList<ChargeWeightImportWorkItem>();
|
||||||
|
for (java.util.Map.Entry<String, ChargeWeightImportRow> entry : dedup.entrySet())
|
||||||
|
{
|
||||||
|
String orderNo = entry.getKey();
|
||||||
|
ChargeWeightImportRow row = entry.getValue();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (row.getChargeWeight() == null || row.getChargeWeight().compareTo(BigDecimal.ZERO) <= 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("人工修改计费重必须大于 0");
|
||||||
|
}
|
||||||
|
String customer = row.getCustomer() == null ? "" : row.getCustomer().trim();
|
||||||
|
if (customer.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户不能为空");
|
||||||
|
}
|
||||||
|
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
|
||||||
|
if (bill == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("订单不存在");
|
||||||
|
}
|
||||||
|
// data-scope in request thread
|
||||||
|
self.getBill(bill.getBillId());
|
||||||
|
if (!customerMatches(bill, customer))
|
||||||
|
{
|
||||||
|
throw new ServiceException("客户与订单不匹配");
|
||||||
|
}
|
||||||
|
if (BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("订单已退费");
|
||||||
|
}
|
||||||
|
items.add(new ChargeWeightImportWorkItem(bill.getBillId(), orderNo,
|
||||||
|
row.getChargeWeight().setScale(3, RoundingMode.HALF_UP)));
|
||||||
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
|
{
|
||||||
|
initialFail++;
|
||||||
|
if (preFailTips.length() < 800)
|
||||||
|
{
|
||||||
|
String tip = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
|
||||||
|
if (preFailTips.length() > 0)
|
||||||
|
{
|
||||||
|
preFailTips.append("; ");
|
||||||
|
}
|
||||||
|
preFailTips.append(orderNo).append(":").append(tip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (items.isEmpty() && initialFail == 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("导入数据不能为空");
|
||||||
|
}
|
||||||
|
if (items.size() > CHARGE_WEIGHT_IMPORT_MAX)
|
||||||
|
{
|
||||||
|
throw new ServiceException("导入行数超限,最多 "
|
||||||
|
+ CHARGE_WEIGHT_IMPORT_MAX + " 条");
|
||||||
|
}
|
||||||
|
String taskId = IdUtils.fastSimpleUUID();
|
||||||
|
int total = items.size() + initialFail;
|
||||||
|
chargeWeightImportAsyncRunner.put(taskId, ChargeWeightImportTask.STATUS_PENDING, 0, "queued",
|
||||||
|
preFailTips.length() > 0 ? preFailTips.toString() : null, total, 0, initialFail);
|
||||||
|
chargeWeightImportAsyncRunner.run(taskId, items, initialFail);
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChargeWeightImportTask getChargeWeightImportTask(String taskId)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(taskId) || chargeWeightImportAsyncRunner == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return chargeWeightImportAsyncRunner.get(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ChargeWeightImportRow> buildChargeWeightTemplateRows(ChargeWeightTemplateRequest request)
|
||||||
|
{
|
||||||
|
if (request == null || !request.isFillByFilter())
|
||||||
|
{
|
||||||
|
return new ArrayList<ChargeWeightImportRow>();
|
||||||
|
}
|
||||||
|
List<String> orderNos = normalizeCodeList(request.getOrderNos());
|
||||||
|
List<String> trackingNos = normalizeCodeList(request.getTrackingNos());
|
||||||
|
List<String> transferNos = normalizeCodeList(request.getTransferNos());
|
||||||
|
List<String> customerCodes = normalizeCodeList(request.getCustomerCodes());
|
||||||
|
List<String> productCodes = normalizeCodeList(request.getProductCodes());
|
||||||
|
String creditArrearsStatus = StringUtils.isEmpty(request.getCreditArrearsStatus()) ? null
|
||||||
|
: request.getCreditArrearsStatus().trim();
|
||||||
|
String beginBizDate = StringUtils.isEmpty(request.getBeginBizDate()) ? null
|
||||||
|
: request.getBeginBizDate().trim();
|
||||||
|
String endBizDate = StringUtils.isEmpty(request.getEndBizDate()) ? null
|
||||||
|
: request.getEndBizDate().trim();
|
||||||
|
boolean hasFilter = (orderNos != null && !orderNos.isEmpty())
|
||||||
|
|| (trackingNos != null && !trackingNos.isEmpty())
|
||||||
|
|| (transferNos != null && !transferNos.isEmpty())
|
||||||
|
|| (customerCodes != null && !customerCodes.isEmpty())
|
||||||
|
|| (productCodes != null && !productCodes.isEmpty())
|
||||||
|
|| StringUtils.isNotEmpty(creditArrearsStatus)
|
||||||
|
|| StringUtils.isNotEmpty(beginBizDate)
|
||||||
|
|| StringUtils.isNotEmpty(endBizDate);
|
||||||
|
if (!hasFilter)
|
||||||
|
{
|
||||||
|
throw new ServiceException("\u8bf7\u5148\u8bbe\u7f6e\u5217\u8868\u7b5b\u9009\u6761\u4ef6");
|
||||||
|
}
|
||||||
|
PrcOrderBill query = new PrcOrderBill();
|
||||||
|
query.setOrderNos(orderNos);
|
||||||
|
query.setTrackingNos(trackingNos);
|
||||||
|
query.setTransferNos(transferNos);
|
||||||
|
query.setCustomerCodes(customerCodes);
|
||||||
|
query.setProductCodes(productCodes);
|
||||||
|
query.setCreditArrearsStatus(creditArrearsStatus);
|
||||||
|
if (beginBizDate != null)
|
||||||
|
{
|
||||||
|
query.getParams().put("beginBizDate", beginBizDate);
|
||||||
|
}
|
||||||
|
if (endBizDate != null)
|
||||||
|
{
|
||||||
|
query.getParams().put("endBizDate", endBizDate);
|
||||||
|
}
|
||||||
|
List<PrcOrderBill> list = self.selectBillList(query);
|
||||||
|
if (list == null || list.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException("\u5f53\u524d\u7b5b\u9009\u6761\u4ef6\u4e0b\u6ca1\u6709\u53ef\u5bfc\u51fa\u7684\u8ba2\u5355");
|
||||||
|
}
|
||||||
|
if (list.size() > CHARGE_WEIGHT_IMPORT_MAX)
|
||||||
|
{
|
||||||
|
throw new ServiceException("\u7b5b\u9009\u7ed3\u679c\u8d85\u8fc7 "
|
||||||
|
+ CHARGE_WEIGHT_IMPORT_MAX + " \u6761\uff0c\u8bf7\u7f29\u5c0f\u7b5b\u9009\u8303\u56f4");
|
||||||
|
}
|
||||||
|
List<ChargeWeightImportRow> rows = new ArrayList<ChargeWeightImportRow>(list.size());
|
||||||
|
for (PrcOrderBill bill : list)
|
||||||
|
{
|
||||||
|
if (bill == null || StringUtils.isEmpty(bill.getOrderNo()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (BillRefundStatus.REFUNDED.getCode().equalsIgnoreCase(bill.getRefundStatus()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ChargeWeightImportRow row = new ChargeWeightImportRow();
|
||||||
|
row.setOrderNo(bill.getOrderNo());
|
||||||
|
String customer = StringUtils.isNotEmpty(bill.getCustomerCode()) ? bill.getCustomerCode()
|
||||||
|
: bill.getCustomerName();
|
||||||
|
row.setCustomer(customer);
|
||||||
|
row.setChargeWeight(null);
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
if (rows.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException("\u5f53\u524d\u7b5b\u9009\u6761\u4ef6\u4e0b\u6ca1\u6709\u53ef\u5bfc\u51fa\u7684\u8ba2\u5355");
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TmsPriceQuoteResult toResult(String billId)
|
||||||
|
{
|
||||||
|
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId);
|
||||||
|
if (bill == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.bill.notFound"));
|
||||||
|
}
|
||||||
|
return toResult(bill, incomeBillFeeMapper.selectByBillId(bill.getBillId()),
|
||||||
|
payBillFeeMapper.selectByBillId(bill.getBillId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** charge_weight differs from weight => treated as manual override. */
|
||||||
|
private static boolean isManualChargeWeight(PrcOrderBill bill)
|
||||||
|
{
|
||||||
|
if (bill == null || bill.getChargeWeight() == null || bill.getWeight() == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return bill.getChargeWeight().compareTo(bill.getWeight()) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prefer manual charge_weight when present; otherwise system weight. */
|
||||||
|
private static BigDecimal resolveCalcWeight(PrcOrderBill existed, BigDecimal systemWeight)
|
||||||
|
{
|
||||||
|
if (isManualChargeWeight(existed))
|
||||||
|
{
|
||||||
|
return existed.getChargeWeight();
|
||||||
|
}
|
||||||
|
return systemWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist charge_weight: keep manual override when calc weight != system weight,
|
||||||
|
* or when existing bill already had a manual override.
|
||||||
|
*/
|
||||||
|
private static BigDecimal resolvePersistChargeWeight(BigDecimal systemWeight, BigDecimal resultChargeWeight)
|
||||||
|
{
|
||||||
|
// calc weight != system weight => keep result as manual charge_weight
|
||||||
|
if (systemWeight != null && resultChargeWeight != null
|
||||||
|
&& systemWeight.compareTo(resultChargeWeight) != 0)
|
||||||
|
{
|
||||||
|
return resultChargeWeight;
|
||||||
|
}
|
||||||
|
return resultChargeWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean customerMatches(PrcOrderBill bill, String customer)
|
||||||
|
{
|
||||||
|
if (bill == null || StringUtils.isEmpty(customer))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String code = bill.getCustomerCode() == null ? "" : bill.getCustomerCode().trim();
|
||||||
|
String name = bill.getCustomerName() == null ? "" : bill.getCustomerName().trim();
|
||||||
|
return customer.equalsIgnoreCase(code) || customer.equals(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TmsPriceBillRequest buildBillRequestFromBill(PrcOrderBill bill)
|
||||||
|
{
|
||||||
|
TmsPriceBillRequest req = new TmsPriceBillRequest();
|
||||||
|
req.setOrderNo(bill.getOrderNo());
|
||||||
|
req.setTrackingNo(bill.getTrackingNo());
|
||||||
|
req.setTransferNo(bill.getTransferNo());
|
||||||
|
req.setBelongSaler(bill.getBelongSaler());
|
||||||
|
req.setBelongSalerCode(bill.getBelongSalerCode());
|
||||||
|
req.setSettlementMan(bill.getSettlementMan());
|
||||||
|
req.setSettlementManCode(bill.getSettlementManCode());
|
||||||
|
req.setServiceMan(bill.getServiceMan());
|
||||||
|
req.setServiceManCode(bill.getServiceManCode());
|
||||||
|
req.setCustomerId(bill.getCustomerId());
|
||||||
|
req.setCustomerCode(bill.getCustomerCode());
|
||||||
|
req.setCustomerName(bill.getCustomerName());
|
||||||
|
req.setProductCode(bill.getProductCode());
|
||||||
|
req.setCountryCode(bill.getCountryCode());
|
||||||
|
req.setCountryName(bill.getCountryName());
|
||||||
|
req.setDestPostcode(bill.getDestPostcode());
|
||||||
|
req.setPieces(bill.getPieces());
|
||||||
|
req.setActualWeight(bill.getActualWeight());
|
||||||
|
req.setReceiveWeight(bill.getReceiveWeight());
|
||||||
|
req.setVolume(bill.getVolume());
|
||||||
|
req.setVolumeWeight(bill.getVolumeWeight());
|
||||||
|
req.setPkgLength(bill.getPkgLength());
|
||||||
|
req.setPkgWidth(bill.getPkgWidth());
|
||||||
|
req.setPkgHeight(bill.getPkgHeight());
|
||||||
|
req.setShipperName(bill.getShipperName());
|
||||||
|
req.setConsigneeName(bill.getConsigneeName());
|
||||||
|
req.setWarehouseCode(bill.getWarehouseCode());
|
||||||
|
req.setOrderTime(bill.getOrderTime());
|
||||||
|
req.setOrderRemark(bill.getOrderRemark());
|
||||||
|
if (bill.getTmsId() != null)
|
||||||
|
{
|
||||||
|
req.setTmsId(Integer.valueOf(bill.getTmsId().intValue()));
|
||||||
|
}
|
||||||
|
if (bill.getBizDate() != null)
|
||||||
|
{
|
||||||
|
req.setBizDate(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", bill.getBizDate()));
|
||||||
|
}
|
||||||
|
BigDecimal w = bill.getChargeWeight() != null ? bill.getChargeWeight() : bill.getWeight();
|
||||||
|
req.setWeight(w);
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private Date resolveBizDate(String bizDate)
|
private Date resolveBizDate(String bizDate)
|
||||||
{
|
{
|
||||||
if (StringUtils.isEmpty(bizDate))
|
if (StringUtils.isEmpty(bizDate))
|
||||||
|
|||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
package com.bms.price.task;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import com.bms.common.core.redis.RedisCache;
|
||||||
|
import com.bms.common.utils.StringUtils;
|
||||||
|
import com.bms.price.domain.ChargeWeightImportTask;
|
||||||
|
import com.bms.price.service.IPrcOrderBillService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async runner: import charge_weight updates and rebill each order.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class PrcChargeWeightImportAsyncRunner
|
||||||
|
{
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PrcChargeWeightImportAsyncRunner.class);
|
||||||
|
|
||||||
|
public static final String CACHE_KEY_PREFIX = "price:billing:chargeWeight:import:task:";
|
||||||
|
public static final int TTL_MINUTES = 60;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Lazy
|
||||||
|
private IPrcOrderBillService orderBillService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisCache redisCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param items pre-validated rows with billId filled in remark field unused;
|
||||||
|
* use orderNo + chargeWeight; billId stored in customer field as billId carrier
|
||||||
|
* -- better use a dedicated work item. See run(List of Object[]).
|
||||||
|
*/
|
||||||
|
@Async("threadPoolTaskExecutor")
|
||||||
|
public void run(String taskId, List<ChargeWeightImportWorkItem> items, int initialFail)
|
||||||
|
{
|
||||||
|
int total = (items == null ? 0 : items.size()) + Math.max(initialFail, 0);
|
||||||
|
int success = 0;
|
||||||
|
int fail = Math.max(initialFail, 0);
|
||||||
|
StringBuilder failTips = new StringBuilder();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
put(taskId, ChargeWeightImportTask.STATUS_RUNNING, 1, "running", null, total, 0, fail);
|
||||||
|
int workSize = items == null ? 0 : items.size();
|
||||||
|
if (workSize == 0)
|
||||||
|
{
|
||||||
|
if (fail > 0)
|
||||||
|
{
|
||||||
|
put(taskId, ChargeWeightImportTask.STATUS_SUCCESS, 100,
|
||||||
|
"success=0, fail=" + fail, "all rows failed validation", total, 0, fail);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
put(taskId, ChargeWeightImportTask.STATUS_FAIL, 100, "failed", "no rows", 0, 0, 0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < workSize; i++)
|
||||||
|
{
|
||||||
|
ChargeWeightImportWorkItem item = items.get(i);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
orderBillService.updateChargeWeightByBillId(item.getBillId(), item.getChargeWeight());
|
||||||
|
success++;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
fail++;
|
||||||
|
if (failTips.length() < 800)
|
||||||
|
{
|
||||||
|
String tip = e.getMessage();
|
||||||
|
if (StringUtils.isEmpty(tip))
|
||||||
|
{
|
||||||
|
tip = e.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
if (failTips.length() > 0)
|
||||||
|
{
|
||||||
|
failTips.append("; ");
|
||||||
|
}
|
||||||
|
failTips.append(item.getOrderNo()).append(":").append(tip);
|
||||||
|
}
|
||||||
|
log.warn("charge weight import failed taskId={} orderNo={}", taskId, item.getOrderNo(), e);
|
||||||
|
}
|
||||||
|
int done = i + 1 + Math.max(initialFail, 0);
|
||||||
|
int percent = Math.min(99, (int) Math.floor(100.0 * done / Math.max(total, 1)));
|
||||||
|
put(taskId, ChargeWeightImportTask.STATUS_RUNNING, percent,
|
||||||
|
"processing " + done + "/" + total, null, total, success, fail);
|
||||||
|
}
|
||||||
|
String msg = "success=" + success + ", fail=" + fail;
|
||||||
|
if (fail > 0 && failTips.length() > 0)
|
||||||
|
{
|
||||||
|
put(taskId, ChargeWeightImportTask.STATUS_SUCCESS, 100, msg, failTips.toString(),
|
||||||
|
total, success, fail);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
put(taskId, ChargeWeightImportTask.STATUS_SUCCESS, 100, msg, null, total, success, fail);
|
||||||
|
}
|
||||||
|
log.info("charge weight import done taskId={} total={} success={} fail={}",
|
||||||
|
taskId, total, success, fail);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.error("charge weight import aborted taskId={}", taskId, e);
|
||||||
|
put(taskId, ChargeWeightImportTask.STATUS_FAIL, 100, "failed", errorMessage(e),
|
||||||
|
total, success, fail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void put(String taskId, String status, int percent, String message, String error,
|
||||||
|
int total, int success, int fail)
|
||||||
|
{
|
||||||
|
ChargeWeightImportTask task = new ChargeWeightImportTask();
|
||||||
|
task.setTaskId(taskId);
|
||||||
|
task.setStatus(status);
|
||||||
|
task.setPercent(percent);
|
||||||
|
task.setMessage(message);
|
||||||
|
task.setError(error);
|
||||||
|
task.setTotal(total);
|
||||||
|
task.setSuccess(success);
|
||||||
|
task.setFail(fail);
|
||||||
|
redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChargeWeightImportTask get(String taskId)
|
||||||
|
{
|
||||||
|
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String errorMessage(Exception e)
|
||||||
|
{
|
||||||
|
String msg = e.getMessage();
|
||||||
|
if (StringUtils.isEmpty(msg))
|
||||||
|
{
|
||||||
|
msg = e.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pre-validated work item for async import. */
|
||||||
|
public static class ChargeWeightImportWorkItem implements java.io.Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private String billId;
|
||||||
|
private String orderNo;
|
||||||
|
private BigDecimal chargeWeight;
|
||||||
|
|
||||||
|
public ChargeWeightImportWorkItem()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChargeWeightImportWorkItem(String billId, String orderNo, BigDecimal chargeWeight)
|
||||||
|
{
|
||||||
|
this.billId = billId;
|
||||||
|
this.orderNo = orderNo;
|
||||||
|
this.chargeWeight = chargeWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBillId()
|
||||||
|
{
|
||||||
|
return billId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBillId(String billId)
|
||||||
|
{
|
||||||
|
this.billId = billId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrderNo()
|
||||||
|
{
|
||||||
|
return orderNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrderNo(String orderNo)
|
||||||
|
{
|
||||||
|
this.orderNo = orderNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getChargeWeight()
|
||||||
|
{
|
||||||
|
return chargeWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChargeWeight(BigDecimal chargeWeight)
|
||||||
|
{
|
||||||
|
this.chargeWeight = chargeWeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- billing list: edit/import charge weight (reuse charge_weight; keep weight)
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||||
|
SELECT 2331, N'修改计费重', m.menu_id, 21, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:chargeWeightEdit', '#', 'admin', sysdate(), N'计费列表单票/导入修改计费重并重新计费'
|
||||||
|
FROM (
|
||||||
|
SELECT menu_id FROM sys_menu
|
||||||
|
WHERE perms = 'price:billing:bill' AND menu_type = 'C'
|
||||||
|
UNION ALL
|
||||||
|
SELECT menu_id FROM sys_menu
|
||||||
|
WHERE component = 'price/billing/bill' AND menu_type = 'C'
|
||||||
|
) m
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM sys_menu x WHERE x.menu_id = 2331 OR x.perms = 'price:billing:chargeWeightEdit')
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
|
SELECT 1, 2331
|
||||||
|
FROM DUAL
|
||||||
|
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2331)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = 2331);
|
||||||
Binary file not shown.
@@ -1,4 +1,8 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import { blobValidate } from '@/utils/bms'
|
||||||
|
import { ElLoading } from 'element-plus'
|
||||||
|
import errorCode from '@/utils/errorCode'
|
||||||
|
|
||||||
/** Quote trial for billing center */
|
/** Quote trial for billing center */
|
||||||
export function quoteBilling(data) {
|
export function quoteBilling(data) {
|
||||||
@@ -187,3 +191,89 @@ export function updateCreditArrears(billIds) {
|
|||||||
data: billIds
|
data: billIds
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 计费列表:单票修改计费重(写 charge_weight,保留 weight)并重新计费 */
|
||||||
|
export function updateChargeWeight(data) {
|
||||||
|
return request({
|
||||||
|
url: '/price/billing/bill/chargeWeight',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChargeWeightImportTask(taskId) {
|
||||||
|
return request({
|
||||||
|
url: '/price/billing/bill/chargeWeight/import/task/' + taskId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickChargeWeightImportTaskId(res) {
|
||||||
|
const d = res && res.data
|
||||||
|
if (!d) return ''
|
||||||
|
if (typeof d === 'string' || typeof d === 'number') return String(d)
|
||||||
|
return String(d.taskId || d.id || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollChargeWeightImportTask(taskId, onProgress) {
|
||||||
|
const started = Date.now()
|
||||||
|
const maxMs = 30 * 60 * 1000
|
||||||
|
while (Date.now() - started < maxMs) {
|
||||||
|
const res = await getChargeWeightImportTask(taskId)
|
||||||
|
const task = res.data || {}
|
||||||
|
if (typeof onProgress === 'function') onProgress(task)
|
||||||
|
if (task.status === 'SUCCESS') return { task }
|
||||||
|
if (task.status === 'FAIL') {
|
||||||
|
const err = new Error(task.error || task.message || 'charge weight import failed')
|
||||||
|
err.task = task
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800))
|
||||||
|
}
|
||||||
|
throw new Error('charge weight import timeout')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async import charge weight + rebill; poll until done.
|
||||||
|
* @param {File|Blob} file
|
||||||
|
* @param {(task: object) => void} [onProgress]
|
||||||
|
*/
|
||||||
|
export async function importChargeWeightAsync(file, onProgress) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const submitRes = await request({
|
||||||
|
url: '/price/billing/bill/chargeWeight/importData',
|
||||||
|
method: 'post',
|
||||||
|
data: formData,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
|
||||||
|
// Backend parses Excel + validates rows before returning taskId
|
||||||
|
timeout: 10 * 60 * 1000
|
||||||
|
})
|
||||||
|
const taskId = pickChargeWeightImportTaskId(submitRes)
|
||||||
|
if (!taskId) throw new Error('missing charge weight import taskId')
|
||||||
|
return pollChargeWeightImportTask(taskId, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Download charge-weight import template (optionally fill by list filters). */
|
||||||
|
export async function downloadChargeWeightTemplate(data, filename) {
|
||||||
|
const loading = ElLoading.service({ text: '正在下载模板,请稍候', background: 'rgba(0, 0, 0, 0.7)' })
|
||||||
|
try {
|
||||||
|
const blobData = await request({
|
||||||
|
url: '/price/billing/bill/chargeWeight/importTemplate',
|
||||||
|
method: 'post',
|
||||||
|
data: data || {},
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: 120000
|
||||||
|
})
|
||||||
|
if (blobValidate(blobData)) {
|
||||||
|
saveAs(new Blob([blobData]), filename || ('批量修改计费重导入模板_' + Date.now() + '.xlsx'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const resText = await blobData.text()
|
||||||
|
const rspObj = JSON.parse(resText)
|
||||||
|
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] || '下载模板失败'
|
||||||
|
throw new Error(errMsg)
|
||||||
|
} finally {
|
||||||
|
loading.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -186,6 +186,11 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
// Custom template download: () => void | Promise; overrides templateUrl / templateAction
|
||||||
|
templateDownloadHandler: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
// 确定按钮文字
|
// 确定按钮文字
|
||||||
submitText: {
|
submitText: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -217,7 +222,7 @@ const uploadUrl = computed(() => {
|
|||||||
return import.meta.env.VITE_APP_BASE_API + props.action + '?' + query.toString()
|
return import.meta.env.VITE_APP_BASE_API + props.action + '?' + query.toString()
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasTemplate = computed(() => !!(props.templateUrl || props.templateAction))
|
const hasTemplate = computed(() => !!(props.templateDownloadHandler || props.templateUrl || props.templateAction))
|
||||||
|
|
||||||
// 打开对话框(供父组件通过 ref 调用)
|
// 打开对话框(供父组件通过 ref 调用)
|
||||||
function open() {
|
function open() {
|
||||||
@@ -231,6 +236,10 @@ function open() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function handleClosed() {
|
function handleClosed() {
|
||||||
isUploading.value = false
|
isUploading.value = false
|
||||||
progressPercent.value = 0
|
progressPercent.value = 0
|
||||||
@@ -238,8 +247,17 @@ function handleClosed() {
|
|||||||
selectedFile.value = null
|
selectedFile.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载模板:静态文件直接 <a download>,接口走 proxy.download
|
// 下载模板:自定义 handler > 静态文件 > 接口 proxy.download
|
||||||
function handleDownloadTemplate() {
|
async function handleDownloadTemplate() {
|
||||||
|
if (typeof props.templateDownloadHandler === 'function') {
|
||||||
|
try {
|
||||||
|
await props.templateDownloadHandler()
|
||||||
|
} catch (e) {
|
||||||
|
const tip = (e && e.message) ? e.message : '下载模板失败'
|
||||||
|
proxy.$modal.msgError(tip)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (props.templateUrl) {
|
if (props.templateUrl) {
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = props.templateUrl
|
link.href = props.templateUrl
|
||||||
@@ -353,7 +371,7 @@ async function handleSubmit() {
|
|||||||
uploadRef.value?.submit?.()
|
uploadRef.value?.submit?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ open })
|
defineExpose({ open, close })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -83,9 +83,26 @@
|
|||||||
@click="openBatchFee"
|
@click="openBatchFee"
|
||||||
>批量增加费用</el-button>
|
>批量增加费用</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="Upload"
|
||||||
|
v-hasPermi="['price:billing:chargeWeightEdit']"
|
||||||
|
@click="openChargeWeightImport"
|
||||||
|
>批量修改计费重</el-button>
|
||||||
|
</el-col>
|
||||||
</template>
|
</template>
|
||||||
<template #col-action="{ row }">
|
<template #col-action="{ row }">
|
||||||
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
|
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
icon="Edit"
|
||||||
|
:disabled="row.refundStatus === 'REFUNDED'"
|
||||||
|
v-hasPermi="['price:billing:chargeWeightEdit']"
|
||||||
|
@click="openEditChargeWeight(row)"
|
||||||
|
>改计费重</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -106,15 +123,61 @@
|
|||||||
:selected-ids="batchFeeSelectedIds"
|
:selected-ids="batchFeeSelectedIds"
|
||||||
@success="onBatchFeeSuccess"
|
@success="onBatchFeeSuccess"
|
||||||
/>
|
/>
|
||||||
|
<edit-charge-weight-dialog
|
||||||
|
v-model="editCwOpen"
|
||||||
|
:bill-id="editCwRow?.billId || ''"
|
||||||
|
:order-no="editCwRow?.orderNo || ''"
|
||||||
|
:origin-weight="editCwRow?.weight"
|
||||||
|
:current-charge-weight="editCwRow?.chargeWeight"
|
||||||
|
@success="onChargeWeightUpdated"
|
||||||
|
/>
|
||||||
|
<excel-import-dialog
|
||||||
|
ref="chargeWeightImportRef"
|
||||||
|
title="批量修改计费重"
|
||||||
|
width="520px"
|
||||||
|
action="/price/billing/bill/chargeWeight/importData"
|
||||||
|
template-file-name="批量修改计费重导入模板"
|
||||||
|
:show-update-support="false"
|
||||||
|
:import-desc="chargeWeightImportDesc"
|
||||||
|
:template-download-handler="downloadChargeWeightImportTemplate"
|
||||||
|
:submit-handler="runChargeWeightImportAsync"
|
||||||
|
@success="onChargeWeightImportSuccess"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<el-checkbox v-model="cwFillByFilter">按当前筛选条件带出订单号和客户</el-checkbox>
|
||||||
|
<div class="cw-fill-hint">勾选后下载模板会写入筛选结果的订单号、客户代码;人工修改计费重列留空待填</div>
|
||||||
|
</template>
|
||||||
|
</excel-import-dialog>
|
||||||
|
<el-dialog
|
||||||
|
v-model="cwImportProgressVisible"
|
||||||
|
title="正在导入修改计费重"
|
||||||
|
width="420px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
:show-close="false"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<div class="import-progress">
|
||||||
|
<el-progress
|
||||||
|
:percentage="cwImportProgressPercent"
|
||||||
|
:status="cwImportProgressStatus"
|
||||||
|
:indeterminate="cwImportProgressIndeterminate"
|
||||||
|
:stroke-width="10"
|
||||||
|
/>
|
||||||
|
<div class="import-progress-msg">{{ cwImportProgressMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup name="BillingBill">
|
<script setup name="BillingBill">
|
||||||
import { listBill, getBill, refundBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, updateCreditArrears } from '@/api/price/billing'
|
import { listBill, getBill, refundBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate } from '@/api/price/billing'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useProducts } from '@/components/ProductSelect/useProducts'
|
import { useProducts } from '@/components/ProductSelect/useProducts'
|
||||||
import BillDetail from './components/BillDetail.vue'
|
import BillDetail from './components/BillDetail.vue'
|
||||||
import BatchAddIncomeFeeDialog from './components/BatchAddIncomeFeeDialog.vue'
|
import BatchAddIncomeFeeDialog from './components/BatchAddIncomeFeeDialog.vue'
|
||||||
|
import EditChargeWeightDialog from './components/EditChargeWeightDialog.vue'
|
||||||
|
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||||
|
|
||||||
// 订单号下拉远程搜索:返回匹配的订单号列表(最多 20 条)
|
// 订单号下拉远程搜索:返回匹配的订单号列表(最多 20 条)
|
||||||
function searchOrderNoOptions(keyword) {
|
function searchOrderNoOptions(keyword) {
|
||||||
@@ -149,6 +212,21 @@ const batchFeeOpen = ref(false)
|
|||||||
const batchFeeListQuery = ref({})
|
const batchFeeListQuery = ref({})
|
||||||
const batchFeeSelectedIds = ref([])
|
const batchFeeSelectedIds = ref([])
|
||||||
const refundingKey = ref(null)
|
const refundingKey = ref(null)
|
||||||
|
const editCwOpen = ref(false)
|
||||||
|
const editCwRow = ref(null)
|
||||||
|
const chargeWeightImportRef = ref()
|
||||||
|
const chargeWeightImportDesc = [
|
||||||
|
'模板列:订单号、客户、人工修改计费重(kg)',
|
||||||
|
'客户可填名称或代码,须与订单客户一致',
|
||||||
|
'导入后异步写入计费重并重新计费;原计费重(weight)不覆盖',
|
||||||
|
'下载模板可选:按当前列表筛选条件带出订单号和客户'
|
||||||
|
]
|
||||||
|
const cwFillByFilter = ref(false)
|
||||||
|
const cwImportProgressVisible = ref(false)
|
||||||
|
const cwImportProgressPercent = ref(0)
|
||||||
|
const cwImportProgressMessage = ref('')
|
||||||
|
const cwImportProgressStatus = ref(undefined)
|
||||||
|
const cwImportProgressIndeterminate = ref(false)
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ label: '订单号', prop: 'orderNo', minWidth: 140, showOverflowTooltip: true },
|
{ label: '订单号', prop: 'orderNo', minWidth: 140, showOverflowTooltip: true },
|
||||||
@@ -160,6 +238,7 @@ const columns = [
|
|||||||
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
|
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
|
||||||
{ label: '件数', prop: 'pieces', align: 'center', width: 70 },
|
{ label: '件数', prop: 'pieces', align: 'center', width: 70 },
|
||||||
{ label: '计费重', prop: 'chargeWeight', align: 'center', width: 90, slot: 'chargeWeight' },
|
{ label: '计费重', prop: 'chargeWeight', align: 'center', width: 90, slot: 'chargeWeight' },
|
||||||
|
{ label: '原计费重', prop: 'weight', align: 'center', width: 100, slot: 'chargeWeight' },
|
||||||
{ label: '收货重', prop: 'receiveWeight', align: 'center', width: 90, slot: 'chargeWeight' },
|
{ label: '收货重', prop: 'receiveWeight', align: 'center', width: 90, slot: 'chargeWeight' },
|
||||||
{ label: '应收总额', prop: 'incomeTotal', align: 'right', width: 120, slot: 'incomeTotal' },
|
{ label: '应收总额', prop: 'incomeTotal', align: 'right', width: 120, slot: 'incomeTotal' },
|
||||||
{ label: '应付总额', prop: 'payTotal', align: 'right', width: 120, slot: 'payTotal' },
|
{ label: '应付总额', prop: 'payTotal', align: 'right', width: 120, slot: 'payTotal' },
|
||||||
@@ -172,7 +251,7 @@ const columns = [
|
|||||||
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
|
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
|
||||||
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
|
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
|
||||||
// { label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
|
// { label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
|
||||||
{ label: '操作', prop: 'action', align: 'center', width: 160, fixed: 'right', slot: 'action', showInToolbar: false }
|
{ label: '操作', prop: 'action', align: 'center', width: 240, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||||
]
|
]
|
||||||
|
|
||||||
const arrearsStatusOptions = [
|
const arrearsStatusOptions = [
|
||||||
@@ -316,6 +395,120 @@ function onBatchFeeSuccess() {
|
|||||||
tableRef.value?.refresh?.()
|
tableRef.value?.refresh?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEditChargeWeight(row) {
|
||||||
|
if (!row || !row.billId) return
|
||||||
|
if (row.refundStatus === 'REFUNDED') {
|
||||||
|
ElMessage.warning('该订单已退费')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
editCwRow.value = row
|
||||||
|
editCwOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChargeWeightUpdated() {
|
||||||
|
tableRef.value?.refresh?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChargeWeightTemplateQuery() {
|
||||||
|
const q = tableRef.value?.getQueryParams?.() || {}
|
||||||
|
const { pageNum, pageSize, bizDateRange, ...rest } = q
|
||||||
|
const payload = {
|
||||||
|
fillByFilter: !!cwFillByFilter.value,
|
||||||
|
orderNos: rest.orderNos || [],
|
||||||
|
trackingNos: rest.trackingNos || [],
|
||||||
|
transferNos: rest.transferNos || [],
|
||||||
|
customerCodes: rest.customerCodes || [],
|
||||||
|
productCodes: rest.productCodes || [],
|
||||||
|
creditArrearsStatus: rest.creditArrearsStatus || undefined
|
||||||
|
}
|
||||||
|
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
|
||||||
|
payload.beginBizDate = bizDateRange[0] || null
|
||||||
|
payload.endBizDate = bizDateRange[1] || null
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadChargeWeightImportTemplate() {
|
||||||
|
const payload = buildChargeWeightTemplateQuery()
|
||||||
|
if (payload.fillByFilter && !hasBatchFeeQuery(tableRef.value?.getQueryParams?.() || {})) {
|
||||||
|
throw new Error('请先设置列表筛选条件')
|
||||||
|
}
|
||||||
|
await downloadChargeWeightTemplate(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openChargeWeightImport() {
|
||||||
|
cwFillByFilter.value = false
|
||||||
|
chargeWeightImportRef.value?.open?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCwImportProgress(t) {
|
||||||
|
cwImportProgressIndeterminate.value = false
|
||||||
|
cwImportProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||||
|
const total = Number(t.total) || 0
|
||||||
|
const success = Number(t.success) || 0
|
||||||
|
const fail = Number(t.fail) || 0
|
||||||
|
if (t.status === 'PENDING') {
|
||||||
|
cwImportProgressMessage.value = '\u6392\u961f\u4e2d\u2026'
|
||||||
|
} else if (t.status === 'RUNNING') {
|
||||||
|
if (total > 0) {
|
||||||
|
cwImportProgressMessage.value = '\u6b63\u5728\u5904\u7406 ' + success + '/' + total +
|
||||||
|
(fail ? ('\uff08\u5931\u8d25 ' + fail + '\uff09') : '')
|
||||||
|
} else {
|
||||||
|
cwImportProgressMessage.value = t.message || '\u6b63\u5728\u5bfc\u5165\u2026'
|
||||||
|
}
|
||||||
|
} else if (t.status === 'SUCCESS') {
|
||||||
|
cwImportProgressMessage.value = '\u5b8c\u6210\uff1a\u6210\u529f ' + success + '\uff0c\u5931\u8d25 ' + fail
|
||||||
|
cwImportProgressStatus.value = fail > 0 ? 'warning' : 'success'
|
||||||
|
} else if (t.status === 'FAIL') {
|
||||||
|
cwImportProgressMessage.value = t.error || t.message || '\u5bfc\u5165\u5931\u8d25'
|
||||||
|
cwImportProgressStatus.value = 'exception'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChargeWeightImportAsync(uploadFile) {
|
||||||
|
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||||||
|
if (!raw) throw new Error('\u8bf7\u5148\u9009\u62e9\u6587\u4ef6')
|
||||||
|
// Close import dialog so progress dialog is the only visible wait UI
|
||||||
|
chargeWeightImportRef.value?.close?.()
|
||||||
|
cwImportProgressVisible.value = true
|
||||||
|
cwImportProgressPercent.value = 0
|
||||||
|
cwImportProgressIndeterminate.value = true
|
||||||
|
cwImportProgressMessage.value = '\u6b63\u5728\u89e3\u6790Excel\u5e76\u6821\u9a8c\u8ba2\u5355\uff0c\u8bf7\u7a0d\u5019\u2026'
|
||||||
|
cwImportProgressStatus.value = undefined
|
||||||
|
try {
|
||||||
|
const { task } = await importChargeWeightAsync(raw, applyCwImportProgress)
|
||||||
|
cwImportProgressIndeterminate.value = false
|
||||||
|
cwImportProgressPercent.value = 100
|
||||||
|
const success = Number(task && task.success) || 0
|
||||||
|
const fail = Number(task && task.fail) || 0
|
||||||
|
const tip = '\u5bfc\u5165\u5b8c\u6210\uff1a\u6210\u529f ' + success + '\uff0c\u5931\u8d25 ' + fail
|
||||||
|
if (fail > 0) {
|
||||||
|
cwImportProgressStatus.value = 'warning'
|
||||||
|
cwImportProgressMessage.value = tip + (task && task.error ? ('\uff1b' + task.error) : '')
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||||
|
return tip + (task && task.error ? ('\uff1b' + task.error) : '')
|
||||||
|
}
|
||||||
|
cwImportProgressStatus.value = 'success'
|
||||||
|
cwImportProgressMessage.value = tip
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1200))
|
||||||
|
return tip
|
||||||
|
} catch (e) {
|
||||||
|
cwImportProgressIndeterminate.value = false
|
||||||
|
cwImportProgressStatus.value = 'exception'
|
||||||
|
const tip = (e && e.task && e.task.error) || (e && e.message) || '\u5bfc\u5165\u5931\u8d25'
|
||||||
|
cwImportProgressMessage.value = tip
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1200))
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
cwImportProgressVisible.value = false
|
||||||
|
cwImportProgressIndeterminate.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChargeWeightImportSuccess() {
|
||||||
|
tableRef.value?.refresh?.()
|
||||||
|
}
|
||||||
|
|
||||||
function formatMoney(val) {
|
function formatMoney(val) {
|
||||||
if (val === null || val === undefined || val === '') return '-'
|
if (val === null || val === undefined || val === '') return '-'
|
||||||
const num = Number(val)
|
const num = Number(val)
|
||||||
@@ -388,6 +581,22 @@ function refundTagType(status) {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
.cw-fill-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.import-progress { padding: 8px 4px 4px; }
|
||||||
|
.import-progress-msg {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
max-height: 160px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* 搜索栏走 BasicTable search-per-row=4 的栅格布局(固定 4 项一行,多余换行),
|
/* 搜索栏走 BasicTable search-per-row=4 的栅格布局(固定 4 项一行,多余换行),
|
||||||
这里只补齐插槽渲染的控件宽度,不再覆盖 BasicForm 的布局 */
|
这里只补齐插槽渲染的控件宽度,不再覆盖 BasicForm 的布局 */
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
<el-descriptions class="detail-desc" :column="2" border size="small">
|
<el-descriptions class="detail-desc" :column="2" border size="small">
|
||||||
<el-descriptions-item label="产品">{{ productLabel(detail) }}</el-descriptions-item>
|
<el-descriptions-item label="产品">{{ productLabel(detail) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="计费重量(kg)">{{ formatWeight(detail.chargeWeight ?? detail.weight) }}</el-descriptions-item>
|
<el-descriptions-item label="计费重量(kg)">{{ formatWeight(detail.chargeWeight ?? detail.weight) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="原计费重(kg)">{{ formatWeight(detail.weight) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="重量段">{{ detail.weightLabel || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="重量段">{{ detail.weightLabel || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="计费时间">{{ detail.bizDate || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="计费时间">{{ detail.bizDate || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="计价来源">
|
<el-descriptions-item label="计价来源">
|
||||||
@@ -94,6 +95,7 @@
|
|||||||
<el-tab-pane label="应收" name="income">
|
<el-tab-pane label="应收" name="income">
|
||||||
<div class="fee-toolbar">
|
<div class="fee-toolbar">
|
||||||
<el-button type="primary" plain size="small" icon="Plus" v-hasPermi="['price:billing:feeAdd']" @click="openAddFee">增加费用</el-button>
|
<el-button type="primary" plain size="small" icon="Plus" v-hasPermi="['price:billing:feeAdd']" @click="openAddFee">增加费用</el-button>
|
||||||
|
<el-button type="primary" plain size="small" icon="Edit" v-hasPermi="['price:billing:chargeWeightEdit']" :disabled="detail.refundStatus === 'REFUNDED'" @click="openEditCw">修改计费重</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-if="incomeFees.length" :data="incomeFees" border size="small" class="fee-table">
|
<el-table v-if="incomeFees.length" :data="incomeFees" border size="small" class="fee-table">
|
||||||
<el-table-column label="费用名称" min-width="140" show-overflow-tooltip>
|
<el-table-column label="费用名称" min-width="140" show-overflow-tooltip>
|
||||||
@@ -171,11 +173,20 @@
|
|||||||
:edit-row="editFeeRow"
|
:edit-row="editFeeRow"
|
||||||
@success="onFeeAdded"
|
@success="onFeeAdded"
|
||||||
/>
|
/>
|
||||||
|
<edit-charge-weight-dialog
|
||||||
|
v-model="editCwOpen"
|
||||||
|
:bill-id="detail?.billId || ''"
|
||||||
|
:order-no="detail?.orderNo || ''"
|
||||||
|
:origin-weight="detail?.weight"
|
||||||
|
:current-charge-weight="detail?.chargeWeight"
|
||||||
|
@success="onChargeWeightUpdated"
|
||||||
|
/>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import AddIncomeFeeDialog from './AddIncomeFeeDialog.vue'
|
import AddIncomeFeeDialog from './AddIncomeFeeDialog.vue'
|
||||||
|
import EditChargeWeightDialog from './EditChargeWeightDialog.vue'
|
||||||
import { deleteIncomeBillFee } from '@/api/price/billing'
|
import { deleteIncomeBillFee } from '@/api/price/billing'
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
@@ -192,6 +203,7 @@ const emit = defineEmits(['update:modelValue', 'updated'])
|
|||||||
const feeTab = ref('income')
|
const feeTab = ref('income')
|
||||||
const addFeeOpen = ref(false)
|
const addFeeOpen = ref(false)
|
||||||
const editFeeRow = ref(null)
|
const editFeeRow = ref(null)
|
||||||
|
const editCwOpen = ref(false)
|
||||||
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
|
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
|
||||||
|
|
||||||
const incomeFees = computed(() => {
|
const incomeFees = computed(() => {
|
||||||
@@ -240,6 +252,16 @@ function onFeeAdded(data) {
|
|||||||
if (data) emit('updated', data)
|
if (data) emit('updated', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEditCw() {
|
||||||
|
if (!props.detail?.billId) return
|
||||||
|
if (props.detail.refundStatus === 'REFUNDED') return
|
||||||
|
editCwOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChargeWeightUpdated(data) {
|
||||||
|
if (data) emit('updated', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function customerActiveLabel(status) {
|
function customerActiveLabel(status) {
|
||||||
const map = { ACTIVE: '正常', MISSING: '不存在', DISABLED: '停用' }
|
const map = { ACTIVE: '正常', MISSING: '不存在', DISABLED: '停用' }
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:title="dlgTitle"
|
||||||
|
v-model="visible"
|
||||||
|
width="420px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@closed="onClosed"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
|
<el-form-item label="订单号">
|
||||||
|
<span>{{ orderNo || '-' }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="原计费重(kg)">
|
||||||
|
<span>{{ formatWeight(originWeight) }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="当前计费重(kg)">
|
||||||
|
<span>{{ formatWeight(currentChargeWeight) }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="人工修改计费重" prop="chargeWeight">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.chargeWeight"
|
||||||
|
:min="0.001"
|
||||||
|
:precision="3"
|
||||||
|
:step="0.1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submit">确 定</el-button>
|
||||||
|
<el-button :disabled="submitting" @click="visible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { updateChargeWeight } from '@/api/price/billing'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const dlgTitle = '修改计费重'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
billId: { type: String, default: '' },
|
||||||
|
orderNo: { type: String, default: '' },
|
||||||
|
originWeight: { type: [Number, String], default: null },
|
||||||
|
currentChargeWeight: { type: [Number, String], default: null }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'success'])
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (v) => emit('update:modelValue', v)
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const submitting = ref(false)
|
||||||
|
const form = reactive({ chargeWeight: undefined })
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
chargeWeight: [
|
||||||
|
{ required: true, message: '请输入人工修改计费重', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (_r, v, cb) => {
|
||||||
|
if (v == null || Number(v) <= 0) cb(new Error('计费重必须大于 0'))
|
||||||
|
else cb()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (!open) return
|
||||||
|
const cur = props.currentChargeWeight
|
||||||
|
form.chargeWeight = cur == null || cur === '' ? undefined : Number(cur)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function formatWeight(val) {
|
||||||
|
if (val === null || val === undefined || val === '') return '-'
|
||||||
|
const num = Number(val)
|
||||||
|
if (Number.isNaN(num)) return val
|
||||||
|
return num.toFixed(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClosed() {
|
||||||
|
form.chargeWeight = undefined
|
||||||
|
formRef.value?.resetFields?.()
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
formRef.value?.validate?.((ok) => {
|
||||||
|
if (!ok) return
|
||||||
|
if (!props.billId) return
|
||||||
|
submitting.value = true
|
||||||
|
updateChargeWeight({ billId: props.billId, chargeWeight: form.chargeWeight })
|
||||||
|
.then((res) => {
|
||||||
|
ElMessage.success('修改成功,已重新计费')
|
||||||
|
emit('success', res.data || null)
|
||||||
|
visible.value = false
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitting.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user