落地海运协同群:测服靠会话存档听群话,报价单能带上协同字段。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+30
@@ -207,6 +207,36 @@ public class InquiryAgentController {
|
||||
return Result.OK(workOrderService.findByCollabChat(firstStr(body, "chatId", "chat_id")));
|
||||
}
|
||||
|
||||
@ApiOperation("协同结束(不改六态,这单不能再拉群)")
|
||||
@PostMapping("/ticket/endCollabGroup")
|
||||
public Result<Map<String, Object>> endCollabGroup(@RequestBody Map<String, Object> body,
|
||||
HttpServletRequest http) {
|
||||
if (!authed(http)) {
|
||||
return Result.error(401, "unauthorized");
|
||||
}
|
||||
return Result.OK(workOrderService.endCollabGroup(firstStr(body, "workOrderNo", "work_order_no")));
|
||||
}
|
||||
|
||||
@ApiOperation("群附件元数据入库(不改六态)")
|
||||
@PostMapping("/ticket/attachFile")
|
||||
public Result<Map<String, Object>> attachFile(@RequestBody Map<String, Object> body,
|
||||
HttpServletRequest http) {
|
||||
if (!authed(http)) {
|
||||
return Result.error(401, "unauthorized");
|
||||
}
|
||||
return Result.OK(workOrderService.attachFile(body));
|
||||
}
|
||||
|
||||
@ApiOperation("产品报价写入新版本")
|
||||
@PostMapping("/ticket/upsertProductQuote")
|
||||
public Result<Map<String, Object>> upsertProductQuote(@RequestBody Map<String, Object> body,
|
||||
HttpServletRequest http) {
|
||||
if (!authed(http)) {
|
||||
return Result.error(401, "unauthorized");
|
||||
}
|
||||
return Result.OK(workOrderService.upsertProductQuote(body));
|
||||
}
|
||||
|
||||
private boolean authed(HttpServletRequest http) {
|
||||
String got = http.getHeader("X-Agent-Token");
|
||||
if (StringUtils.isBlank(got)) {
|
||||
|
||||
+15
@@ -89,4 +89,19 @@ public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
|
||||
* 用协同群 chat_id 找回工单。只读。
|
||||
*/
|
||||
Map<String, Object> findByCollabChat(String chatId);
|
||||
|
||||
/**
|
||||
* 协同结束:不改六态,这单不能再拉群。
|
||||
*/
|
||||
Map<String, Object> endCollabGroup(String workOrderNo);
|
||||
|
||||
/**
|
||||
* 群附件元数据入库。不改六态。
|
||||
*/
|
||||
Map<String, Object> attachFile(Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 产品报价写入新版本并视情况迁「已报价」。
|
||||
*/
|
||||
Map<String, Object> upsertProductQuote(Map<String, Object> body);
|
||||
}
|
||||
|
||||
+21
-1
@@ -538,10 +538,30 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
out.put("pod", first(src, "目的港", "pod", "destination"));
|
||||
out.put("destination", first(src, "目的港", "destination", "pod"));
|
||||
out.put("commodity", first(src, "品名", "commodity"));
|
||||
// 海运七项:合同键是 quantity / fcl_or_lcl / container_type_qty,工单事实是中文键
|
||||
out.put("quantity", first(src, "货量", "货物数量", "quantity"));
|
||||
out.put("fcl_or_lcl", first(src, "整柜或拼柜", "fcl_or_lcl"));
|
||||
out.put("container_type_qty", first(src, "箱型箱量", "container_type_qty"));
|
||||
out.put("pieces", first(src, "件数", "pieces"));
|
||||
out.put("weight_kg", first(src, "毛重", "weight_kg"));
|
||||
out.put("volume_cbm", first(src, "体积", "volume_cbm"));
|
||||
out.put("quote_date", first(src, "报价日期", "quote_date"));
|
||||
out.put("quote_date", first(src, "报价日期", "quote_date", "询价日期"));
|
||||
out.put("trade_terms", first(src, "贸易条款", "trade_terms"));
|
||||
out.put("cargo_ready_date", first(src, "货好时间", "cargo_ready_date"));
|
||||
out.put("hs_code", first(src, "商品海关编码", "HS编码", "海关编码", "hs_code"));
|
||||
String oil = first(src, "是否含油", "has_oil");
|
||||
String elec = first(src, "是否含电", "has_battery");
|
||||
String mag = first(src, "是否含磁", "has_magnet");
|
||||
String oem = first(src, "oil_elec_mag", "是否含油,电,磁");
|
||||
if (StringUtils.isBlank(oem) && (StringUtils.isNotBlank(oil) || StringUtils.isNotBlank(elec) || StringUtils.isNotBlank(mag))) {
|
||||
oem = "油:" + (StringUtils.isNotBlank(oil) ? oil : "-")
|
||||
+ ";电:" + (StringUtils.isNotBlank(elec) ? elec : "-")
|
||||
+ ";磁:" + (StringUtils.isNotBlank(mag) ? mag : "-");
|
||||
}
|
||||
if (StringUtils.isNotBlank(oem)) {
|
||||
out.put("oil_elec_mag", oem);
|
||||
out.put("是否含油,电,磁", oem);
|
||||
}
|
||||
if (StringUtils.isBlank(out.getString("quote_date"))) {
|
||||
out.put("quote_date", new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date()));
|
||||
}
|
||||
|
||||
+143
@@ -756,9 +756,152 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
}
|
||||
}
|
||||
out.put("collab_facts", collabFacts);
|
||||
boolean ended = "1".equals(collabFacts.get("__ended"))
|
||||
|| "true".equalsIgnoreCase(StringUtils.trimToEmpty(collabFacts.get("__ended")));
|
||||
out.put("collab_ended", ended);
|
||||
out.put("status", StringUtils.defaultString(ticket.getStatus()));
|
||||
out.put("business_line", StringUtils.defaultString(ticket.getBusinessLine()));
|
||||
out.put("facts", parseFactsJson(ticket.getFactsJson()));
|
||||
out.put("quote", latestQuotePayload(ticket.getWorkOrderNo()));
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 协同结束:在协同字段里打结束标记。不改六态、不清 chat_id(方便群里回话)。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> endCollabGroup(String workOrderNo) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("workOrderNo", workOrderNo);
|
||||
Map<String, String> flag = new LinkedHashMap<>();
|
||||
flag.put("__ended", "1");
|
||||
body.put("facts", flag);
|
||||
Map<String, Object> out = patchCollabFacts(body);
|
||||
out.put("collab_ended", Boolean.TRUE);
|
||||
InquiryTicket ticket = findByWorkOrderNo(workOrderNo);
|
||||
if (ticket != null) {
|
||||
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "end_collab_group", "agent", "协同结束");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 群附件只存元数据。终态拦截在智能体侧,这里不再改六态。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> attachFile(Map<String, Object> body) {
|
||||
String no = text(body, "workOrderNo", "work_order_no");
|
||||
InquiryTicket ticket = findByWorkOrderNo(no);
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
if (ticket == null) {
|
||||
out.put("ok", false);
|
||||
out.put("error", "ticket_not_found");
|
||||
return out;
|
||||
}
|
||||
InquiryAttachment row = new InquiryAttachment();
|
||||
row.setTicketId(ticket.getId());
|
||||
row.setWorkOrderNo(no);
|
||||
row.setFileName(text(body, "fileName", "file_name"));
|
||||
row.setObjectKey(text(body, "objectKey", "object_key"));
|
||||
row.setContentType(text(body, "contentType", "content_type"));
|
||||
row.setCreateTime(new Date());
|
||||
attachmentMapper.insert(row);
|
||||
out.put("ok", true);
|
||||
out.put("work_order_no", no);
|
||||
out.put("file_name", row.getFileName());
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品报价盖 TMS 价:新插一条 inq_quote,主表售价跟着改。
|
||||
* 询价中/协商中可迁已报价;已报价只加版本。终态由智能体先拦。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> upsertProductQuote(Map<String, Object> body) {
|
||||
String no = text(body, "workOrderNo", "work_order_no");
|
||||
InquiryTicket ticket = findByWorkOrderNo(no);
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
if (ticket == null) {
|
||||
out.put("ok", false);
|
||||
out.put("error", "ticket_not_found");
|
||||
return out;
|
||||
}
|
||||
Object raw = body == null ? null : body.get("quote");
|
||||
Map<String, Object> quote = raw instanceof Map ? (Map<String, Object>) raw : new LinkedHashMap<>();
|
||||
Long n = quoteMapper.selectCount(new LambdaQueryWrapper<InquiryQuote>()
|
||||
.eq(InquiryQuote::getWorkOrderNo, no));
|
||||
int ver = n == null ? 1 : n.intValue() + 1;
|
||||
InquiryQuote row = new InquiryQuote();
|
||||
row.setTicketId(ticket.getId());
|
||||
row.setWorkOrderNo(no);
|
||||
row.setQuoteVersion(ver);
|
||||
row.setSource("PRODUCT");
|
||||
row.setSourceLabel(String.valueOf(quote.getOrDefault("source_label", "产品报价")));
|
||||
row.setPriceText(String.valueOf(quote.getOrDefault("total", "")));
|
||||
row.setHighlighted(1);
|
||||
row.setPayloadJson(JSON.toJSONString(quote));
|
||||
row.setCreateTime(new Date());
|
||||
quoteMapper.insert(row);
|
||||
Object total = quote.get("total");
|
||||
Object ccy = quote.get("currency");
|
||||
if (total != null && StringUtils.isNotBlank(String.valueOf(total))) {
|
||||
String sale = (ccy == null ? "" : String.valueOf(ccy) + " ") + total;
|
||||
ticket.setSalePrice(sale.trim());
|
||||
ticket.setUpdateTime(new Date());
|
||||
updateById(ticket);
|
||||
}
|
||||
String to = text(body, "toStatus", "to_status");
|
||||
if (StringUtils.isBlank(to)) {
|
||||
to = TicketStatuses.QUOTED;
|
||||
}
|
||||
if (TicketStatuses.canTransit(ticket.getStatus(), to)) {
|
||||
TicketTransitionRequest tr = new TicketTransitionRequest();
|
||||
tr.setWorkOrderNo(no);
|
||||
tr.setToStatus(to);
|
||||
transition(tr, "agent");
|
||||
}
|
||||
out.put("ok", true);
|
||||
out.put("quote_version", ver);
|
||||
out.put("status", ticket.getStatus());
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseFactsJson(String raw) {
|
||||
Map<String, String> facts = new LinkedHashMap<>();
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
return facts;
|
||||
}
|
||||
JSONObject parsed = JSON.parseObject(raw);
|
||||
if (parsed == null) {
|
||||
return facts;
|
||||
}
|
||||
for (String key : parsed.keySet()) {
|
||||
if (parsed.get(key) != null) {
|
||||
facts.put(key, String.valueOf(parsed.get(key)));
|
||||
}
|
||||
}
|
||||
return facts;
|
||||
}
|
||||
|
||||
private Map<String, Object> latestQuotePayload(String workOrderNo) {
|
||||
Map<String, Object> quote = new LinkedHashMap<>();
|
||||
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
|
||||
.eq(InquiryQuote::getWorkOrderNo, workOrderNo)
|
||||
.orderByDesc(InquiryQuote::getCreateTime)
|
||||
.last("LIMIT 1"));
|
||||
if (quotes != null && !quotes.isEmpty() && StringUtils.isNotBlank(quotes.get(0).getPayloadJson())) {
|
||||
JSONObject parsed = JSON.parseObject(quotes.get(0).getPayloadJson());
|
||||
if (parsed != null) {
|
||||
quote.putAll(parsed);
|
||||
}
|
||||
}
|
||||
return quote;
|
||||
}
|
||||
|
||||
private static String memberIdsJson(Map<String, Object> body) {
|
||||
if (body == null) {
|
||||
return "[]";
|
||||
|
||||
+645
-127
@@ -32,6 +32,45 @@ public final class QuoteTemplateExcelSupport {
|
||||
"FFFFCC00", "FFFFD966", "FFFFF2CC", "FFFFE699"
|
||||
));
|
||||
|
||||
/**
|
||||
* 按标签找格子时用的词典。与合同 labelDictionary 对齐,并补上海运询价单上的「费用总计 / RFQ No」。
|
||||
* 只用于填表兜底,不发明价格。
|
||||
*/
|
||||
private static final Map<String, String> FILL_BY_LABEL = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
FILL_BY_LABEL.put("询价日期", "quote_date");
|
||||
FILL_BY_LABEL.put("报价日期", "quote_date");
|
||||
FILL_BY_LABEL.put("品名", "commodity");
|
||||
FILL_BY_LABEL.put("货物数量", "quantity");
|
||||
FILL_BY_LABEL.put("整柜或拼柜", "fcl_or_lcl");
|
||||
FILL_BY_LABEL.put("箱型箱量", "container_type_qty");
|
||||
FILL_BY_LABEL.put("起运港", "pol");
|
||||
FILL_BY_LABEL.put("目的港", "pod");
|
||||
FILL_BY_LABEL.put("报价有效期", "valid_until");
|
||||
FILL_BY_LABEL.put("有效期至", "valid_until");
|
||||
FILL_BY_LABEL.put("费用总计", "total_amount");
|
||||
FILL_BY_LABEL.put("报价合计", "total_amount");
|
||||
FILL_BY_LABEL.put("RFQ No", "quote_no");
|
||||
FILL_BY_LABEL.put("报价单编号", "quote_no");
|
||||
FILL_BY_LABEL.put("报价编号", "quote_no");
|
||||
FILL_BY_LABEL.put("贸易条款", "trade_terms");
|
||||
FILL_BY_LABEL.put("货好时间", "cargo_ready_date");
|
||||
FILL_BY_LABEL.put("商品海关编码", "hs_code");
|
||||
FILL_BY_LABEL.put("HS编码", "hs_code");
|
||||
FILL_BY_LABEL.put("海关编码", "hs_code");
|
||||
FILL_BY_LABEL.put("是否含油,电,磁", "oil_elec_mag");
|
||||
FILL_BY_LABEL.put("件数", "pieces");
|
||||
FILL_BY_LABEL.put("重量", "weight_kg");
|
||||
FILL_BY_LABEL.put("体积", "volume_cbm");
|
||||
FILL_BY_LABEL.put("始发地", "origin");
|
||||
FILL_BY_LABEL.put("目的地", "destination");
|
||||
FILL_BY_LABEL.put("币别", "currency");
|
||||
FILL_BY_LABEL.put("业务员", "sales_name");
|
||||
FILL_BY_LABEL.put("客户名称", "customer_name");
|
||||
FILL_BY_LABEL.put("联系人", "contact_name");
|
||||
}
|
||||
|
||||
private QuoteTemplateExcelSupport() {
|
||||
}
|
||||
|
||||
@@ -188,6 +227,8 @@ public final class QuoteTemplateExcelSupport {
|
||||
writeCell(wb, cellRef, String.valueOf(val));
|
||||
}
|
||||
}
|
||||
// 海运询价单映射常只对上 4 个点,且起运港映进了标签合并格。按标签再写一遍正确格子。
|
||||
writeFactsByLabel(wb, facts, yellow);
|
||||
writeFeeRows(wb, mapping, facts, yellow);
|
||||
// 后台常未配 fee_rows:按「空运费」等名称写入金额列,SUM 才能加算
|
||||
writeFeeAmountsByName(wb, facts, yellow);
|
||||
@@ -235,7 +276,7 @@ public final class QuoteTemplateExcelSupport {
|
||||
writeFeeCol(sheet, r, columns.getString("currency"), rowObj.getString("currency"), yellow, sheet.getSheetName());
|
||||
}
|
||||
if (columns.containsKey("remark")) {
|
||||
writeFeeCol(sheet, r, columns.getString("remark"), rowObj.getString("remark"), yellow, sheet.getSheetName());
|
||||
writeFeeCol(sheet, r, columns.getString("remark"), formatFeeRemark(rowObj.getString("remark")), yellow, sheet.getSheetName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,6 +299,8 @@ public final class QuoteTemplateExcelSupport {
|
||||
cell = row.createCell(col);
|
||||
}
|
||||
writeNumericOrText(cell, value);
|
||||
// 映射表写出的备注同样要换行,否则只看得见半截
|
||||
applyRemarkWrap(sheet, row, cell, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,9 +345,21 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (cell == null || cell.getCellType() != CellType.FORMULA) {
|
||||
continue;
|
||||
}
|
||||
String formula = cell.getCellFormula();
|
||||
try {
|
||||
ev.evaluateFormulaCell(cell);
|
||||
CellType result = ev.evaluateFormulaCell(cell);
|
||||
// 空的 D*G 会算出错误,海运未用费目行不要留下一个「=」
|
||||
if (result == CellType.ERROR && StringUtils.isNotBlank(formula)) {
|
||||
cell.setCellFormula(formula);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
if (StringUtils.isNotBlank(formula)) {
|
||||
cell.setCellFormula(formula);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 还原失败不挡出单
|
||||
}
|
||||
log.warn("报价公式未重算 {}!{}: {}",
|
||||
sheet.getSheetName(),
|
||||
new CellReference(cell).formatAsString(),
|
||||
@@ -321,7 +376,11 @@ public final class QuoteTemplateExcelSupport {
|
||||
|
||||
/**
|
||||
* 按费用名称把 TMS 费用行写进模板:单位 / 币别 / 单价 / 数量 / 金额 / 备注。
|
||||
* 映射没有 fee_rows 时也能对上「空运费」等已有行。没有的字段不编、不覆盖模板原字。
|
||||
* 映射没有 fee_rows 时也能对上「空运费」等已有行。
|
||||
* <p>
|
||||
* 海运询价单有三段费用表,且「海运费用」先出现在整行标题里。
|
||||
* 必须跳过标题行,列映射取费目上方最近的表头,对不上的费目追加到空行。
|
||||
* 禁止把数量写进「货物数量」标签列,否则会把费目名盖成 1。
|
||||
*/
|
||||
public static void writeFeeAmountsByName(Workbook wb, JSONObject facts, Set<String> yellow) {
|
||||
JSONArray rows = parseFeeRows(facts == null ? null : facts.get("fee_rows"));
|
||||
@@ -329,16 +388,11 @@ public final class QuoteTemplateExcelSupport {
|
||||
return;
|
||||
}
|
||||
String headerCurrency = facts == null ? "" : StringUtils.trimToEmpty(facts.getString("currency"));
|
||||
String boxType = firstFact(facts, "container_type_qty", "箱型箱量");
|
||||
for (int s = 0; s < wb.getNumberOfSheets(); s++) {
|
||||
Sheet sheet = wb.getSheetAt(s);
|
||||
Map<String, Integer> cols = findFeeTableColumns(sheet);
|
||||
if (cols.isEmpty()) {
|
||||
cols = fallbackFeeColumns(sheet);
|
||||
}
|
||||
Integer amountCol = cols.get("amount");
|
||||
if (amountCol == null) {
|
||||
amountCol = 6;
|
||||
}
|
||||
Set<Integer> usedRows = new HashSet<>();
|
||||
List<JSONObject> unmatched = new ArrayList<>();
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
JSONObject rowObj = rows.getJSONObject(i);
|
||||
if (rowObj == null) {
|
||||
@@ -349,68 +403,134 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (StringUtils.isBlank(name) || StringUtils.isBlank(amount) || "-".equals(amount)) {
|
||||
continue;
|
||||
}
|
||||
Cell nameCell = findCellByFeeName(sheet, name);
|
||||
Cell nameCell = findCellByFeeName(sheet, name, usedRows);
|
||||
if (nameCell == null) {
|
||||
unmatched.add(rowObj);
|
||||
continue;
|
||||
}
|
||||
int r = nameCell.getRowIndex();
|
||||
writeAmountCell(sheet, r, amountCol, amount, yellow);
|
||||
String lineCurrency = StringUtils.trimToEmpty(rowObj.getString("currency"));
|
||||
if (StringUtils.isBlank(lineCurrency)) {
|
||||
lineCurrency = headerCurrency;
|
||||
usedRows.add(r);
|
||||
Map<String, Integer> cols = findFeeTableColumnsAbove(sheet, r);
|
||||
writeFeeDetailRow(sheet, r, cols, rowObj, headerCurrency, boxType, yellow, false);
|
||||
}
|
||||
for (JSONObject rowObj : unmatched) {
|
||||
int empty = findEmptyFeeDataRow(sheet, usedRows);
|
||||
if (empty < 0) {
|
||||
log.warn("报价单没有空行可追加费用 {}", rowObj.getString("name"));
|
||||
continue;
|
||||
}
|
||||
writeMappedText(sheet, r, cols.get("currency"), lineCurrency, yellow);
|
||||
writeMappedText(sheet, r, cols.get("unit"), rowObj.getString("unit"), yellow);
|
||||
writeMappedNumber(sheet, r, cols.get("quantity"), rowObj.getString("quantity"), yellow);
|
||||
writeMappedNumber(sheet, r, cols.get("unit_price"), rowObj.getString("unit_price"), yellow);
|
||||
writeMappedText(sheet, r, cols.get("remark"), rowObj.getString("remark"), yellow);
|
||||
usedRows.add(empty);
|
||||
Map<String, Integer> cols = findFeeTableColumnsAbove(sheet, empty);
|
||||
writeFeeDetailRow(sheet, empty, cols, rowObj, headerCurrency, boxType, yellow, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 只认「费用名称 + 金额」那一行表头,避免把货物信息里的件/KG 当成费用列。
|
||||
* 写一行费用。writeName=false 时不碰已有费目名,避免把「文件费/Doc.」盖成数量 1。
|
||||
*/
|
||||
private static void writeFeeDetailRow(Sheet sheet, int r, Map<String, Integer> cols, JSONObject rowObj,
|
||||
String headerCurrency, String boxType, Set<String> yellow, boolean writeName) {
|
||||
if (cols == null || cols.isEmpty()) {
|
||||
cols = fallbackFeeColumns(sheet);
|
||||
}
|
||||
Integer amountCol = cols.get("amount");
|
||||
if (amountCol == null) {
|
||||
amountCol = 7;
|
||||
}
|
||||
Integer nameCol = cols.get("name");
|
||||
Integer qtyCol = cols.get("quantity");
|
||||
// 数量列若和费目名同列,多半是误把「货物数量」当成了费用数量
|
||||
if (qtyCol != null && nameCol != null && qtyCol.equals(nameCol)) {
|
||||
qtyCol = null;
|
||||
}
|
||||
if (writeName) {
|
||||
writeMappedText(sheet, r, nameCol == null ? 0 : nameCol, rowObj.getString("name"), yellow);
|
||||
}
|
||||
String amount = StringUtils.trimToEmpty(rowObj.getString("amount"));
|
||||
writeAmountCell(sheet, r, amountCol, amount, yellow);
|
||||
String lineCurrency = StringUtils.trimToEmpty(rowObj.getString("currency"));
|
||||
if (StringUtils.isBlank(lineCurrency)) {
|
||||
lineCurrency = headerCurrency;
|
||||
}
|
||||
writeMappedText(sheet, r, cols.get("currency"), lineCurrency, yellow);
|
||||
writeMappedText(sheet, r, cols.get("unit"), rowObj.getString("unit"), yellow);
|
||||
writeMappedNumber(sheet, r, qtyCol, rowObj.getString("quantity"), yellow);
|
||||
writeMappedNumber(sheet, r, cols.get("unit_price"), rowObj.getString("unit_price"), yellow);
|
||||
writeMappedText(sheet, r, cols.get("remark"), formatFeeRemark(rowObj.getString("remark")), yellow);
|
||||
Integer boxCol = pickContainerRateColumn(cols, boxType);
|
||||
if (boxCol != null && !boxCol.equals(amountCol)) {
|
||||
String rate = firstNonBlank(rowObj.getString("unit_price"), amount);
|
||||
writeMappedNumber(sheet, r, boxCol, rate, yellow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 只认「费用名称/项目 + 金额」那一行表头,避免把货物信息里的件/KG 当成费用列。
|
||||
* 海运询价单表头是「项目/Items」不是「费用名称」。
|
||||
* 币别和数量中间的空列或 CNY 列,按现网空运模板当成单价。
|
||||
*/
|
||||
static Map<String, Integer> findFeeTableColumns(Sheet sheet) {
|
||||
Map<String, Integer> cols = new LinkedHashMap<>();
|
||||
if (sheet == null) {
|
||||
return cols;
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
int headerRow = -1;
|
||||
int lastCol = 0;
|
||||
int maxRow = Math.min(sheet.getLastRowNum(), 40);
|
||||
int maxRow = Math.min(sheet.getLastRowNum(), 80);
|
||||
for (int r = 0; r <= maxRow; r++) {
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
boolean hasAmount = false;
|
||||
boolean hasFeeName = false;
|
||||
short last = row.getLastCellNum();
|
||||
for (int c = 0; c < last; c++) {
|
||||
String text = readCellText(row.getCell(c));
|
||||
if (text.contains("金额") || text.contains("金額") || "amount".equalsIgnoreCase(text)) {
|
||||
hasAmount = true;
|
||||
}
|
||||
if (text.contains("费用名称") || text.contains("费用名")) {
|
||||
hasFeeName = true;
|
||||
}
|
||||
}
|
||||
if (hasAmount && hasFeeName) {
|
||||
headerRow = r;
|
||||
lastCol = last;
|
||||
break;
|
||||
Map<String, Integer> cols = parseFeeHeaderRow(sheet, r);
|
||||
if (!cols.isEmpty()) {
|
||||
return cols;
|
||||
}
|
||||
}
|
||||
if (headerRow < 0) {
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从费目行往上找最近的费用表头。海运有始发/海运/目的三段,列不能都用第一张表。
|
||||
*/
|
||||
static Map<String, Integer> findFeeTableColumnsAbove(Sheet sheet, int dataRow) {
|
||||
if (sheet == null) {
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
for (int r = Math.max(dataRow - 1, 0); r >= 0; r--) {
|
||||
Map<String, Integer> cols = parseFeeHeaderRow(sheet, r);
|
||||
if (!cols.isEmpty()) {
|
||||
return cols;
|
||||
}
|
||||
}
|
||||
Map<String, Integer> first = findFeeTableColumns(sheet);
|
||||
return first.isEmpty() ? fallbackFeeColumns(sheet) : first;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析一行是否为费用表头,并记下各列。
|
||||
*/
|
||||
static Map<String, Integer> parseFeeHeaderRow(Sheet sheet, int r) {
|
||||
Map<String, Integer> cols = new LinkedHashMap<>();
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) {
|
||||
return cols;
|
||||
}
|
||||
Row row = sheet.getRow(headerRow);
|
||||
for (int c = 0; c < lastCol; c++) {
|
||||
boolean hasAmount = false;
|
||||
boolean hasFeeName = false;
|
||||
short last = row.getLastCellNum();
|
||||
for (int c = 0; c < last; c++) {
|
||||
String text = readCellText(row.getCell(c));
|
||||
if (isAmountHeader(text)) {
|
||||
hasAmount = true;
|
||||
}
|
||||
if (isFeeNameHeader(text)) {
|
||||
hasFeeName = true;
|
||||
}
|
||||
}
|
||||
if (!hasAmount || !hasFeeName) {
|
||||
return cols;
|
||||
}
|
||||
for (int c = 0; c < last; c++) {
|
||||
String text = StringUtils.trimToEmpty(readCellText(row.getCell(c)));
|
||||
if (text.contains("费用名称") || text.contains("费用名")) {
|
||||
if (isFeeNameHeader(text)) {
|
||||
cols.put("name", c);
|
||||
} else if (isContainerTypeHeader(text)) {
|
||||
cols.put(normalizeContainerType(text), c);
|
||||
} else if (text.contains("单位") || text.contains("單位")) {
|
||||
cols.put("unit", c);
|
||||
} else if (text.contains("币别") || text.contains("币種") || text.contains("币种")) {
|
||||
@@ -419,7 +539,7 @@ public final class QuoteTemplateExcelSupport {
|
||||
cols.put("unit_price", c);
|
||||
} else if (text.contains("数量") || text.contains("數量")) {
|
||||
cols.put("quantity", c);
|
||||
} else if (text.contains("金额") || text.contains("金額") || "amount".equalsIgnoreCase(text)) {
|
||||
} else if (isAmountHeader(text)) {
|
||||
cols.put("amount", c);
|
||||
} else if (text.contains("备注") || text.contains("備註")) {
|
||||
cols.put("remark", c);
|
||||
@@ -431,29 +551,25 @@ public final class QuoteTemplateExcelSupport {
|
||||
cols.put("unit_price", c);
|
||||
}
|
||||
}
|
||||
if (!cols.containsKey("name")) {
|
||||
cols.put("name", 0);
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只从费用表头行取列。禁止扫到「货物数量」把数量列认成 A。
|
||||
*/
|
||||
private static Map<String, Integer> fallbackFeeColumns(Sheet sheet) {
|
||||
Map<String, Integer> found = findFeeTableColumns(sheet);
|
||||
if (!found.isEmpty()) {
|
||||
return found;
|
||||
}
|
||||
Map<String, Integer> cols = new LinkedHashMap<>();
|
||||
putCol(cols, "amount", findHeaderColumn(sheet, Arrays.asList("金额", "金額", "amount")));
|
||||
putCol(cols, "currency", findHeaderColumn(sheet, Arrays.asList("币别", "币種", "币种")));
|
||||
putCol(cols, "unit", findHeaderColumn(sheet, Arrays.asList("单位", "單位")));
|
||||
putCol(cols, "quantity", findHeaderColumn(sheet, Arrays.asList("数量", "數量")));
|
||||
putCol(cols, "unit_price", findHeaderColumn(sheet, Arrays.asList("单价", "單價")));
|
||||
putCol(cols, "remark", findHeaderColumn(sheet, Arrays.asList("备注", "備註")));
|
||||
if (!cols.containsKey("amount")) {
|
||||
cols.put("amount", 6);
|
||||
}
|
||||
cols.put("amount", 6);
|
||||
return cols;
|
||||
}
|
||||
|
||||
private static void putCol(Map<String, Integer> cols, String key, int col) {
|
||||
if (col >= 0) {
|
||||
cols.put(key, col);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeMappedText(Sheet sheet, int rowIdx, Integer col, String value, Set<String> yellow) {
|
||||
if (col == null || StringUtils.isBlank(value) || "-".equals(value.trim())) {
|
||||
return;
|
||||
@@ -502,35 +618,17 @@ public final class QuoteTemplateExcelSupport {
|
||||
return new JSONArray();
|
||||
}
|
||||
|
||||
private static int findHeaderColumn(Sheet sheet, List<String> titles) {
|
||||
int maxRow = Math.min(sheet.getLastRowNum(), 40);
|
||||
for (int r = 0; r <= maxRow; r++) {
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
short last = row.getLastCellNum();
|
||||
for (int c = 0; c < last; c++) {
|
||||
Cell cell = row.getCell(c);
|
||||
String text = readCellText(cell);
|
||||
if (StringUtils.isBlank(text)) {
|
||||
continue;
|
||||
}
|
||||
for (String t : titles) {
|
||||
if (text.contains(t)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static Cell findCellByFeeName(Sheet sheet, String name) {
|
||||
/**
|
||||
* 按费目名找数据行。跳过整行标题(「海运费用」先出现在 A29 合并标题里)。
|
||||
* 只认「海运费↔海运费用」这种加「用/费用」的宽松匹配,不对「操作费」误中「码头操作费」。
|
||||
*/
|
||||
private static Cell findCellByFeeName(Sheet sheet, String name, Set<Integer> usedRows) {
|
||||
String want = name.trim();
|
||||
Cell fuzzy = null;
|
||||
int maxRow = Math.min(sheet.getLastRowNum(), 80);
|
||||
for (int r = 0; r <= maxRow; r++) {
|
||||
if (usedRows != null && usedRows.contains(r)) {
|
||||
continue;
|
||||
}
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) {
|
||||
continue;
|
||||
@@ -539,35 +637,39 @@ public final class QuoteTemplateExcelSupport {
|
||||
for (int c = 0; c < last; c++) {
|
||||
Cell cell = row.getCell(c);
|
||||
String text = readCellText(cell);
|
||||
if (StringUtils.isBlank(text)) {
|
||||
if (StringUtils.isBlank(text) || isFeeTableHeader(text) || isSectionTitle(sheet, cell)) {
|
||||
continue;
|
||||
}
|
||||
if (want.equals(text.trim())) {
|
||||
if (feeNameMatches(text, want)) {
|
||||
return cell;
|
||||
}
|
||||
if (fuzzy == null && text.contains(want) && want.length() >= 2) {
|
||||
fuzzy = cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fuzzy;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用明细表头原字,回填系统字段时不得覆盖。
|
||||
* 海运表头带英文「项目/Items」「金额/Ammount」,以及 20GP 箱型列。
|
||||
*/
|
||||
static boolean isFeeTableHeader(String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return false;
|
||||
}
|
||||
String t = text.trim();
|
||||
return "项目".equals(t) || "费用名称".equals(t) || "费用名".equals(t)
|
||||
|| "单位".equals(t) || "單位".equals(t)
|
||||
|| "币别".equals(t) || "币種".equals(t) || "币种".equals(t)
|
||||
|| "单价".equals(t) || "單價".equals(t)
|
||||
|| "数量".equals(t) || "數量".equals(t)
|
||||
|| "金额".equals(t) || "金額".equals(t)
|
||||
|| "备注".equals(t) || "備註".equals(t);
|
||||
if (isContainerTypeHeader(text)) {
|
||||
return true;
|
||||
}
|
||||
String primary = feePrimaryName(text);
|
||||
return "项目".equals(primary) || "费用名称".equals(primary) || "费用名".equals(primary)
|
||||
|| "单位".equals(primary) || "單位".equals(primary)
|
||||
|| "币别".equals(primary) || "币種".equals(primary) || "币种".equals(primary)
|
||||
|| "单价".equals(primary) || "單價".equals(primary)
|
||||
|| "数量".equals(primary) || "數量".equals(primary)
|
||||
|| "金额".equals(primary) || "金額".equals(primary)
|
||||
|| "备注".equals(primary) || "備註".equals(primary)
|
||||
|| "Items".equalsIgnoreCase(primary) || "Cur.".equalsIgnoreCase(primary)
|
||||
|| "Unit".equalsIgnoreCase(primary) || "Qty.".equalsIgnoreCase(primary)
|
||||
|| "Ammount".equalsIgnoreCase(primary) || "Amount".equalsIgnoreCase(primary);
|
||||
}
|
||||
|
||||
private static String readCellText(Cell cell) {
|
||||
@@ -589,8 +691,17 @@ public final class QuoteTemplateExcelSupport {
|
||||
row = sheet.createRow(dest[0]);
|
||||
}
|
||||
Cell cell = row.getCell(dest[1]);
|
||||
CellStyle style = cell == null ? null : cell.getCellStyle();
|
||||
// 海运模板金额格是 =D31*G31。只改缓存的话,后面 refreshFormulas 会按空的 40HQ 列算回 0。
|
||||
if (cell != null && cell.getCellType() == CellType.FORMULA) {
|
||||
row.removeCell(cell);
|
||||
cell = null;
|
||||
}
|
||||
if (cell == null) {
|
||||
cell = row.createCell(dest[1]);
|
||||
if (style != null) {
|
||||
cell.setCellStyle(style);
|
||||
}
|
||||
}
|
||||
String norm = stripMoneyPrefix(amount);
|
||||
try {
|
||||
@@ -628,7 +739,93 @@ public final class QuoteTemplateExcelSupport {
|
||||
if (cell == null) {
|
||||
cell = row.createCell(dest[1]);
|
||||
}
|
||||
cell.setCellValue(value == null ? "" : value);
|
||||
String text = value == null ? "" : value;
|
||||
cell.setCellValue(text);
|
||||
applyRemarkWrap(sheet, row, cell, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 海运备注常是「计价单位:柜;加点系数:0.0000000」。
|
||||
* 模板备注列很窄(约 13 个英文字宽),整句挤一行只能看见半截;
|
||||
* 按分号拆成两行,销售一眼能看完两段。
|
||||
*/
|
||||
static String formatFeeRemark(String raw) {
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
return raw;
|
||||
}
|
||||
String text = raw.trim();
|
||||
if (!text.contains("计价") && !text.contains("加点") && !text.contains("计费")) {
|
||||
return text;
|
||||
}
|
||||
return text.replace(';', '\n').replace(';', '\n').replaceAll("\n+", "\n").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 备注打开换行、顶对齐,并按最长一行把列加宽、按行数加高。
|
||||
* 只加宽备注这类长句所在列,不改其它列,避免整页打印缩放被带跑。
|
||||
*/
|
||||
static void applyRemarkWrap(Sheet sheet, Row row, Cell cell, String text) {
|
||||
if (sheet == null || row == null || cell == null || StringUtils.isBlank(text) || text.length() < 10) {
|
||||
return;
|
||||
}
|
||||
String shown = formatFeeRemark(text);
|
||||
if (!shown.equals(text)) {
|
||||
cell.setCellValue(shown);
|
||||
text = shown;
|
||||
}
|
||||
CellStyle copy = sheet.getWorkbook().createCellStyle();
|
||||
if (cell.getCellStyle() != null) {
|
||||
copy.cloneStyleFrom(cell.getCellStyle());
|
||||
}
|
||||
copy.setWrapText(true);
|
||||
copy.setShrinkToFit(false);
|
||||
copy.setVerticalAlignment(VerticalAlignment.TOP);
|
||||
cell.setCellStyle(copy);
|
||||
boolean feeRemark = text.contains("\n") || text.contains("计价") || text.contains("加点") || text.contains("计费");
|
||||
if (feeRemark) {
|
||||
widenRemarkColumn(sheet, cell.getColumnIndex(), text);
|
||||
}
|
||||
int lines = countWrappedLines(sheet, cell.getColumnIndex(), text);
|
||||
float need = Math.max(row.getHeightInPoints(), Math.min(16f * lines + 6f, 80f));
|
||||
if (row.getHeightInPoints() < need) {
|
||||
row.setHeightInPoints(need);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按最长一行估算列宽。中文按 2 个英文字宽,上限 26,避免把整表撑破。
|
||||
*/
|
||||
static void widenRemarkColumn(Sheet sheet, int col, String text) {
|
||||
if (sheet == null || col < 0 || StringUtils.isBlank(text)) {
|
||||
return;
|
||||
}
|
||||
int longest = 0;
|
||||
for (String part : text.split("\\r?\\n")) {
|
||||
longest = Math.max(longest, displayWidth(part));
|
||||
}
|
||||
int want = Math.min(Math.max(longest + 2, 18), 26);
|
||||
int current = sheet.getColumnWidth(col) / 256;
|
||||
if (current < want) {
|
||||
sheet.setColumnWidth(col, want * 256);
|
||||
}
|
||||
}
|
||||
|
||||
private static int countWrappedLines(Sheet sheet, int col, String text) {
|
||||
int charsPerLine = Math.max(sheet.getColumnWidth(col) / 256, 8);
|
||||
int lines = 0;
|
||||
for (String part : text.split("\\r?\\n", -1)) {
|
||||
int width = Math.max(displayWidth(part), 1);
|
||||
lines += Math.max(1, (width + charsPerLine - 1) / charsPerLine);
|
||||
}
|
||||
return Math.max(lines, 1);
|
||||
}
|
||||
|
||||
private static int displayWidth(String text) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
n += text.charAt(i) > 127 ? 2 : 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static int[] resolveMergeOrigin(Sheet sheet, int row, int col) {
|
||||
@@ -662,13 +859,17 @@ public final class QuoteTemplateExcelSupport {
|
||||
String text = value == null ? "" : value;
|
||||
// 标签格「报价编号:」只换冒号后的值,避免整格被工单号顶掉看起来像残缺
|
||||
String existing = readCellText(cell);
|
||||
// 「币别」右侧常被误映到「单价」表头,不能把 CNY 盖掉单价二字
|
||||
if (isFeeTableHeader(existing)) {
|
||||
// 映射点常落在合并标签格 / 20GP 表头上,不能把原字盖掉
|
||||
if (isFeeTableHeader(existing) || isLabelLike(existing) || isSectionTitle(sheet, cell)) {
|
||||
return;
|
||||
}
|
||||
int colon = Math.max(existing.indexOf(':'), existing.indexOf(':'));
|
||||
if (colon >= 0 && colon == existing.length() - 1) {
|
||||
text = existing + text;
|
||||
if (colon >= 0) {
|
||||
String prefix = existing.substring(0, colon + 1);
|
||||
if (colon == existing.length() - 1 || prefix.toUpperCase(Locale.ROOT).contains("RFQ")
|
||||
|| prefix.contains("编号")) {
|
||||
text = prefix + text;
|
||||
}
|
||||
}
|
||||
cell.setCellValue(text);
|
||||
CellStyle style = cell.getCellStyle();
|
||||
@@ -756,39 +957,137 @@ public final class QuoteTemplateExcelSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签右侧的可填格。海运询价单标签常是 E4:G4 合并,值在 H4:J4;
|
||||
* 不能再取合并区内的 F4,否则会把「起运港」四字盖掉。
|
||||
* 表头「币别」右侧是 20GP,对不上就空着,不要映到箱型列。
|
||||
*/
|
||||
private static String resolveFillTarget(Sheet sheet, Cell labelCell, String sheetName, String text, DataFormatter fmt) {
|
||||
// 同一格「标签:值」
|
||||
int idx = Math.max(text.indexOf(':'), text.indexOf(':'));
|
||||
if (idx >= 0 && idx < text.length() - 1 && StringUtils.isNotBlank(text.substring(idx + 1).trim())) {
|
||||
// 值已在标签格内:映射到本格(生成时会整格重写为纯值,运营需知悉)
|
||||
// RFQ No:样例 / 报价编号:WO 这类:值在本格冒号后
|
||||
return sheetName + "!" + new CellReference(labelCell).formatAsString();
|
||||
}
|
||||
// 右侧相邻非黄空格或任意右侧格
|
||||
int rowIdx = labelCell.getRowIndex();
|
||||
int startCol = labelCell.getColumnIndex() + 1;
|
||||
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
|
||||
org.apache.poi.ss.util.CellRangeAddress range = sheet.getMergedRegion(i);
|
||||
if (range.isInRange(rowIdx, labelCell.getColumnIndex())) {
|
||||
startCol = range.getLastColumn() + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Row row = labelCell.getRow();
|
||||
int nextCol = labelCell.getColumnIndex() + 1;
|
||||
Cell right = row.getCell(nextCol);
|
||||
if (right == null) {
|
||||
right = row.createCell(nextCol);
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
String firstEmpty = null;
|
||||
String wideEmpty = null;
|
||||
int last = Math.max(row.getLastCellNum(), startCol + 10);
|
||||
for (int c = startCol; c <= last; c++) {
|
||||
int[] dest = resolveMergeOrigin(sheet, rowIdx, c);
|
||||
Cell cand = row.getCell(dest[1]);
|
||||
String exist = readCellText(cand);
|
||||
if (isFeeTableHeader(exist) || isLabelLike(exist) || isSectionTitle(sheet, cand)) {
|
||||
continue;
|
||||
}
|
||||
String ref = sheetName + "!" + new CellReference(dest[0], dest[1]).formatAsString();
|
||||
if (StringUtils.isNotBlank(exist)) {
|
||||
// 映射已经写过本字段,不要再跨到右侧「航程时间」那一格
|
||||
if (firstEmpty == null && wideEmpty == null) {
|
||||
return ref;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (firstEmpty == null) {
|
||||
firstEmpty = ref;
|
||||
}
|
||||
if (wideEmpty == null && mergeWidth(sheet, dest[0], dest[1]) >= 2) {
|
||||
wideEmpty = ref;
|
||||
}
|
||||
}
|
||||
return wideEmpty != null ? wideEmpty : firstEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射点不够时,按标签把询价七项 / 合计写到标签右侧空格。
|
||||
* 币别若落在费用表头行则跳过,避免 CNY 盖掉 20GP。
|
||||
*/
|
||||
static void writeFactsByLabel(Workbook wb, JSONObject facts, Set<String> yellow) {
|
||||
if (wb == null || facts == null) {
|
||||
return;
|
||||
}
|
||||
DataFormatter fmt = new DataFormatter();
|
||||
List<String[]> pending = new ArrayList<>();
|
||||
for (int s = 0; s < wb.getNumberOfSheets(); s++) {
|
||||
Sheet sheet = wb.getSheetAt(s);
|
||||
if (sheet == null) {
|
||||
continue;
|
||||
}
|
||||
String sheetName = sheet.getSheetName();
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int r = 0; r <= lastRow; r++) {
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
boolean feeHeader = !parseFeeHeaderRow(sheet, r).isEmpty();
|
||||
short lastCol = row.getLastCellNum();
|
||||
for (int c = 0; c < lastCol; c++) {
|
||||
Cell cell = row.getCell(c);
|
||||
if (cell == null) {
|
||||
continue;
|
||||
}
|
||||
String text = StringUtils.trimToEmpty(fmt.formatCellValue(cell));
|
||||
if (text.isEmpty() || isSectionTitle(sheet, cell) || isFeeTableHeader(text)) {
|
||||
continue;
|
||||
}
|
||||
String code = matchLabel(text.replace(":", ":").trim(), FILL_BY_LABEL);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
continue;
|
||||
}
|
||||
if ("currency".equals(code) && feeHeader) {
|
||||
continue;
|
||||
}
|
||||
String val = firstFact(facts, code, text.replace(":", "").replace(":", "").trim());
|
||||
if (StringUtils.isBlank(val) || "-".equals(val)) {
|
||||
continue;
|
||||
}
|
||||
String fillRef = resolveFillTarget(sheet, cell, sheetName, text, fmt);
|
||||
if (StringUtils.isBlank(fillRef) || (yellow != null && yellow.contains(fillRef))) {
|
||||
continue;
|
||||
}
|
||||
pending.add(new String[]{fillRef, val});
|
||||
}
|
||||
}
|
||||
}
|
||||
// 先扫完再写:POI 遍历行时 createCell 会 ConcurrentModificationException
|
||||
for (String[] one : pending) {
|
||||
writeCell(wb, one[0], one[1]);
|
||||
}
|
||||
return sheetName + "!" + new CellReference(right).formatAsString();
|
||||
}
|
||||
|
||||
private static String matchLabel(String normalized, Map<String, String> dict) {
|
||||
if (dict == null || dict.isEmpty()) {
|
||||
if (dict == null || dict.isEmpty() || StringUtils.isBlank(normalized)) {
|
||||
return null;
|
||||
}
|
||||
String key = normalized;
|
||||
String key = normalized.replace(":", ":").trim();
|
||||
if (dict.containsKey(key)) {
|
||||
return dict.get(key);
|
||||
}
|
||||
// 去冒号再试
|
||||
String noColon = key.replace(":", "").trim();
|
||||
// 「*货物数量/Quantity…」只取斜杠前中文,避免星号和英文把词典对不上
|
||||
String primary = feePrimaryName(key);
|
||||
if (dict.containsKey(primary)) {
|
||||
return dict.get(primary);
|
||||
}
|
||||
String noColon = primary.replace(":", "").trim();
|
||||
for (Map.Entry<String, String> e : dict.entrySet()) {
|
||||
String dk = e.getKey().replace(":", ":").replace(":", "").trim();
|
||||
if (dk.length() < 2 || noColon.length() < 2) {
|
||||
continue;
|
||||
}
|
||||
if (dk.equalsIgnoreCase(noColon) || noColon.startsWith(dk) || dk.startsWith(noColon)) {
|
||||
if (dk.length() >= 2 && noColon.length() >= 2) {
|
||||
return e.getValue();
|
||||
}
|
||||
return e.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -837,6 +1136,225 @@ public final class QuoteTemplateExcelSupport {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 询价单左侧/右侧说明文字。写进去会把「起运港/POL…」盖成港口名,销售看起来像空表。
|
||||
*/
|
||||
static boolean isLabelLike(String text) {
|
||||
if (StringUtils.isBlank(text) || text.length() < 4) {
|
||||
return false;
|
||||
}
|
||||
if (isFeeTableHeader(text) || isContainerTypeHeader(text)) {
|
||||
return false;
|
||||
}
|
||||
String[] markers = {
|
||||
"起运港", "目的港", "询价日期", "品名", "货物数量", "整柜或拼柜", "箱型箱量",
|
||||
"贸易条款", "货好时间", "商品海关编码", "是否含油", "提货地址", "送货地址", "报价有效期", "费用总计",
|
||||
"始发港", "目的地", "Inquiry Date", "Commodity", "Place of Receipt"
|
||||
};
|
||||
for (String m : markers) {
|
||||
if (text.contains(m) && (text.contains("/") || text.contains(":") || text.contains("(") || text.length() >= 6)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 整行分区标题:始发港费用 / 海运费用 / 汇总。模糊匹配「海运费」时必须跳过。
|
||||
*/
|
||||
static boolean isSectionTitle(Sheet sheet, Cell cell) {
|
||||
if (cell == null) {
|
||||
return false;
|
||||
}
|
||||
String text = readCellText(cell);
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return false;
|
||||
}
|
||||
if (text.contains("汇总") || text.contains("免用箱期") || text.contains("Origin Charges")
|
||||
|| text.contains("Destination Charges") || text.contains("Local Charges")) {
|
||||
return true;
|
||||
}
|
||||
int row = cell.getRowIndex();
|
||||
int col = cell.getColumnIndex();
|
||||
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
|
||||
org.apache.poi.ss.util.CellRangeAddress range = sheet.getMergedRegion(i);
|
||||
if (range.isInRange(row, col) && range.getLastColumn() - range.getFirstColumn() >= 6
|
||||
&& (text.contains("费用") || text.toLowerCase(Locale.ROOT).contains("freight")
|
||||
|| text.toLowerCase(Locale.ROOT).contains("charges"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean isAmountHeader(String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return false;
|
||||
}
|
||||
String primary = feePrimaryName(text);
|
||||
return "金额".equals(primary) || "金額".equals(primary)
|
||||
|| "amount".equalsIgnoreCase(primary) || "ammount".equalsIgnoreCase(primary)
|
||||
|| text.contains("金额") || text.contains("金額");
|
||||
}
|
||||
|
||||
static boolean isFeeNameHeader(String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return false;
|
||||
}
|
||||
String primary = feePrimaryName(text);
|
||||
return "费用名称".equals(primary) || "费用名".equals(primary) || "项目".equals(primary)
|
||||
|| text.contains("费用名称") || text.startsWith("项目/") || "项目/Items".equals(text.trim());
|
||||
}
|
||||
|
||||
static boolean isContainerTypeHeader(String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return false;
|
||||
}
|
||||
String t = text.trim().toUpperCase(Locale.ROOT).replace(" ", "");
|
||||
return t.equals("20GP") || t.equals("40GP") || t.equals("40HQ") || t.equals("40HC")
|
||||
|| t.equals("45HQ") || t.equals("40NOR") || t.equals("20HQ");
|
||||
}
|
||||
|
||||
static String normalizeContainerType(String text) {
|
||||
return text == null ? "" : text.trim().toUpperCase(Locale.ROOT).replace(" ", "");
|
||||
}
|
||||
|
||||
static String feePrimaryName(String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
String t = text.trim();
|
||||
int slash = t.indexOf('/');
|
||||
if (slash < 0) {
|
||||
slash = t.indexOf('/');
|
||||
}
|
||||
if (slash > 0) {
|
||||
t = t.substring(0, slash).trim();
|
||||
}
|
||||
if (t.startsWith("*")) {
|
||||
t = t.substring(1).trim();
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 海运费=海运费用;文件费=文件费/Doc.。不对「操作费」误中「码头操作费」。
|
||||
*/
|
||||
static boolean feeNameMatches(String cellText, String want) {
|
||||
if (StringUtils.isBlank(cellText) || StringUtils.isBlank(want)) {
|
||||
return false;
|
||||
}
|
||||
String full = cellText.trim();
|
||||
if (full.equals(want)) {
|
||||
return true;
|
||||
}
|
||||
String primary = feePrimaryName(full);
|
||||
if (primary.equals(want)) {
|
||||
return true;
|
||||
}
|
||||
return primary.equals(want + "用") || primary.equals(want + "费用")
|
||||
|| want.equals(primary + "用") || want.equals(primary + "费用");
|
||||
}
|
||||
|
||||
static Integer pickContainerRateColumn(Map<String, Integer> cols, String boxType) {
|
||||
if (cols == null || StringUtils.isBlank(boxType)) {
|
||||
return null;
|
||||
}
|
||||
String box = boxType.toUpperCase(Locale.ROOT).replace(" ", "");
|
||||
String[] keys = {"45HQ", "40HQ", "40HC", "40GP", "20GP", "20HQ"};
|
||||
for (String key : keys) {
|
||||
if (box.contains(key) && cols.containsKey(key)) {
|
||||
return cols.get(key);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用表里费目名为空的数据行,用来追加 TMS 有、模板没预印的费目。
|
||||
*/
|
||||
static int findEmptyFeeDataRow(Sheet sheet, Set<Integer> usedRows) {
|
||||
if (sheet == null) {
|
||||
return -1;
|
||||
}
|
||||
int maxRow = Math.min(sheet.getLastRowNum() + 8, 80);
|
||||
List<Integer> headers = new ArrayList<>();
|
||||
for (int r = 0; r <= maxRow; r++) {
|
||||
if (!parseFeeHeaderRow(sheet, r).isEmpty()) {
|
||||
headers.add(r);
|
||||
}
|
||||
}
|
||||
for (Integer header : headers) {
|
||||
Map<String, Integer> cols = parseFeeHeaderRow(sheet, header);
|
||||
int nameCol = cols.getOrDefault("name", 0);
|
||||
for (int r = header + 1; r <= maxRow; r++) {
|
||||
if (!parseFeeHeaderRow(sheet, r).isEmpty()) {
|
||||
break;
|
||||
}
|
||||
Row row = sheet.getRow(r);
|
||||
Cell first = row == null ? null : row.getCell(nameCol);
|
||||
if (isSectionTitle(sheet, first)) {
|
||||
break;
|
||||
}
|
||||
if (usedRows != null && usedRows.contains(r)) {
|
||||
continue;
|
||||
}
|
||||
String name = readCellText(first);
|
||||
if (StringUtils.isBlank(name)) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int mergeWidth(Sheet sheet, int row, int col) {
|
||||
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
|
||||
org.apache.poi.ss.util.CellRangeAddress range = sheet.getMergedRegion(i);
|
||||
if (range.isInRange(row, col)) {
|
||||
return range.getLastColumn() - range.getFirstColumn() + 1;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static String firstFact(JSONObject facts, String... keys) {
|
||||
if (facts == null) {
|
||||
return "";
|
||||
}
|
||||
for (String k : keys) {
|
||||
Object v = facts.get(k);
|
||||
if (v != null && StringUtils.isNotBlank(String.valueOf(v))) {
|
||||
return String.valueOf(v).trim();
|
||||
}
|
||||
}
|
||||
// 只传了「货好时间」时,按标签词典反查 cargo_ready_date
|
||||
if (keys != null && keys.length > 0) {
|
||||
String code = keys[0];
|
||||
for (Map.Entry<String, String> e : FILL_BY_LABEL.entrySet()) {
|
||||
if (code.equals(e.getValue())) {
|
||||
Object v = facts.get(e.getKey());
|
||||
if (v != null && StringUtils.isNotBlank(String.valueOf(v))) {
|
||||
return String.valueOf(v).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static String firstNonBlank(String... vals) {
|
||||
if (vals == null) {
|
||||
return "";
|
||||
}
|
||||
for (String v : vals) {
|
||||
if (StringUtils.isNotBlank(v) && !"-".equals(v.trim())) {
|
||||
return v.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** sheet!A1 解析。 */
|
||||
private static final class ParsedRef {
|
||||
final String sheet;
|
||||
|
||||
+5
-1
@@ -95,7 +95,11 @@
|
||||
"重量": "weight_kg",
|
||||
"重 量": "weight_kg",
|
||||
"体积": "volume_cbm",
|
||||
"体 积": "volume_cbm"
|
||||
"体 积": "volume_cbm",
|
||||
"费用总计": "total_amount",
|
||||
"报价合计": "total_amount",
|
||||
"RFQ No": "quote_no",
|
||||
"询价单号": "quote_no"
|
||||
},
|
||||
"samplePreviewFacts": {
|
||||
"customer_name": "ABC Trading Co.",
|
||||
|
||||
+259
@@ -122,6 +122,46 @@ public class QuoteTemplateFillTest {
|
||||
assertEquals(1333.6, row.getCell(5).getNumericCellValue(), 0.01);
|
||||
assertEquals(14669.60, row.getCell(6).getNumericCellValue(), 0.01);
|
||||
assertEquals("计费重进位+1000KG", row.getCell(7).getStringCellValue());
|
||||
assertTrue(row.getCell(7).getCellStyle().getWrapText());
|
||||
assertTrue(row.getHeightInPoints() >= 28f);
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaFeeRemarkSplitsAndWidensColumn() throws Exception {
|
||||
Path src = Files.createTempFile("quote-remark-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-remark-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("海运询价单");
|
||||
sheet.setColumnWidth(8, 13 * 256);
|
||||
org.apache.poi.ss.usermodel.Row head = sheet.createRow(17);
|
||||
head.createCell(0).setCellValue("项目");
|
||||
head.createCell(7).setCellValue("金额");
|
||||
head.createCell(8).setCellValue("备注");
|
||||
sheet.createRow(18).createCell(0).setCellValue("燃油附加费");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject();
|
||||
mapping.put("fields", new JSONObject());
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("fee_rows", QuoteTemplateExcelSupport.parseFeeRows(
|
||||
"[{\"name\":\"燃油附加费\",\"amount\":\"15\",\"remark\":\"计价单位:柜;加点系数:0.0000000\"}]"));
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook wb = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = wb.getSheetAt(0);
|
||||
org.apache.poi.ss.usermodel.Row row = sheet.getRow(18);
|
||||
String remark = row.getCell(8).getStringCellValue();
|
||||
assertTrue(remark.contains("计价单位:柜"));
|
||||
assertTrue(remark.contains("加点系数:0.0000000"));
|
||||
assertTrue(remark.contains("\n"));
|
||||
assertTrue(row.getCell(8).getCellStyle().getWrapText());
|
||||
assertTrue(sheet.getColumnWidth(8) >= 18 * 256);
|
||||
assertTrue(row.getHeightInPoints() >= 30f);
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
@@ -244,4 +284,223 @@ public class QuoteTemplateFillTest {
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildFillFactsProjectsSeaSevenFields() {
|
||||
Map<String, Object> facts = new HashMap<>();
|
||||
facts.put("起运港", "DONGGUAN");
|
||||
facts.put("目的港", "海防");
|
||||
facts.put("品名", "机器设备");
|
||||
facts.put("货量", "15件");
|
||||
facts.put("整柜或拼柜", "整柜");
|
||||
facts.put("箱型箱量", "20GP*1");
|
||||
facts.put("报价日期", "2026-09-15");
|
||||
Map<String, Object> quote = new HashMap<>();
|
||||
quote.put("total", "2287.65");
|
||||
quote.put("currency", "CNY");
|
||||
quote.put("fee_lines",
|
||||
"[{\"name\":\"海运费\",\"amount\":\"100\",\"currency\":\"USD\",\"quantity\":\"1\",\"unit\":\"柜\"},"
|
||||
+ "{\"name\":\"文件费\",\"amount\":\"450\",\"currency\":\"CNY\",\"quantity\":\"1\",\"unit\":\"票\"}]");
|
||||
JSONObject out = org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl
|
||||
.buildFillFacts("WO202609150032", facts, quote, "WuJiLin");
|
||||
assertEquals("DONGGUAN", out.getString("pol"));
|
||||
assertEquals("海防", out.getString("pod"));
|
||||
assertEquals("机器设备", out.getString("commodity"));
|
||||
assertEquals("15件", out.getString("quantity"));
|
||||
assertEquals("整柜", out.getString("fcl_or_lcl"));
|
||||
assertEquals("20GP*1", out.getString("container_type_qty"));
|
||||
assertEquals("2287.65", out.getString("total_amount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildFillFactsProjectsCollabFields() {
|
||||
Map<String, Object> facts = new HashMap<>();
|
||||
facts.put("起运港", "DONGGUAN");
|
||||
facts.put("货好时间", "随时可提");
|
||||
facts.put("是否含油", "否");
|
||||
facts.put("是否含电", "否");
|
||||
facts.put("HS编码", "8479");
|
||||
JSONObject out = org.jeecg.modules.inquiry.service.impl.InquiryQuoteTemplateServiceImpl
|
||||
.buildFillFacts("WO202609150041", facts, new HashMap<>(), "WuJiLin");
|
||||
assertEquals("随时可提", out.getString("cargo_ready_date"));
|
||||
assertEquals("8479", out.getString("hs_code"));
|
||||
assertTrue(out.getString("oil_elec_mag").contains("油:否"));
|
||||
assertTrue(out.getString("oil_elec_mag").contains("电:否"));
|
||||
assertEquals(2, out.getJSONArray("fee_rows").size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 海运询价单:起运港标签是 E4:G4 合并,值在 H4:J4。
|
||||
* 旧映射把 pol 指到 F4,会把标签盖成港口名;费用数量还会误中「货物数量」列把费目名改成 1。
|
||||
* 出单必须:标签还在、港口/货量写在值格、TMS 金额进金额列。
|
||||
*/
|
||||
@Test
|
||||
public void seaInquiryFormKeepsLabelsAndWritesTmsFees() throws Exception {
|
||||
Path src = Files.createTempFile("quote-sea-src", ".xlsx");
|
||||
Path out = Files.createTempFile("quote-sea-out", ".xlsx");
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("海运询价单");
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(3, 3, 1, 3));
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(3, 3, 4, 6));
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(3, 3, 7, 9));
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 1, 3));
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 4, 6));
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(4, 4, 7, 9));
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(5, 5, 1, 3));
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(7, 7, 1, 3));
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(28, 28, 0, 9));
|
||||
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(43, 43, 2, 9));
|
||||
row(sheet, 3).createCell(0).setCellValue("*询价日期/Inquiry Date:");
|
||||
row(sheet, 3).createCell(4).setCellValue("起运港/POL(有指定起运港口的必填)");
|
||||
row(sheet, 4).createCell(0).setCellValue("*品名/Commodity:");
|
||||
row(sheet, 4).createCell(4).setCellValue("目的港/POD(有指定目的港口的必填)");
|
||||
row(sheet, 5).createCell(0).setCellValue("*货物数量/Quantity (或提供装箱单/Packing List)");
|
||||
row(sheet, 6).createCell(0).setCellValue("*整柜或拼柜/FCL or LCL:");
|
||||
row(sheet, 7).createCell(0).setCellValue("*箱型箱量/Type & Qty of Cont.:");
|
||||
row(sheet, 17).createCell(0).setCellValue("项目/Items");
|
||||
row(sheet, 17).createCell(1).setCellValue("币别/Cur.");
|
||||
row(sheet, 17).createCell(2).setCellValue("20GP");
|
||||
row(sheet, 17).createCell(7).setCellValue("金额/Ammount");
|
||||
row(sheet, 28).createCell(0).setCellValue("海运费用/Ocean Freight");
|
||||
org.apache.poi.ss.usermodel.Row oceanHead = row(sheet, 29);
|
||||
oceanHead.createCell(0).setCellValue("项目/Items");
|
||||
oceanHead.createCell(1).setCellValue("币别/Cur.");
|
||||
oceanHead.createCell(2).setCellValue("20GP");
|
||||
oceanHead.createCell(7).setCellValue("金额/Ammount");
|
||||
row(sheet, 30).createCell(0).setCellValue("海运费用/Ocean Freight");
|
||||
row(sheet, 30).createCell(7).setCellFormula("D31*G31");
|
||||
org.apache.poi.ss.usermodel.Row destHead = row(sheet, 34);
|
||||
destHead.createCell(0).setCellValue("项目/Items");
|
||||
destHead.createCell(1).setCellValue("币别/Cur.");
|
||||
destHead.createCell(7).setCellValue("金额/Ammount");
|
||||
row(sheet, 35).createCell(0).setCellValue("文件费/Doc.");
|
||||
row(sheet, 43).createCell(0).setCellValue("费用总计/Total Costs");
|
||||
try (java.io.OutputStream os = Files.newOutputStream(src)) {
|
||||
wb.write(os);
|
||||
}
|
||||
}
|
||||
JSONObject mapping = new JSONObject();
|
||||
JSONObject fields = new JSONObject();
|
||||
fields.put("pol", "海运询价单!F4");
|
||||
fields.put("pod", "海运询价单!F5");
|
||||
fields.put("currency", "海运询价单!C18");
|
||||
mapping.put("fields", fields);
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("pol", "DONGGUAN");
|
||||
facts.put("pod", "海防");
|
||||
facts.put("commodity", "机器设备");
|
||||
facts.put("quantity", "15件");
|
||||
facts.put("fcl_or_lcl", "整柜");
|
||||
facts.put("container_type_qty", "20GP*1");
|
||||
facts.put("currency", "CNY");
|
||||
facts.put("total_amount", "2287.65");
|
||||
facts.put("fee_rows", QuoteTemplateExcelSupport.parseFeeRows(
|
||||
"[{\"name\":\"海运费\",\"amount\":\"100\",\"currency\":\"USD\",\"quantity\":\"1\",\"unit\":\"柜\"},"
|
||||
+ "{\"name\":\"文件费\",\"amount\":\"450\",\"currency\":\"CNY\",\"quantity\":\"1\",\"unit\":\"票\"},"
|
||||
+ "{\"name\":\"燃油附加费\",\"amount\":\"15\",\"currency\":\"USD\",\"quantity\":\"1\",\"unit\":\"柜\"}]"));
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = filled.getSheetAt(0);
|
||||
assertTrue(read(sheet, 3, 4).contains("起运港"));
|
||||
assertEquals("DONGGUAN", read(sheet, 3, 7));
|
||||
assertTrue(read(sheet, 4, 4).contains("目的港"));
|
||||
assertEquals("海防", read(sheet, 4, 7));
|
||||
assertEquals("机器设备", read(sheet, 4, 1));
|
||||
assertEquals("15件", read(sheet, 5, 1));
|
||||
assertEquals("整柜", read(sheet, 6, 1));
|
||||
assertEquals("20GP*1", read(sheet, 7, 1));
|
||||
assertEquals("20GP", read(sheet, 17, 2));
|
||||
assertTrue(read(sheet, 28, 0).contains("海运费用"));
|
||||
assertTrue(read(sheet, 30, 0).contains("海运费用"));
|
||||
assertEquals(CellType.NUMERIC, sheet.getRow(30).getCell(7).getCellType());
|
||||
assertEquals(100.0, sheet.getRow(30).getCell(7).getNumericCellValue(), 0.01);
|
||||
assertEquals("USD", read(sheet, 30, 1));
|
||||
assertTrue(read(sheet, 35, 0).contains("文件费"));
|
||||
assertEquals(450.0, sheet.getRow(35).getCell(7).getNumericCellValue(), 0.01);
|
||||
assertEquals("燃油附加费", read(sheet, 18, 0));
|
||||
assertEquals(15.0, sheet.getRow(18).getCell(7).getNumericCellValue(), 0.01);
|
||||
assertEquals(2287.65, sheet.getRow(43).getCell(2).getNumericCellValue(), 0.01);
|
||||
} finally {
|
||||
Files.deleteIfExists(src);
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realSeaTemplateWritesPortsAndFees() throws Exception {
|
||||
Path src = java.nio.file.Paths.get("d:/Desktop/友通达询价智能体重制版/deploy/sql/sea_tpl.xlsx");
|
||||
if (!Files.isRegularFile(src)) {
|
||||
return;
|
||||
}
|
||||
Path out = Files.createTempFile("quote-sea-real", ".xlsx");
|
||||
JSONObject mapping = new JSONObject();
|
||||
JSONObject fields = new JSONObject();
|
||||
fields.put("pol", "海运询价单!F4");
|
||||
fields.put("pod", "海运询价单!F5");
|
||||
fields.put("currency", "海运询价单!C18");
|
||||
fields.put("valid_until", "海运询价单!B16");
|
||||
mapping.put("fields", fields);
|
||||
JSONObject facts = new JSONObject(true);
|
||||
facts.put("pol", "DONGGUAN");
|
||||
facts.put("pod", "海防");
|
||||
facts.put("commodity", "机器设备");
|
||||
facts.put("quantity", "15件");
|
||||
facts.put("fcl_or_lcl", "整柜");
|
||||
facts.put("container_type_qty", "20GP*1");
|
||||
facts.put("quote_date", "2026-09-15");
|
||||
facts.put("valid_until", "2026-09-30");
|
||||
facts.put("currency", "CNY");
|
||||
facts.put("total_amount", "2287.65");
|
||||
facts.put("fee_rows", QuoteTemplateExcelSupport.parseFeeRows(
|
||||
"[{\"name\":\"海运费\",\"amount\":\"100.00\",\"currency\":\"USD\",\"quantity\":\"1\",\"unit\":\"柜\"},"
|
||||
+ "{\"name\":\"燃油附加费\",\"amount\":\"15.00\",\"currency\":\"USD\",\"quantity\":\"1\",\"unit\":\"柜\"},"
|
||||
+ "{\"name\":\"操作费\",\"amount\":\"680.00\",\"currency\":\"CNY\",\"quantity\":\"1\",\"unit\":\"票\"},"
|
||||
+ "{\"name\":\"文件费\",\"amount\":\"450.00\",\"currency\":\"CNY\",\"quantity\":\"1\",\"unit\":\"票\"},"
|
||||
+ "{\"name\":\"封条费\",\"amount\":\"50.00\",\"currency\":\"CNY\",\"quantity\":\"1\",\"unit\":\"柜\"},"
|
||||
+ "{\"name\":\"设备费\",\"amount\":\"50.00\",\"currency\":\"CNY\",\"quantity\":\"1\",\"unit\":\"柜\"},"
|
||||
+ "{\"name\":\"报关费\",\"amount\":\"200.00\",\"currency\":\"CNY\",\"quantity\":\"1\",\"unit\":\"票\"}]"));
|
||||
QuoteTemplateExcelSupport.fillWorkbook(src, out, mapping, facts);
|
||||
try (Workbook filled = new XSSFWorkbook(Files.newInputStream(out))) {
|
||||
Sheet sheet = filled.getSheet("海运询价单");
|
||||
assertTrue(read(sheet, 3, 4).contains("起运港"));
|
||||
assertEquals("DONGGUAN", read(sheet, 3, 7));
|
||||
assertTrue(read(sheet, 4, 4).contains("目的港"));
|
||||
assertEquals("海防", read(sheet, 4, 7));
|
||||
assertEquals("机器设备", read(sheet, 4, 1));
|
||||
assertEquals("15件", read(sheet, 5, 1));
|
||||
assertEquals("整柜", read(sheet, 6, 1));
|
||||
assertEquals("20GP*1", read(sheet, 7, 1));
|
||||
assertEquals("20GP", read(sheet, 17, 2));
|
||||
assertTrue(read(sheet, 28, 0).contains("海运费用"));
|
||||
assertTrue(read(sheet, 30, 0).contains("海运费用"));
|
||||
assertEquals(CellType.NUMERIC, sheet.getRow(30).getCell(7).getCellType());
|
||||
assertEquals(100.0, sheet.getRow(30).getCell(7).getNumericCellValue(), 0.01);
|
||||
assertTrue(read(sheet, 35, 0).contains("文件费"));
|
||||
assertEquals(CellType.NUMERIC, sheet.getRow(35).getCell(7).getCellType());
|
||||
assertEquals("", read(sheet, 15, 7));
|
||||
assertEquals(450.0, sheet.getRow(35).getCell(7).getNumericCellValue(), 0.01);
|
||||
assertEquals("操作费", read(sheet, 18, 0));
|
||||
assertEquals(680.0, sheet.getRow(18).getCell(7).getNumericCellValue(), 0.01);
|
||||
assertEquals(2287.65, sheet.getRow(43).getCell(2).getNumericCellValue(), 0.01);
|
||||
} finally {
|
||||
Files.deleteIfExists(out);
|
||||
}
|
||||
}
|
||||
|
||||
private static org.apache.poi.ss.usermodel.Row row(Sheet sheet, int r) {
|
||||
org.apache.poi.ss.usermodel.Row exist = sheet.getRow(r);
|
||||
return exist == null ? sheet.createRow(r) : exist;
|
||||
}
|
||||
|
||||
private static String read(Sheet sheet, int r, int c) {
|
||||
org.apache.poi.ss.usermodel.Row row = sheet.getRow(r);
|
||||
if (row == null || row.getCell(c) == null) {
|
||||
return "";
|
||||
}
|
||||
org.apache.poi.ss.usermodel.Cell cell = row.getCell(c);
|
||||
if (cell.getCellType() == CellType.NUMERIC) {
|
||||
return String.valueOf(cell.getNumericCellValue());
|
||||
}
|
||||
return cell.getStringCellValue();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user