落地销售调价:按自然语言改价并单独记销售价,拉群和后台不再把调后价当成 TMS 原价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-20 14:40:20 +08:00
co-authored by Cursor
parent e4743541f7
commit 26dd5d72a1
36 changed files with 2795 additions and 176 deletions
@@ -1,6 +1,7 @@
package org.jeecg.modules.inquiry.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
@@ -14,6 +15,7 @@ import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 询价工单实体,对应表 inq_ticket。
@@ -88,6 +90,14 @@ public class InquiryTicket implements Serializable {
@ApiModelProperty("六态")
private String status;
/**
* 列表「报价信息」:本单全部报价行。不入库,enrich 时填。
* TMS / 手工 / 销售调价各留一行,禁止只用最新一条冒充 TMS。
*/
@TableField(exist = false)
@ApiModelProperty("报价信息(展示)")
private List<InquiryQuote> quotes;
@ApiModelProperty("成本价展示")
private String costPrice;
@@ -65,6 +65,7 @@ public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
/**
* 智能体续办:按工单号取字段/报价/推断停点。只读。
* quote 是当前生效价(可能已是销售调价);tms_quote 永远是 TMS 原价,拉群摘要必须用这份。
* 必须校验 salesWecomId 与工单发起人一致。
*/
Map<String, Object> getForAgent(String workOrderNo, String salesWecomId);
@@ -101,7 +102,7 @@ public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
Map<String, Object> attachFile(Map<String, Object> body);
/**
* 产品报价写入新版本并视情况迁「已报价」。
* 手工报价 / 销售调价写入新版本。销售调价另记一条状态流转备注。
*/
Map<String, Object> upsertProductQuote(Map<String, Object> body);
@@ -23,7 +23,9 @@ import org.jeecg.modules.inquiry.mapper.InquiryStaffRoleMapper;
import org.jeecg.modules.inquiry.mapper.InquiryTicketMapper;
import org.jeecg.modules.inquiry.mapper.InquiryTicketStatusLogMapper;
import org.jeecg.modules.inquiry.service.IInquiryWorkOrderService;
import org.jeecg.modules.inquiry.support.InquiryQuoteDisplaySupport;
import org.jeecg.modules.inquiry.support.InquiryTicketDisplaySupport;
import org.jeecg.modules.inquiry.support.InquiryTicketPriceSupport;
import org.jeecg.modules.inquiry.tms.InquiryTmsQueryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -47,7 +49,7 @@ import java.util.Set;
* 询价工单主账实现。
* <p>
* 副作用:改 inq_ticket.status、写 inq_ticket_status_log、改 inq_exception_case。
* 列表/详情会就地补展示字段(salesName、demandTitle/Detail),只影响响应,不写回库。
* 列表/详情会就地补展示字段(salesName、需求摘要、成本价/销售价/利润点),只影响响应,不写回库。
* 线程:单请求事务;只锁当前工单行(DB 行级),不串行其它工单。
* 为何:Graph/前端成功不能冒充主账六态成功,所有 transition 必须过本服务。
*/
@@ -127,6 +129,14 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
.eq(InquiryQuote::getWorkOrderNo, workOrderNo)
.orderByAsc(InquiryQuote::getCreateTime));
if (quotes != null) {
for (InquiryQuote q : quotes) {
if (q == null) {
continue;
}
q.setSourceLabel(InquiryQuoteDisplaySupport.displayLabel(q.getSource(), q.getSourceLabel()));
}
}
List<InquiryAttachment> attachments = attachmentMapper.selectList(new LambdaQueryWrapper<InquiryAttachment>()
.eq(InquiryAttachment::getWorkOrderNo, workOrderNo)
.orderByAsc(InquiryAttachment::getCreateTime));
@@ -147,7 +157,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
/**
* 列表/详情响应补齐:发起人姓名(员工表)+ 询价需求摘要(结构化字段)。
* 列表/详情响应补齐:发起人姓名、询价需求摘要、成本价/销售价/利润点、报价信息行。
* 副作用:只改内存中的 ticket 展示字段,不 updateById。
*/
private void enrichDisplayFields(List<InquiryTicket> tickets) {
@@ -155,6 +165,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
return;
}
Map<String, String> staffNameByWecom = loadStaffNames(tickets);
Map<String, List<InquiryQuote>> quotesByNo = loadQuotesByWorkOrder(tickets);
for (InquiryTicket ticket : tickets) {
if (ticket == null) {
continue;
@@ -163,9 +174,47 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
ticket.setSalesName(InquiryTicketDisplaySupport.resolveSalesDisplayName(
ticket.getSalesName(), ticket.getSalesWecomId(), staffName));
InquiryTicketDisplaySupport.applyRequirementSummary(ticket);
List<InquiryQuote> quoteRows = quotesByNo.get(ticket.getWorkOrderNo());
InquiryTicketPriceSupport.apply(ticket, quoteRows);
if (quoteRows != null) {
for (InquiryQuote q : quoteRows) {
if (q == null) {
continue;
}
q.setSourceLabel(InquiryQuoteDisplaySupport.displayLabel(q.getSource(), q.getSourceLabel()));
}
}
ticket.setQuotes(quoteRows);
}
}
/** 列表一次取出本页工单的报价记录,避免按单 N+1。 */
private Map<String, List<InquiryQuote>> loadQuotesByWorkOrder(List<InquiryTicket> tickets) {
List<String> nos = new ArrayList<>();
for (InquiryTicket ticket : tickets) {
if (ticket != null && StringUtils.isNotBlank(ticket.getWorkOrderNo())) {
nos.add(ticket.getWorkOrderNo());
}
}
if (nos.isEmpty()) {
return Collections.emptyMap();
}
List<InquiryQuote> rows = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
.in(InquiryQuote::getWorkOrderNo, nos)
.orderByAsc(InquiryQuote::getCreateTime));
Map<String, List<InquiryQuote>> out = new HashMap<>();
if (rows == null) {
return out;
}
for (InquiryQuote row : rows) {
if (row == null || StringUtils.isBlank(row.getWorkOrderNo())) {
continue;
}
out.computeIfAbsent(row.getWorkOrderNo(), k -> new ArrayList<>()).add(row);
}
return out;
}
/** 批量按企微 userid 取员工姓名,避免列表 N+1。 */
private Map<String, String> loadStaffNames(List<InquiryTicket> tickets) {
Set<String> wecomIds = new HashSet<>();
@@ -513,6 +562,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
/**
* 智能体续办读单:只读。不属于该销售则 owned=false,不回字段。
* quote=当前生效价;tms_quote=TMS 原价。调价后二者不能混用。
*/
@Override
public Map<String, Object> getForAgent(String workOrderNo, String salesWecomId) {
@@ -572,15 +622,30 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
}
}
// 最新一条=当前生效价;TMS 原价单独留,拉群摘要禁止用调后价冒充 TMS。
Map<String, Object> quote = new LinkedHashMap<>();
Map<String, Object> tmsQuote = new LinkedHashMap<>();
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
.eq(InquiryQuote::getWorkOrderNo, no)
.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);
.orderByDesc(InquiryQuote::getCreateTime));
if (quotes != null) {
for (InquiryQuote row : quotes) {
if (row == null || StringUtils.isBlank(row.getPayloadJson())) {
continue;
}
JSONObject parsed = JSON.parseObject(row.getPayloadJson());
if (parsed == null) {
continue;
}
if (quote.isEmpty()) {
quote.putAll(parsed);
}
if (tmsQuote.isEmpty()
&& InquiryQuoteDisplaySupport.isTms(row.getSource(), row.getSourceLabel())) {
tmsQuote.putAll(parsed);
tmsQuote.putIfAbsent("source", "TMS");
tmsQuote.putIfAbsent("source_label", InquiryQuoteDisplaySupport.TMS_LABEL);
}
}
}
String status = StringUtils.defaultString(ticket.getStatus());
@@ -611,6 +676,8 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
out.put("salesName", StringUtils.defaultString(ticket.getSalesName()));
out.put("facts", facts);
out.put("quote", quote);
out.put("tms_quote", tmsQuote);
out.put("tmsQuote", tmsQuote);
out.put("wait_phase", waitPhase);
out.put("lost_reason", StringUtils.defaultString(ticket.getLostReason()));
out.put("lostReason", StringUtils.defaultString(ticket.getLostReason()));
@@ -853,8 +920,8 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
/**
* 产品报价盖 TMS 价:新插一条 inq_quote,主表售价跟着改。
* 询价中/协商中可迁已报价;已报价只加版本。终态由智能体先拦。
* 手工报价 / 销售调价:新插一条 inq_quote,来源固定为手工报价,不覆盖 TMS 那一行。
* 销售调价必须再写一条状态流转备注(当时时间点)。询价中/协商中可迁已报价。
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -870,6 +937,10 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
Object raw = body == null ? null : body.get("quote");
Map<String, Object> quote = raw instanceof Map ? (Map<String, Object>) raw : new LinkedHashMap<>();
String remark = text(body, "remark");
String kind = text(body, "kind", "quoteKind");
boolean salesAdjust = InquiryQuoteDisplaySupport.isSalesAdjust(kind, remark)
|| "adjust".equalsIgnoreCase(kind);
Long n = quoteMapper.selectCount(new LambdaQueryWrapper<InquiryQuote>()
.eq(InquiryQuote::getWorkOrderNo, no));
int ver = n == null ? 1 : n.intValue() + 1;
@@ -877,21 +948,29 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
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.setSource(salesAdjust ? "SALES_ADJUST" : "PRODUCT");
row.setSourceLabel(salesAdjust ? InquiryQuoteDisplaySupport.ADJUST_LABEL : InquiryQuoteDisplaySupport.MANUAL_LABEL);
Object total = quote.get("total");
Object ccy = quote.get("currency");
String sale = "";
if (total != null && StringUtils.isNotBlank(String.valueOf(total))) {
sale = (ccy == null ? "" : String.valueOf(ccy) + " ") + total;
row.setPriceText(sale.trim());
} else {
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);
}
ticket.setManualQuote(1);
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
.eq(InquiryQuote::getWorkOrderNo, no)
.orderByAsc(InquiryQuote::getCreateTime));
InquiryTicketPriceSupport.apply(ticket, quotes);
ticket.setUpdateTime(new Date());
updateById(ticket);
String from = ticket.getStatus();
String to = text(body, "toStatus", "to_status");
if (StringUtils.isBlank(to)) {
to = TicketStatuses.QUOTED;
@@ -901,6 +980,11 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
tr.setWorkOrderNo(no);
tr.setToStatus(to);
transition(tr, "agent");
ticket = findByWorkOrderNo(no);
}
if (salesAdjust) {
String note = StringUtils.isNotBlank(remark) ? remark : "销售调价";
appendLog(ticket, from, ticket.getStatus(), InquiryQuoteDisplaySupport.ACTION_ADJUST, "agent", note);
}
out.put("ok", true);
out.put("quote_version", ver);
@@ -0,0 +1,56 @@
package org.jeecg.modules.inquiry.support;
import org.apache.commons.lang3.StringUtils;
/**
* 后台报价来源展示:TMS 标准报价 / 手工报价 / 销售调价。
* <p>
* 职责:根据 source / sourceLabel / 备注判断来源,禁止把产品报价或销售调价再显示成 TMS。
* 副作用:无。只算字符串,不写库。
* 为何:inq_quote.source_label 历史上会原样带上 TMS 文案;后台必须以 source 为准。
*/
public final class InquiryQuoteDisplaySupport {
public static final String TMS_LABEL = "TMS标准报价";
public static final String MANUAL_LABEL = "手工报价";
public static final String ADJUST_LABEL = "销售调价";
public static final String ACTION_ADJUST = "quote_adjust";
public static final String ACTION_MANUAL = "manual_quote";
private InquiryQuoteDisplaySupport() {
}
/**
* 后台「报价信息」一行的来源名。
*
* @param source 库里的 source:TMS / PRODUCT / SALES_ADJUST
* @param sourceLabel 库里的 sourceLabel,可能被错误写成 TMS 标准报价
*/
public static String displayLabel(String source, String sourceLabel) {
if (isSalesAdjust(source, sourceLabel)) {
return ADJUST_LABEL;
}
if (isTms(source, sourceLabel)) {
return TMS_LABEL;
}
return MANUAL_LABEL;
}
/**
* TMS 行:只认 source=TMS。产品/调价即使 label 写成 TMS 也不算。
*/
public static boolean isTms(String source, String sourceLabel) {
String src = StringUtils.trimToEmpty(source);
if ("PRODUCT".equalsIgnoreCase(src) || "SALES_ADJUST".equalsIgnoreCase(src)) {
return false;
}
return "TMS".equalsIgnoreCase(src) || StringUtils.containsIgnoreCase(sourceLabel, "TMS");
}
public static boolean isSalesAdjust(String source, String remark) {
if ("SALES_ADJUST".equalsIgnoreCase(StringUtils.trimToEmpty(source))) {
return true;
}
return StringUtils.startsWith(StringUtils.trimToEmpty(remark), "销售调价");
}
}
@@ -0,0 +1,120 @@
package org.jeecg.modules.inquiry.support;
import org.apache.commons.lang3.StringUtils;
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.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 后台成本价 / 销售价 / 利润点:按业务线从报价记录重算。
* <p>
* 海运/陆运:TMS、产品报价是成本;销售调价后才有销售价,利润点=(销售-成本)/成本。
* 空运:成本价始终空;TMS/航线/调价都是销售侧;利润点只在销售调过价后算。
* 副作用:只改传入 ticket 的三个展示字段,不写库。调用方决定是否 updateById。
*/
public final class InquiryTicketPriceSupport {
private static final Pattern AMOUNT = Pattern.compile("(-?\\d+(?:\\.\\d+)?)");
private InquiryTicketPriceSupport() {
}
/**
* 按报价记录就地写入成本价、销售价、利润点。
*
* @param ticket 当前工单,读取 businessLine
* @param quotes 该单全部报价,按时间从旧到新
*/
public static void apply(InquiryTicket ticket, List<InquiryQuote> quotes) {
if (ticket == null) {
return;
}
boolean air = isAir(ticket.getBusinessLine());
InquiryQuote lastTms = null;
InquiryQuote lastManual = null;
InquiryQuote lastAdjust = null;
if (quotes != null) {
for (InquiryQuote q : quotes) {
if (q == null) {
continue;
}
if (InquiryQuoteDisplaySupport.isSalesAdjust(q.getSource(), q.getSourceLabel())) {
lastAdjust = q;
} else if (InquiryQuoteDisplaySupport.isTms(q.getSource(), q.getSourceLabel())) {
lastTms = q;
} else {
lastManual = q;
}
}
}
String tmsPrice = priceOf(lastTms);
String manualPrice = priceOf(lastManual);
String adjustPrice = priceOf(lastAdjust);
if (air) {
ticket.setCostPrice("");
if (StringUtils.isNotBlank(adjustPrice)) {
ticket.setSalePrice(adjustPrice);
String baseline = StringUtils.isNotBlank(manualPrice) ? manualPrice : tmsPrice;
ticket.setProfitRate(profitText(adjustPrice, baseline));
} else {
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("");
}
}
static boolean isAir(String businessLine) {
String raw = StringUtils.trimToEmpty(businessLine);
return "AIR".equalsIgnoreCase(raw) || raw.contains("空运");
}
static String priceOf(InquiryQuote quote) {
return quote == null ? "" : StringUtils.trimToEmpty(quote.getPriceText());
}
static BigDecimal parseAmount(String priceText) {
if (StringUtils.isBlank(priceText) || "-".equals(priceText.trim())) {
return null;
}
Matcher m = AMOUNT.matcher(priceText.replace(",", ""));
if (!m.find()) {
return null;
}
try {
return new BigDecimal(m.group(1));
} catch (Exception e) {
return null;
}
}
/**
* (销售 − 基准) / 基准 × 100%,没有基准或销售则空。
*/
static String profitText(String saleText, String baselineText) {
BigDecimal sale = parseAmount(saleText);
BigDecimal base = parseAmount(baselineText);
if (sale == null || base == null || base.compareTo(BigDecimal.ZERO) == 0) {
return "";
}
BigDecimal pct = sale.subtract(base)
.divide(base, 6, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"))
.setScale(2, RoundingMode.HALF_UP);
return pct.stripTrailingZeros().toPlainString() + "%";
}
}
@@ -11,6 +11,7 @@ import org.jeecg.modules.inquiry.entity.InquiryQuote;
import org.jeecg.modules.inquiry.entity.InquiryTicket;
import org.jeecg.modules.inquiry.mapper.InquiryQuoteMapper;
import org.jeecg.modules.inquiry.service.IInquiryWorkOrderService;
import org.jeecg.modules.inquiry.support.InquiryTicketPriceSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -226,8 +227,11 @@ public class InquiryTmsQueryService {
row.setPayloadJson(JSON.toJSONString(quote));
row.setCreateTime(new Date());
quoteMapper.insert(row);
// 后台列表/详情读主表售价;只写子表的话工单页看起来是空的。
applyQuoteToTicket(ticket, quote);
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
.eq(InquiryQuote::getWorkOrderNo, workOrderNo)
.orderByAsc(InquiryQuote::getCreateTime));
InquiryTicketPriceSupport.apply(ticket, quotes);
ticket.setTmsHit(1);
workOrderService.updateById(ticket);
if (TicketStatuses.canTransit(ticket.getStatus(), TicketStatuses.QUOTED)) {
TicketTransitionRequest tr = new TicketTransitionRequest();
@@ -456,24 +460,17 @@ public class InquiryTmsQueryService {
/**
* 把本次 TMS 报价写回工单主表,供后台列表/详情展示。
* 没有的成本/毛利率不编;售价用销售合计。
* 海运/陆运进成本价,空运进销售价;利润点必须等销售调价后才算。
*/
private static void applyQuoteToTicket(InquiryTicket ticket, Map<String, String> quote) {
static void applyQuoteToTicket(InquiryTicket ticket, Map<String, String> quote) {
if (ticket == null || quote == null) {
return;
}
String sale = TmsQuoteCurrency.formatDisplay(quote.get("currency"), quote.get("total"));
if (StringUtils.isNotBlank(sale) && !"-".equals(sale)) {
ticket.setSalePrice(sale);
}
String cost = text(quote.get("cost"));
if (StringUtils.isNotBlank(cost) && !"-".equals(cost)) {
ticket.setCostPrice(cost);
}
String profit = text(quote.get("profit_rate"));
if (StringUtils.isNotBlank(profit) && !"-".equals(profit)) {
ticket.setProfitRate(profit);
}
InquiryQuote row = new InquiryQuote();
row.setSource("TMS");
row.setSourceLabel("TMS标准报价");
row.setPriceText(TmsQuoteCurrency.formatDisplay(quote.get("currency"), quote.get("total")));
InquiryTicketPriceSupport.apply(ticket, java.util.Collections.singletonList(row));
ticket.setTmsHit(1);
}
@@ -0,0 +1,30 @@
package org.jeecg.modules.inquiry.support;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 后台报价来源:TMS 与手工必须分开,禁止把产品/调价再写成 TMS 标准报价。
*/
public class InquiryQuoteDisplaySupportTest {
@Test
public void tmsKeepsStandardLabel() {
assertEquals("TMS标准报价", InquiryQuoteDisplaySupport.displayLabel("TMS", "TMS 标准报价"));
assertEquals("TMS标准报价", InquiryQuoteDisplaySupport.displayLabel("TMS", ""));
assertTrue(InquiryQuoteDisplaySupport.isTms("TMS", "随便"));
}
@Test
public void productAndAdjustAreNotTms() {
assertEquals("手工报价", InquiryQuoteDisplaySupport.displayLabel("PRODUCT", "产品报价"));
assertEquals("销售调价", InquiryQuoteDisplaySupport.displayLabel("SALES_ADJUST", "TMS 标准报价"));
assertEquals("手工报价", InquiryQuoteDisplaySupport.displayLabel("PRODUCT", "TMS标准报价"));
assertFalse(InquiryQuoteDisplaySupport.isTms("PRODUCT", "TMS 标准报价"));
assertTrue(InquiryQuoteDisplaySupport.isSalesAdjust("SALES_ADJUST", "销售调价:运输费改到 100"));
assertTrue(InquiryQuoteDisplaySupport.isSalesAdjust("PRODUCT", "销售调价:总成本价改到 180"));
}
}
@@ -0,0 +1,93 @@
package org.jeecg.modules.inquiry.support;
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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 后台三列:海运/陆运成本与销售分开,空运只走销售价。
*/
public class InquiryTicketPriceSupportTest {
@Test
public void landTmsOnlyGoesToCost() {
InquiryTicket ticket = ticket("LAND");
InquiryTicketPriceSupport.apply(ticket, Collections.singletonList(quote("TMS", "TMS标准报价", "CNY 121.00")));
assertEquals("CNY 121.00", ticket.getCostPrice());
assertEquals("", ticket.getSalePrice());
assertEquals("", ticket.getProfitRate());
}
@Test
public void landAdjustKeepsCostWritesSaleAndProfit() {
InquiryTicket ticket = ticket("LAND");
InquiryTicketPriceSupport.apply(ticket, Arrays.asList(
quote("TMS", "TMS标准报价", "CNY 121.00"),
quote("SALES_ADJUST", "销售调价", "CNY 150.00")));
assertEquals("CNY 121.00", ticket.getCostPrice());
assertEquals("CNY 150.00", ticket.getSalePrice());
assertEquals("23.97%", ticket.getProfitRate());
}
@Test
public void landProductBecomesCostAndKeepsSale() {
InquiryTicket ticket = ticket("SEA");
InquiryTicketPriceSupport.apply(ticket, Arrays.asList(
quote("TMS", "TMS标准报价", "CNY 100.00"),
quote("SALES_ADJUST", "销售调价", "CNY 130.00"),
quote("PRODUCT", "手工报价", "CNY 110.00")));
assertEquals("CNY 110.00", ticket.getCostPrice());
assertEquals("CNY 130.00", ticket.getSalePrice());
assertEquals("18.18%", ticket.getProfitRate());
}
@Test
public void airTmsGoesToSaleCostEmpty() {
InquiryTicket ticket = ticket("AIR");
InquiryTicketPriceSupport.apply(ticket, Collections.singletonList(quote("TMS", "TMS标准报价", "USD 1280")));
assertEquals("", ticket.getCostPrice());
assertEquals("USD 1280", ticket.getSalePrice());
assertEquals("", ticket.getProfitRate());
}
@Test
public void airRouteAfterAdjustOnlyReplacesBaseline() {
InquiryTicket ticket = ticket("AIR");
InquiryTicketPriceSupport.apply(ticket, Arrays.asList(
quote("TMS", "TMS标准报价", "USD 1000"),
quote("SALES_ADJUST", "销售调价", "USD 1200"),
quote("PRODUCT", "手工报价", "USD 1100")));
assertEquals("", ticket.getCostPrice());
assertEquals("USD 1200", ticket.getSalePrice());
assertEquals("9.09%", ticket.getProfitRate());
}
@Test
public void parseAmountReadsCurrencyPrefix() {
assertEquals("121.00", InquiryTicketPriceSupport.parseAmount("CNY 121.00").toPlainString());
assertTrue(InquiryTicketPriceSupport.isAir("空运"));
}
private static InquiryTicket ticket(String line) {
InquiryTicket t = new InquiryTicket();
t.setBusinessLine(line);
t.setSalePrice("旧销售价");
t.setCostPrice("旧成本价");
t.setProfitRate("旧利润");
return t;
}
private static InquiryQuote quote(String source, String label, String price) {
InquiryQuote q = new InquiryQuote();
q.setSource(source);
q.setSourceLabel(label);
q.setPriceText(price);
return q;
}
}