落地销售调价:按自然语言改价并单独记销售价,拉群和后台不再把调后价当成 TMS 原价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+10
@@ -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;
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
|
||||
+105
-21
@@ -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);
|
||||
|
||||
+56
@@ -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), "销售调价");
|
||||
}
|
||||
}
|
||||
+120
@@ -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() + "%";
|
||||
}
|
||||
}
|
||||
+13
-16
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+30
@@ -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"));
|
||||
}
|
||||
}
|
||||
+93
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user