后台成本价、销售价、利润点按费用逐行展示,费用合计用报价官方总价。选完线路只关页面。报价单文件名带上模板名,空运起运地和转人工跟对应工单走。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
package org.jeecg.modules.inquiry.entity;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 后台成本价 / 销售价 / 利润点的一行。不入库,列表和详情响应里临时带上。
|
||||
* <p>
|
||||
* name 是费用名,最后一行固定为「费用合计」。text 是金额或百分比。
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@ApiModel("报价逐行展示")
|
||||
public class InquiryPriceLine implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("费用名")
|
||||
private String name;
|
||||
|
||||
@ApiModelProperty("金额或百分比")
|
||||
private String text;
|
||||
}
|
||||
+22
@@ -107,6 +107,28 @@ public class InquiryTicket implements Serializable {
|
||||
@ApiModelProperty("毛利率展示")
|
||||
private String profitRate;
|
||||
|
||||
/**
|
||||
* 成本价逐行。不入库,列表/详情 enrich 时按报价记录重算。
|
||||
* 空运整列为空。最后一行是「费用合计」。
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@ApiModelProperty("成本价逐行(展示)")
|
||||
private List<InquiryPriceLine> costLines;
|
||||
|
||||
/**
|
||||
* 销售价逐行。不入库。还没调价的海运/陆运整列为空。
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@ApiModelProperty("销售价逐行(展示)")
|
||||
private List<InquiryPriceLine> saleLines;
|
||||
|
||||
/**
|
||||
* 利润点逐行。不入库。没调过价为空。最后一行是整单利润点。
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@ApiModelProperty("利润点逐行(展示)")
|
||||
private List<InquiryPriceLine> profitLines;
|
||||
|
||||
@ApiModelProperty("是否命中 TMS")
|
||||
private Integer tmsHit;
|
||||
|
||||
|
||||
+3
-1
@@ -18,6 +18,7 @@ import org.jeecg.modules.inquiry.mapper.InquiryQuoteTemplateVersionMapper;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryQuoteTemplateService;
|
||||
import org.jeecg.modules.inquiry.template.QuoteTemplateAiParseClient;
|
||||
import org.jeecg.modules.inquiry.template.QuoteTemplateExcelSupport;
|
||||
import org.jeecg.modules.inquiry.template.QuoteSheetFileName;
|
||||
import org.jeecg.modules.inquiry.template.QuoteTemplateMatcher;
|
||||
import org.jeecg.modules.inquiry.template.QuoteTemplateObjectStore;
|
||||
import org.jeecg.modules.inquiry.template.QuoteTemplateSnapshotRenderer;
|
||||
@@ -770,7 +771,8 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
ok.put("templateId", picked.get("templateId"));
|
||||
ok.put("templateName", picked.get("name"));
|
||||
ok.put("versionNo", picked.get("versionNo"));
|
||||
ok.put("fileName", safeNo + "_quote.xlsx");
|
||||
// 销售看到的名字:工单号 + 后台模板名,不带后缀。磁盘路径仍是带时间戳的 xlsx。
|
||||
ok.put("fileName", QuoteSheetFileName.of(safeNo, String.valueOf(picked.getOrDefault("name", ""))));
|
||||
ok.put("fileBase64", java.util.Base64.getEncoder().encodeToString(bytes));
|
||||
ok.put("fileSize", bytes.length);
|
||||
return ok;
|
||||
|
||||
+611
@@ -0,0 +1,611 @@
|
||||
package org.jeecg.modules.inquiry.support;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryPriceLine;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryQuote;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 后台成本价 / 销售价 / 利润点逐行。
|
||||
* <p>
|
||||
* 职责:从已保存的报价明细铺开展示。不改调价算法,不把各行金额加总冒充费用合计。
|
||||
* 海运/陆运:成本是调价前生效价(手工同名盖 TMS,多出来的补上);销售是最新调价。
|
||||
* 空运:成本整列空;销售是当前生效价;利润点只在调过价之后,对调价前销售价。
|
||||
* 副作用:改 ticket 的三列逐行;手工盖价算出新的官方总价时,同时改成本价和利润点字符串。不写库。
|
||||
* 调用:{@link InquiryTicketPriceSupport#apply} 末尾。列表 enrich 与保存报价后的 update 都走那里。
|
||||
*/
|
||||
public final class InquiryQuoteLineSupport {
|
||||
|
||||
static final String TOTAL_NAME = "费用合计";
|
||||
|
||||
private static final String[] BUCKETS = {"polCostItems", "oceanCostItems", "podCostItems"};
|
||||
|
||||
private InquiryQuoteLineSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按已经算好的总价字符串,补上三列逐行。
|
||||
*
|
||||
* @param ticket 已写入 costPrice / salePrice / profitRate 的工单
|
||||
* @param quotes 该单全部报价,顺序不限,内部按来源取最后一条
|
||||
*/
|
||||
public static void fill(InquiryTicket ticket, List<InquiryQuote> quotes) {
|
||||
if (ticket == null) {
|
||||
return;
|
||||
}
|
||||
boolean air = InquiryTicketPriceSupport.isAir(ticket.getBusinessLine());
|
||||
InquiryQuote lastTms = null;
|
||||
InquiryQuote lastManual = null;
|
||||
InquiryQuote lastAdjust = null;
|
||||
if (quotes != null) {
|
||||
for (InquiryQuote quote : quotes) {
|
||||
if (quote == null) {
|
||||
continue;
|
||||
}
|
||||
if (InquiryQuoteDisplaySupport.isSalesAdjust(quote.getSource(), quote.getSourceLabel())) {
|
||||
lastAdjust = quote;
|
||||
} else if (InquiryQuoteDisplaySupport.isTms(quote.getSource(), quote.getSourceLabel())) {
|
||||
lastTms = quote;
|
||||
} else {
|
||||
lastManual = quote;
|
||||
}
|
||||
}
|
||||
}
|
||||
List<FeeRow> tmsFees = feesOf(lastTms);
|
||||
List<FeeRow> manualFees = feesOf(lastManual);
|
||||
List<FeeRow> adjustFees = feesOf(lastAdjust);
|
||||
|
||||
String costTotal = "";
|
||||
List<FeeRow> costFees = new ArrayList<FeeRow>();
|
||||
if (!air) {
|
||||
costTotal = StringUtils.defaultString(ticket.getCostPrice());
|
||||
if (!manualFees.isEmpty() && !tmsFees.isEmpty()) {
|
||||
costFees = overlay(tmsFees, manualFees);
|
||||
String recomputed = overlayTotal(priceText(lastTms), tmsFees, costFees);
|
||||
if (StringUtils.isNotBlank(recomputed)) {
|
||||
costTotal = recomputed;
|
||||
ticket.setCostPrice(costTotal);
|
||||
}
|
||||
} else if (!manualFees.isEmpty()) {
|
||||
costFees = manualFees;
|
||||
} else {
|
||||
costFees = tmsFees;
|
||||
}
|
||||
} else {
|
||||
ticket.setCostPrice("");
|
||||
}
|
||||
|
||||
boolean adjusted = lastAdjust != null;
|
||||
String saleTotal = "";
|
||||
List<FeeRow> saleFees = new ArrayList<FeeRow>();
|
||||
if (air) {
|
||||
saleTotal = StringUtils.defaultString(ticket.getSalePrice());
|
||||
InquiryQuote saleQuote = lastAdjust != null ? lastAdjust : (lastManual != null ? lastManual : lastTms);
|
||||
saleFees = feesOf(saleQuote);
|
||||
} else if (adjusted) {
|
||||
saleTotal = StringUtils.defaultString(ticket.getSalePrice());
|
||||
saleFees = adjustFees;
|
||||
} else {
|
||||
ticket.setSalePrice("");
|
||||
ticket.setProfitRate("");
|
||||
}
|
||||
|
||||
List<FeeRow> baselineFees;
|
||||
if (air) {
|
||||
if (!manualFees.isEmpty() && !tmsFees.isEmpty()) {
|
||||
baselineFees = overlay(tmsFees, manualFees);
|
||||
} else if (!manualFees.isEmpty()) {
|
||||
baselineFees = manualFees;
|
||||
} else {
|
||||
baselineFees = tmsFees;
|
||||
}
|
||||
} else {
|
||||
baselineFees = costFees;
|
||||
}
|
||||
|
||||
String profitTotal = "";
|
||||
if (adjusted && StringUtils.isNotBlank(saleTotal)) {
|
||||
String locked = profitBaseline(lastAdjust);
|
||||
String baseTotal;
|
||||
if (StringUtils.isNotBlank(locked)) {
|
||||
baseTotal = locked;
|
||||
} else if (air) {
|
||||
baseTotal = lastManual != null ? priceText(lastManual) : priceText(lastTms);
|
||||
} else {
|
||||
baseTotal = costTotal;
|
||||
}
|
||||
profitTotal = StringUtils.defaultString(InquiryTicketPriceSupport.profitText(saleTotal, baseTotal));
|
||||
ticket.setProfitRate(profitTotal);
|
||||
} else if (!adjusted) {
|
||||
ticket.setProfitRate("");
|
||||
}
|
||||
|
||||
if (air) {
|
||||
ticket.setCostLines(Collections.<InquiryPriceLine>emptyList());
|
||||
ticket.setSaleLines(moneyLines(saleFees, saleTotal));
|
||||
ticket.setProfitLines(adjusted && StringUtils.isNotBlank(profitTotal)
|
||||
? profitAgainst(baselineFees, saleFees, profitTotal)
|
||||
: Collections.<InquiryPriceLine>emptyList());
|
||||
return;
|
||||
}
|
||||
if (!adjusted) {
|
||||
ticket.setCostLines(moneyLines(costFees, costTotal));
|
||||
ticket.setSaleLines(Collections.<InquiryPriceLine>emptyList());
|
||||
ticket.setProfitLines(Collections.<InquiryPriceLine>emptyList());
|
||||
return;
|
||||
}
|
||||
List<Aligned> rows = align(costFees, saleFees);
|
||||
ticket.setCostLines(project(rows, true, costTotal));
|
||||
ticket.setSaleLines(project(rows, false, saleTotal));
|
||||
ticket.setProfitLines(profitFromAligned(rows, profitTotal));
|
||||
}
|
||||
|
||||
/**
|
||||
* 手工盖 TMS:同名按出现顺序盖金额,没有的保留,手工多出来的补在后面。
|
||||
* 同名两行不并成一行。
|
||||
*/
|
||||
static List<FeeRow> overlay(List<FeeRow> base, List<FeeRow> incoming) {
|
||||
List<FeeRow> rows = new ArrayList<FeeRow>();
|
||||
for (FeeRow row : base) {
|
||||
rows.add(row.copy());
|
||||
}
|
||||
boolean[] used = new boolean[rows.size()];
|
||||
if (incoming == null) {
|
||||
return rows;
|
||||
}
|
||||
for (FeeRow item : incoming) {
|
||||
int idx = firstUnused(rows, used, item.name);
|
||||
if (idx < 0) {
|
||||
rows.add(item.copy());
|
||||
continue;
|
||||
}
|
||||
used[idx] = true;
|
||||
FeeRow old = rows.get(idx).copy();
|
||||
old.text = item.text;
|
||||
old.amount = item.amount;
|
||||
if (StringUtils.isNotBlank(item.currency)) {
|
||||
old.currency = item.currency;
|
||||
}
|
||||
rows.set(idx, old);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用合计:先用 TMS 官方总价,再加同币种差额。币种不同的行不进差额,也不做汇率。
|
||||
*/
|
||||
static String overlayTotal(String tmsPriceText, List<FeeRow> oldRows, List<FeeRow> newRows) {
|
||||
BigDecimal base = InquiryTicketPriceSupport.parseAmount(tmsPriceText);
|
||||
if (base == null) {
|
||||
return StringUtils.trimToEmpty(tmsPriceText);
|
||||
}
|
||||
String totalCcy = currencyOf(tmsPriceText);
|
||||
boolean[] used = new boolean[oldRows == null ? 0 : oldRows.size()];
|
||||
BigDecimal delta = BigDecimal.ZERO;
|
||||
if (newRows != null) {
|
||||
for (FeeRow neu : newRows) {
|
||||
if (neu.amount == null) {
|
||||
continue;
|
||||
}
|
||||
String lineCcy = StringUtils.isNotBlank(neu.currency) ? neu.currency : totalCcy;
|
||||
if (StringUtils.isNotBlank(lineCcy) && StringUtils.isNotBlank(totalCcy) && !lineCcy.equals(totalCcy)) {
|
||||
continue;
|
||||
}
|
||||
int idx = firstUnused(oldRows, used, neu.name);
|
||||
if (idx < 0) {
|
||||
delta = delta.add(neu.amount);
|
||||
continue;
|
||||
}
|
||||
used[idx] = true;
|
||||
FeeRow old = oldRows.get(idx);
|
||||
if (old.amount == null) {
|
||||
continue;
|
||||
}
|
||||
delta = delta.add(neu.amount.subtract(old.amount));
|
||||
}
|
||||
}
|
||||
return formatMoney(base.add(delta), totalCcy);
|
||||
}
|
||||
|
||||
private static List<InquiryPriceLine> moneyLines(List<FeeRow> fees, String totalText) {
|
||||
List<InquiryPriceLine> lines = new ArrayList<InquiryPriceLine>();
|
||||
if (fees != null) {
|
||||
for (FeeRow fee : fees) {
|
||||
lines.add(line(fee.name, fee.text));
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(totalText)) {
|
||||
lines.add(line(TOTAL_NAME, totalText));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static List<InquiryPriceLine> project(List<Aligned> rows, boolean costSide, String totalText) {
|
||||
List<InquiryPriceLine> lines = new ArrayList<InquiryPriceLine>();
|
||||
for (Aligned row : rows) {
|
||||
FeeRow side = costSide ? row.left : row.right;
|
||||
lines.add(line(row.name, side == null ? "" : side.text));
|
||||
}
|
||||
if (StringUtils.isNotBlank(totalText)) {
|
||||
lines.add(line(TOTAL_NAME, totalText));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 利润点按销售价的费用顺序,对调价前同名行(按出现顺序,不并行)。
|
||||
* 对不上或调价前金额为 0:这一行留空。费用合计用整单利润点。
|
||||
*/
|
||||
private static List<InquiryPriceLine> profitAgainst(List<FeeRow> baseline, List<FeeRow> sale, String totalProfit) {
|
||||
boolean[] used = new boolean[baseline == null ? 0 : baseline.size()];
|
||||
List<InquiryPriceLine> lines = new ArrayList<InquiryPriceLine>();
|
||||
if (sale != null) {
|
||||
for (FeeRow row : sale) {
|
||||
int idx = firstUnused(baseline, used, row.name);
|
||||
FeeRow base = idx < 0 ? null : baseline.get(idx);
|
||||
if (idx >= 0) {
|
||||
used[idx] = true;
|
||||
}
|
||||
lines.add(line(row.name, percentOf(base, row)));
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(totalProfit)) {
|
||||
lines.add(line(TOTAL_NAME, totalProfit));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static List<InquiryPriceLine> profitFromAligned(List<Aligned> rows, String totalProfit) {
|
||||
List<InquiryPriceLine> lines = new ArrayList<InquiryPriceLine>();
|
||||
for (Aligned row : rows) {
|
||||
lines.add(line(row.name, percentOf(row.left, row.right)));
|
||||
}
|
||||
if (StringUtils.isNotBlank(totalProfit)) {
|
||||
lines.add(line(TOTAL_NAME, totalProfit));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static String percentOf(FeeRow before, FeeRow after) {
|
||||
if (before == null || after == null || before.amount == null || after.amount == null) {
|
||||
return "";
|
||||
}
|
||||
if (before.amount.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return "";
|
||||
}
|
||||
return InquiryTicketPriceSupport.profitText(after.text, before.text);
|
||||
}
|
||||
|
||||
private static List<Aligned> align(List<FeeRow> left, List<FeeRow> right) {
|
||||
List<FeeRow> safeRight = right == null ? Collections.<FeeRow>emptyList() : right;
|
||||
boolean[] used = new boolean[safeRight.size()];
|
||||
List<Aligned> out = new ArrayList<Aligned>();
|
||||
if (left != null) {
|
||||
for (FeeRow row : left) {
|
||||
int idx = firstUnused(safeRight, used, row.name);
|
||||
FeeRow match = null;
|
||||
if (idx >= 0) {
|
||||
used[idx] = true;
|
||||
match = safeRight.get(idx);
|
||||
}
|
||||
out.add(new Aligned(row.name, row, match));
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < safeRight.size(); i++) {
|
||||
if (!used[i]) {
|
||||
FeeRow row = safeRight.get(i);
|
||||
out.add(new Aligned(row.name, null, row));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static int firstUnused(List<FeeRow> rows, boolean[] used, String name) {
|
||||
if (rows == null) {
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
if (used != null && i < used.length && used[i]) {
|
||||
continue;
|
||||
}
|
||||
if (sameFee(name, rows.get(i).name)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用名对齐:去空格后全等,或只差「费 / 费用 / 用」。操作费不等于码头操作费。
|
||||
*/
|
||||
static boolean sameFee(String left, String right) {
|
||||
String a = feeKey(left);
|
||||
String b = feeKey(right);
|
||||
if (a.isEmpty() || b.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (a.equals(b)) {
|
||||
return true;
|
||||
}
|
||||
if (a.equals(b + "用") || a.equals(b + "费用") || b.equals(a + "用") || b.equals(a + "费用")) {
|
||||
return true;
|
||||
}
|
||||
return (a + "费").equals(b) || (b + "费").equals(a);
|
||||
}
|
||||
|
||||
private static String feeKey(String name) {
|
||||
String raw = StringUtils.trimToEmpty(name).replace(" ", "").replace("(", "(").replace(")", ")");
|
||||
int slash = raw.indexOf('/');
|
||||
if (slash < 0) {
|
||||
slash = raw.indexOf('/');
|
||||
}
|
||||
if (slash > 0) {
|
||||
raw = raw.substring(0, slash);
|
||||
}
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
private static List<FeeRow> feesOf(InquiryQuote quote) {
|
||||
JSONObject payload = payloadOf(quote);
|
||||
if (payload == null) {
|
||||
return new ArrayList<FeeRow>();
|
||||
}
|
||||
List<FeeRow> rows = readFees(payload.get("fee_lines"));
|
||||
if (rows.isEmpty()) {
|
||||
rows = readFees(payload.get("fee_rows"));
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
JSONObject selected = asObject(payload.get("selected_air_option"));
|
||||
if (selected == null) {
|
||||
selected = asObject(payload.get("selectedAirOption"));
|
||||
}
|
||||
if (selected != null) {
|
||||
rows = readFees(firstPresent(selected, "departureCharges", "departure_charges", "feeItems"));
|
||||
}
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
JSONArray options = asArray(payload.get("airOptions"));
|
||||
if (options == null) {
|
||||
options = asArray(payload.get("air_options"));
|
||||
}
|
||||
if (options != null) {
|
||||
for (int i = 0; i < options.size(); i++) {
|
||||
JSONObject option = asObject(options.get(i));
|
||||
if (option == null) {
|
||||
continue;
|
||||
}
|
||||
rows = readFees(firstPresent(option, "departureCharges", "departure_charges", "feeItems"));
|
||||
if (!rows.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 扁平明细为空才读海运三段。某一段有 fee_rows_ 就用它,没有才用原桶,避免同一段加两遍。
|
||||
if (rows.isEmpty()) {
|
||||
for (String bucket : BUCKETS) {
|
||||
List<FeeRow> keyed = readFees(payload.get("fee_rows_" + bucket));
|
||||
if (!keyed.isEmpty()) {
|
||||
rows.addAll(keyed);
|
||||
} else {
|
||||
rows.addAll(readFees(payload.get(bucket)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static Object firstPresent(JSONObject obj, String... keys) {
|
||||
for (String key : keys) {
|
||||
if (obj.get(key) != null) {
|
||||
return obj.get(key);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<FeeRow> readFees(Object raw) {
|
||||
JSONArray array = asArray(raw);
|
||||
List<FeeRow> rows = new ArrayList<FeeRow>();
|
||||
if (array == null) {
|
||||
return rows;
|
||||
}
|
||||
for (int i = 0; i < array.size(); i++) {
|
||||
FeeRow row = toFee(asObject(array.get(i)));
|
||||
if (row != null) {
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static FeeRow toFee(JSONObject item) {
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
String name = firstText(item, "name", "chargeName", "feeName", "label");
|
||||
if (StringUtils.isBlank(name) || isSubtotal(name) || isTotalAlias(name)) {
|
||||
return null;
|
||||
}
|
||||
String amount = firstText(item, "amount", "totalPrice", "value");
|
||||
if (StringUtils.isBlank(amount) || "-".equals(amount)) {
|
||||
amount = firstText(item, "unitPrice", "unit_price");
|
||||
}
|
||||
if (StringUtils.isBlank(amount) || "-".equals(amount)) {
|
||||
return null;
|
||||
}
|
||||
String currency = firstText(item, "currency", "currencyCode").toUpperCase();
|
||||
if ("RMB".equals(currency)) {
|
||||
currency = "CNY";
|
||||
}
|
||||
String text = amount;
|
||||
String upper = amount.toUpperCase();
|
||||
if (StringUtils.isNotBlank(currency) && !upper.startsWith(currency) && !upper.startsWith("RMB")) {
|
||||
text = currency + " " + amount;
|
||||
}
|
||||
FeeRow row = new FeeRow();
|
||||
row.name = name;
|
||||
row.text = text;
|
||||
row.currency = StringUtils.isNotBlank(currency) ? currency : currencyOf(text);
|
||||
row.amount = InquiryTicketPriceSupport.parseAmount(text);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static boolean isSubtotal(String name) {
|
||||
return feeKey(name).contains("小计");
|
||||
}
|
||||
|
||||
private static boolean isTotalAlias(String name) {
|
||||
String key = feeKey(name);
|
||||
return "费用合计".equals(key) || "费用总计".equals(key) || "总销售价".equals(key)
|
||||
|| "总成本价".equals(key) || "汇总价".equals(key) || "总价".equals(key)
|
||||
|| "报价合计".equals(key);
|
||||
}
|
||||
|
||||
private static String profitBaseline(InquiryQuote quote) {
|
||||
JSONObject payload = payloadOf(quote);
|
||||
if (payload == null) {
|
||||
return "";
|
||||
}
|
||||
String locked = firstText(payload, "profit_baseline", "profitBaseline");
|
||||
return StringUtils.trimToEmpty(locked);
|
||||
}
|
||||
|
||||
private static JSONObject payloadOf(InquiryQuote quote) {
|
||||
if (quote == null || StringUtils.isBlank(quote.getPayloadJson())) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parseObject(quote.getPayloadJson());
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject asObject(Object raw) {
|
||||
if (raw instanceof JSONObject) {
|
||||
return (JSONObject) raw;
|
||||
}
|
||||
if (raw instanceof String && StringUtils.isNotBlank((String) raw)) {
|
||||
try {
|
||||
Object parsed = JSON.parse((String) raw);
|
||||
if (parsed instanceof String) {
|
||||
parsed = JSON.parse((String) parsed);
|
||||
}
|
||||
if (parsed instanceof JSONObject) {
|
||||
return (JSONObject) parsed;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JSONArray asArray(Object raw) {
|
||||
if (raw instanceof JSONArray) {
|
||||
return (JSONArray) raw;
|
||||
}
|
||||
if (raw instanceof List) {
|
||||
JSONArray array = new JSONArray();
|
||||
for (Object item : (List<?>) raw) {
|
||||
array.add(item);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
if (raw instanceof String && StringUtils.isNotBlank((String) raw)) {
|
||||
try {
|
||||
Object parsed = JSON.parse((String) raw);
|
||||
if (parsed instanceof String) {
|
||||
parsed = JSON.parse((String) parsed);
|
||||
}
|
||||
if (parsed instanceof JSONArray) {
|
||||
return (JSONArray) parsed;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String firstText(JSONObject obj, String... keys) {
|
||||
for (String key : keys) {
|
||||
String text = StringUtils.trimToEmpty(obj.getString(key));
|
||||
if (StringUtils.isNotBlank(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String priceText(InquiryQuote quote) {
|
||||
return quote == null ? "" : StringUtils.trimToEmpty(quote.getPriceText());
|
||||
}
|
||||
|
||||
private static String currencyOf(String priceText) {
|
||||
String text = StringUtils.trimToEmpty(priceText).toUpperCase();
|
||||
if (text.startsWith("RMB")) {
|
||||
return "CNY";
|
||||
}
|
||||
if (text.length() >= 3 && Character.isLetter(text.charAt(0))
|
||||
&& Character.isLetter(text.charAt(1)) && Character.isLetter(text.charAt(2))) {
|
||||
return text.substring(0, 3);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String formatMoney(BigDecimal amount, String currency) {
|
||||
String body = amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
|
||||
if (StringUtils.isBlank(currency)) {
|
||||
return body;
|
||||
}
|
||||
return currency + " " + body;
|
||||
}
|
||||
|
||||
private static InquiryPriceLine line(String name, String text) {
|
||||
InquiryPriceLine row = new InquiryPriceLine();
|
||||
row.setName(name);
|
||||
row.setText(StringUtils.defaultString(text));
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 一条费用的展示金额。amount 是比利润用的数字,text 是原样带币种的写法。 */
|
||||
static final class FeeRow {
|
||||
private String name;
|
||||
private String text;
|
||||
private String currency;
|
||||
private BigDecimal amount;
|
||||
|
||||
private FeeRow copy() {
|
||||
FeeRow row = new FeeRow();
|
||||
row.name = name;
|
||||
row.text = text;
|
||||
row.currency = currency;
|
||||
row.amount = amount;
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
/** 成本侧与销售侧对上的一行。某一侧没有时为 null。 */
|
||||
private static final class Aligned {
|
||||
private final String name;
|
||||
private final FeeRow left;
|
||||
private final FeeRow right;
|
||||
|
||||
private Aligned(String name, FeeRow left, FeeRow right) {
|
||||
this.name = name;
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -15,7 +15,7 @@ import java.util.regex.Pattern;
|
||||
* <p>
|
||||
* 海运/陆运:TMS、产品报价是成本;销售调价后才有销售价,利润点=(销售-成本)/成本。
|
||||
* 空运:成本价始终空;TMS/航线/调价都是销售侧;利润点只在销售调过价后算。
|
||||
* 副作用:只改传入 ticket 的三个展示字段,不写库。调用方决定是否 updateById。
|
||||
* 副作用:只改传入 ticket 的展示字段(三个总价字符串和三列逐行),不写库。调用方决定是否 updateById。
|
||||
*/
|
||||
public final class InquiryTicketPriceSupport {
|
||||
|
||||
@@ -65,17 +65,19 @@ public final class InquiryTicketPriceSupport {
|
||||
ticket.setSalePrice(StringUtils.isNotBlank(manualPrice) ? manualPrice : tmsPrice);
|
||||
ticket.setProfitRate("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
String cost = StringUtils.isNotBlank(manualPrice) ? manualPrice : tmsPrice;
|
||||
ticket.setCostPrice(StringUtils.defaultString(cost));
|
||||
if (StringUtils.isNotBlank(adjustPrice)) {
|
||||
ticket.setSalePrice(adjustPrice);
|
||||
ticket.setProfitRate(profitText(adjustPrice, cost));
|
||||
} else {
|
||||
ticket.setSalePrice("");
|
||||
ticket.setProfitRate("");
|
||||
String cost = StringUtils.isNotBlank(manualPrice) ? manualPrice : tmsPrice;
|
||||
ticket.setCostPrice(StringUtils.defaultString(cost));
|
||||
if (StringUtils.isNotBlank(adjustPrice)) {
|
||||
ticket.setSalePrice(adjustPrice);
|
||||
ticket.setProfitRate(profitText(adjustPrice, cost));
|
||||
} else {
|
||||
ticket.setSalePrice("");
|
||||
ticket.setProfitRate("");
|
||||
}
|
||||
}
|
||||
// 三个总价定完后,按同一批报价拆成逐行。有手工明细盖 TMS 时,这里会改写成本价和利润点。
|
||||
InquiryQuoteLineSupport.fill(ticket, quotes);
|
||||
}
|
||||
|
||||
static boolean isAir(String businessLine) {
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package org.jeecg.modules.inquiry.template;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 系统生成报价单的对外文件名:工单号接后台模板名,再加 .xlsx。
|
||||
*
|
||||
* <p>主账填的是 Excel。PDF 由智能体转完后再改成 .pdf。
|
||||
* 航线、产品上传的附件不走本类。
|
||||
*/
|
||||
public final class QuoteSheetFileName {
|
||||
|
||||
private static final String[] SUFFIXES = {".xlsx", ".xls", ".xlsm", ".pdf"};
|
||||
|
||||
private QuoteSheetFileName() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单号原样接上后台模板名,并带 .xlsx。模板名为空时退回「工单号_quote.xlsx」。
|
||||
*
|
||||
* @param workOrderNo 工单号
|
||||
* @param templateName 后台模板名称
|
||||
* @return 对外文件名
|
||||
*/
|
||||
public static String of(String workOrderNo, String templateName) {
|
||||
String no = StringUtils.defaultString(workOrderNo).trim();
|
||||
String name = plain(templateName);
|
||||
if (StringUtils.isBlank(no)) {
|
||||
return StringUtils.defaultIfBlank(name, "quote") + ".xlsx";
|
||||
}
|
||||
if (StringUtils.isBlank(name)) {
|
||||
return no + "_quote.xlsx";
|
||||
}
|
||||
return no + name + ".xlsx";
|
||||
}
|
||||
|
||||
/** 去掉已知后缀和路径非法字符,中文保留。 */
|
||||
static String plain(String name) {
|
||||
String text = StringUtils.defaultString(name).trim();
|
||||
String lower = text.toLowerCase();
|
||||
for (String ext : SUFFIXES) {
|
||||
if (lower.endsWith(ext)) {
|
||||
text = text.substring(0, text.length() - ext.length()).trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
StringBuilder out = new StringBuilder();
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char ch = text.charAt(i);
|
||||
if (ch < 32 || "\\/:*?\"<>|".indexOf(ch) >= 0) {
|
||||
continue;
|
||||
}
|
||||
out.append(ch);
|
||||
}
|
||||
return out.toString().trim();
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package org.jeecg.modules.inquiry.support;
|
||||
|
||||
import org.jeecg.modules.inquiry.entity.InquiryPriceLine;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryQuote;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 后台三列逐行:费用名 + 金额,最后一行费用合计用官方总价。
|
||||
*/
|
||||
public class InquiryQuoteLineSupportTest {
|
||||
|
||||
@Test
|
||||
public void seaUnadjustedShowsCostLinesAndLeavesSaleEmpty() {
|
||||
InquiryTicket ticket = ticket("SEA");
|
||||
InquiryTicketPriceSupport.apply(ticket, Collections.singletonList(quote(
|
||||
"TMS", "TMS标准报价", "CNY 1200.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"1200.00\",\"fee_lines\":["
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"1000\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"卸货费\",\"amount\":\"200\",\"currency\":\"CNY\"}"
|
||||
+ "]}")));
|
||||
assertEquals("CNY 1200.00", ticket.getCostPrice());
|
||||
assertEquals("", ticket.getSalePrice());
|
||||
assertEquals("", ticket.getProfitRate());
|
||||
assertLines(ticket.getCostLines(),
|
||||
"运输费", "CNY 1000",
|
||||
"卸货费", "CNY 200",
|
||||
"费用合计", "CNY 1200.00");
|
||||
assertTrue(ticket.getSaleLines() == null || ticket.getSaleLines().isEmpty());
|
||||
assertTrue(ticket.getProfitLines() == null || ticket.getProfitLines().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaAdjustsOneFeeAndKeepsOfficialTotal() {
|
||||
InquiryTicket ticket = ticket("LAND");
|
||||
InquiryTicketPriceSupport.apply(ticket, Arrays.asList(
|
||||
quote("TMS", "TMS标准报价", "CNY 1200.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"1200.00\",\"fee_lines\":["
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"1000\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"卸货费\",\"amount\":\"200\",\"currency\":\"CNY\"}]}"),
|
||||
quote("SALES_ADJUST", "销售调价", "CNY 1220.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"1220.00\",\"profit_baseline\":\"CNY 1200.00\",\"fee_lines\":["
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"1000\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"卸货费\",\"amount\":\"220\",\"currency\":\"CNY\"}]}")));
|
||||
assertLines(ticket.getCostLines(),
|
||||
"运输费", "CNY 1000",
|
||||
"卸货费", "CNY 200",
|
||||
"费用合计", "CNY 1200.00");
|
||||
assertLines(ticket.getSaleLines(),
|
||||
"运输费", "CNY 1000",
|
||||
"卸货费", "CNY 220",
|
||||
"费用合计", "CNY 1220.00");
|
||||
assertLines(ticket.getProfitLines(),
|
||||
"运输费", "0%",
|
||||
"卸货费", "10%",
|
||||
"费用合计", "1.67%");
|
||||
assertEquals("1.67%", ticket.getProfitRate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void totalOnlyAdjustLeavesFeePercentsAtZero() {
|
||||
InquiryTicket ticket = ticket("SEA");
|
||||
InquiryTicketPriceSupport.apply(ticket, Arrays.asList(
|
||||
quote("TMS", "TMS标准报价", "CNY 2000.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"2000.00\",\"fee_lines\":["
|
||||
+ "{\"name\":\"增点费\",\"amount\":\"100\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"拆板费\",\"amount\":\"100\",\"currency\":\"CNY\"}]}"),
|
||||
quote("SALES_ADJUST", "销售调价", "CNY 2100.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"2100.00\",\"fee_lines\":["
|
||||
+ "{\"name\":\"增点费\",\"amount\":\"100\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"拆板费\",\"amount\":\"100\",\"currency\":\"CNY\"}]}")));
|
||||
assertLines(ticket.getProfitLines(),
|
||||
"增点费", "0%",
|
||||
"拆板费", "0%",
|
||||
"费用合计", "5%");
|
||||
assertEquals("费用合计", ticket.getSaleLines().get(2).getName());
|
||||
assertEquals("CNY 2100.00", ticket.getSaleLines().get(2).getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void secondAdjustComparesToFirstBaseline() {
|
||||
InquiryTicket ticket = ticket("LAND");
|
||||
InquiryTicketPriceSupport.apply(ticket, Arrays.asList(
|
||||
quote("TMS", "TMS标准报价", "CNY 1000.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"1000\",\"fee_lines\":["
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"1000\",\"currency\":\"CNY\"}]}"),
|
||||
quote("SALES_ADJUST", "销售调价", "CNY 1100.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"1100\",\"profit_baseline\":\"1000\",\"fee_lines\":["
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"1100\",\"currency\":\"CNY\"}]}"),
|
||||
quote("SALES_ADJUST", "销售调价", "CNY 1300.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"1300\",\"profit_baseline\":\"1000\",\"fee_lines\":["
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"1300\",\"currency\":\"CNY\"}]}")));
|
||||
assertLines(ticket.getSaleLines(), "运输费", "CNY 1300", "费用合计", "CNY 1300.00");
|
||||
assertLines(ticket.getProfitLines(), "运输费", "30%", "费用合计", "30%");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void airCostStaysEmptyUntilAdjustThenProfitUsesSaleBaseline() {
|
||||
InquiryTicket before = ticket("AIR");
|
||||
InquiryQuote tms = quote("TMS", "TMS标准报价", "CNY 2000.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"2000.00\",\"fee_lines\":["
|
||||
+ "{\"name\":\"空运费\",\"amount\":\"1800\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"增点费\",\"amount\":\"100\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"拆板费\",\"amount\":\"100\",\"currency\":\"CNY\"}]}");
|
||||
InquiryTicketPriceSupport.apply(before, Collections.singletonList(tms));
|
||||
assertTrue(before.getCostLines() == null || before.getCostLines().isEmpty());
|
||||
assertEquals("", before.getCostPrice());
|
||||
assertEquals("CNY 2000.00", before.getSalePrice());
|
||||
assertTrue(before.getProfitLines() == null || before.getProfitLines().isEmpty());
|
||||
assertLines(before.getSaleLines(),
|
||||
"空运费", "CNY 1800",
|
||||
"增点费", "CNY 100",
|
||||
"拆板费", "CNY 100",
|
||||
"费用合计", "CNY 2000.00");
|
||||
|
||||
InquiryTicket after = ticket("空运");
|
||||
InquiryTicketPriceSupport.apply(after, Arrays.asList(
|
||||
tms,
|
||||
quote("SALES_ADJUST", "销售调价", "CNY 2100.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"2100.00\",\"profit_baseline\":\"CNY 2000.00\",\"fee_lines\":["
|
||||
+ "{\"name\":\"空运费\",\"amount\":\"1800\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"增点费\",\"amount\":\"100\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"拆板费\",\"amount\":\"110\",\"currency\":\"CNY\"}]}")));
|
||||
assertTrue(after.getCostLines() == null || after.getCostLines().isEmpty());
|
||||
assertLines(after.getProfitLines(),
|
||||
"空运费", "0%",
|
||||
"增点费", "0%",
|
||||
"拆板费", "10%",
|
||||
"费用合计", "5%");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void airOfficialTotalIsNotTheSumOfDepartureCharges() {
|
||||
InquiryTicket ticket = ticket("AIR");
|
||||
InquiryTicketPriceSupport.apply(ticket, Collections.singletonList(quote(
|
||||
"TMS", "TMS标准报价", "CNY 7918.25",
|
||||
"{\"currency\":\"CNY\",\"total\":\"7918.25\",\"selected_air_option\":{"
|
||||
+ "\"departureCharges\":["
|
||||
+ "{\"name\":\"增点费\",\"amount\":\"100.0\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"拆板费\",\"amount\":\"100.0\",\"currency\":\"USD\"}"
|
||||
+ "]}}")));
|
||||
assertLines(ticket.getSaleLines(),
|
||||
"增点费", "CNY 100.0",
|
||||
"拆板费", "USD 100.0",
|
||||
"费用合计", "CNY 7918.25");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void duplicateFeeNamesStaySeparate() {
|
||||
InquiryTicket ticket = ticket("LAND");
|
||||
InquiryTicketPriceSupport.apply(ticket, Arrays.asList(
|
||||
quote("TMS", "TMS标准报价", "CNY 10000.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"10000\",\"fee_lines\":["
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"7000\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"3000\",\"currency\":\"CNY\"}]}"),
|
||||
quote("SALES_ADJUST", "销售调价", "CNY 11000.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"11000\",\"fee_lines\":["
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"7700\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"3300\",\"currency\":\"CNY\"}]}")));
|
||||
assertEquals(3, ticket.getCostLines().size());
|
||||
assertEquals("CNY 7000", ticket.getCostLines().get(0).getText());
|
||||
assertEquals("CNY 3000", ticket.getCostLines().get(1).getText());
|
||||
assertLines(ticket.getProfitLines(), "运输费", "10%", "运输费", "10%", "费用合计", "10%");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manualOverlayKeepsUntouchedTmsFee() {
|
||||
InquiryTicket ticket = ticket("SEA");
|
||||
InquiryTicketPriceSupport.apply(ticket, Arrays.asList(
|
||||
quote("TMS", "TMS标准报价", "CNY 1200.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"1200.00\",\"fee_lines\":["
|
||||
+ "{\"name\":\"运输费\",\"amount\":\"1000\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"卸货费\",\"amount\":\"200\",\"currency\":\"CNY\"}]}"),
|
||||
quote("PRODUCT", "手工报价", "CNY 320.00",
|
||||
"{\"currency\":\"CNY\",\"total\":\"320\",\"fee_lines\":["
|
||||
+ "{\"name\":\"卸货费\",\"amount\":\"300\",\"currency\":\"CNY\"},"
|
||||
+ "{\"name\":\"配送费\",\"amount\":\"20\",\"currency\":\"CNY\"}]}")));
|
||||
assertLines(ticket.getCostLines(),
|
||||
"运输费", "CNY 1000",
|
||||
"卸货费", "CNY 300",
|
||||
"配送费", "CNY 20",
|
||||
"费用合计", "CNY 1320.00");
|
||||
assertEquals("CNY 1320.00", ticket.getCostPrice());
|
||||
assertTrue(ticket.getSaleLines() == null || ticket.getSaleLines().isEmpty());
|
||||
}
|
||||
|
||||
private static void assertLines(List<InquiryPriceLine> lines, String... nameText) {
|
||||
assertEquals(nameText.length / 2, lines.size());
|
||||
for (int i = 0; i < nameText.length; i += 2) {
|
||||
assertEquals(nameText[i], lines.get(i / 2).getName());
|
||||
assertEquals(nameText[i + 1], lines.get(i / 2).getText());
|
||||
}
|
||||
}
|
||||
|
||||
private static InquiryTicket ticket(String line) {
|
||||
InquiryTicket t = new InquiryTicket();
|
||||
t.setBusinessLine(line);
|
||||
return t;
|
||||
}
|
||||
|
||||
private static InquiryQuote quote(String source, String label, String price, String payload) {
|
||||
InquiryQuote q = new InquiryQuote();
|
||||
q.setSource(source);
|
||||
q.setSourceLabel(label);
|
||||
q.setPriceText(price);
|
||||
q.setPayloadJson(payload);
|
||||
return q;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.inquiry.template;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* 系统报价单对外文件名:工单号接模板名,并带 .xlsx。
|
||||
*/
|
||||
public class QuoteSheetFileNameTest {
|
||||
|
||||
@Test
|
||||
public void joinsWorkOrderAndTemplateNameWithoutSuffix() {
|
||||
assertEquals(
|
||||
"WO202609100001陆运国内运输拼车报价单.xlsx",
|
||||
QuoteSheetFileName.of("WO202609100001", "陆运国内运输拼车报价单"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dropsExtensionOnTemplateName() {
|
||||
assertEquals(
|
||||
"WO202609100001海运报价单模板.xlsx",
|
||||
QuoteSheetFileName.of("WO202609100001", "海运报价单模板.xlsx"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankTemplateFallsBackWithoutSuffix() {
|
||||
assertEquals("WO202609100001_quote.xlsx", QuoteSheetFileName.of("WO202609100001", ""));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user