导入新产品,20.套餐编辑的时候,不能低于基准价,低于就提示不能保存
This commit is contained in:
+12
@@ -110,4 +110,16 @@ public class ProductController extends BaseController
|
|||||||
String message = productService.importProductExcel(file, updateSupport, getUsername());
|
String message = productService.importProductExcel(file, updateSupport, getUsername());
|
||||||
return success(message);
|
return success(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a single-country linehaul cost sheet. countryCode is the destination.
|
||||||
|
*/
|
||||||
|
@Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.IMPORT)
|
||||||
|
@PreAuthorize("@ss.hasPermi('price:product:import')")
|
||||||
|
@PostMapping("/importCountryData")
|
||||||
|
public AjaxResult importCountryData(MultipartFile file, String countryCode) throws Exception
|
||||||
|
{
|
||||||
|
String message = productService.importCountryProductExcel(file, countryCode, getUsername());
|
||||||
|
return success(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ price.set.detail.freight.empty=固定价明细必须填写运费单价
|
|||||||
price.set.detail.registration.empty=固定价明细必须填写挂号费
|
price.set.detail.registration.empty=固定价明细必须填写挂号费
|
||||||
price.set.detail.zone.empty=固定价明细必须选择分区
|
price.set.detail.zone.empty=固定价明细必须选择分区
|
||||||
price.set.detail.roundMode.invalid=取值方式仅支持 UP / DOWN / HALF_UP
|
price.set.detail.roundMode.invalid=取值方式仅支持 UP / DOWN / HALF_UP
|
||||||
|
price.set.detail.freight.belowBase=套餐运费不能低于导入基准价 {0}
|
||||||
|
price.set.detail.registration.belowBase=套餐挂号费不能低于导入基准价 {0}
|
||||||
price.set.customer.notExists=客户绑定不存在
|
price.set.customer.notExists=客户绑定不存在
|
||||||
price.set.customer.unbindAudit=审核中的套餐不允许解绑客户
|
price.set.customer.unbindAudit=审核中的套餐不允许解绑客户
|
||||||
price.set.customer.customerId.empty=客户ID不能为空
|
price.set.customer.customerId.empty=客户ID不能为空
|
||||||
@@ -117,6 +119,11 @@ price.product.import.noSheet=未找到产品明细sheet(需与产品名称一
|
|||||||
price.product.import.noCountry=产品明细sheet无有效国家/重量段数据:{0}
|
price.product.import.noCountry=产品明细sheet无有效国家/重量段数据:{0}
|
||||||
price.product.import.euEmpty=国家表无欧盟成员(is_eu=1),无法展开「欧盟」
|
price.product.import.euEmpty=国家表无欧盟成员(is_eu=1),无法展开「欧盟」
|
||||||
price.product.import.success.unmatched=未匹配国家表(country_code 为空):{0}
|
price.product.import.success.unmatched=未匹配国家表(country_code 为空):{0}
|
||||||
|
price.product.import.countryRequired=请选择目的国家
|
||||||
|
price.product.import.countryNotFound=国家不存在或已停用:{0}
|
||||||
|
price.product.import.countrySheetNotFound=未找到「产品目录」sheet,请上传单国专线成本价表
|
||||||
|
price.product.import.countryNoPrice=未找到该产品的重量段价格:{0}
|
||||||
|
price.product.import.countrySuccess=按国家导入成功({0}):新增 {1} 个产品,跳过 {2} 个(同代码同生效日不覆盖主数据);写入重量段 {3} 行。
|
||||||
# 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约)
|
# 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约)
|
||||||
price.quote.postcodeRequired=该国家/产品有分区,请填写目的邮编
|
price.quote.postcodeRequired=该国家/产品有分区,请填写目的邮编
|
||||||
price.quote.postcodeZoneNotFound=邮编未匹配到分区,请核对目的邮编
|
price.quote.postcodeZoneNotFound=邮编未匹配到分区,请核对目的邮编
|
||||||
|
|||||||
@@ -43,4 +43,15 @@ public interface IBmsProductService
|
|||||||
* @return 导入结果说明(可含 HTML 换行)
|
* @return 导入结果说明(可含 HTML 换行)
|
||||||
*/
|
*/
|
||||||
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception;
|
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a single-country cost sheet (catalog + product weight rows).
|
||||||
|
* The destination country is chosen by the user, not read from the sheet.
|
||||||
|
*
|
||||||
|
* @param file uploaded xlsx/xls
|
||||||
|
* @param countryCode ISO country code (bms_region.country_code_2)
|
||||||
|
* @param operName operator
|
||||||
|
* @return HTML result message
|
||||||
|
*/
|
||||||
|
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception;
|
||||||
}
|
}
|
||||||
|
|||||||
+452
@@ -321,6 +321,100 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception
|
||||||
|
{
|
||||||
|
if (file == null || file.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty"));
|
||||||
|
}
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx")
|
||||||
|
|| filename.toLowerCase(Locale.ROOT).endsWith(".xls")))
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.fileType"));
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(countryCode))
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
|
||||||
|
}
|
||||||
|
BmsRegion region = regionMapper.selectRegionByCode2(countryCode.trim().toUpperCase(Locale.ROOT));
|
||||||
|
if (region == null || "2".equals(region.getDelFlag())
|
||||||
|
|| (StringUtils.isNotEmpty(region.getStatus()) && !"0".equals(region.getStatus())))
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.countryNotFound", countryCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is))
|
||||||
|
{
|
||||||
|
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||||
|
if (catalog == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.countrySheetNotFound"));
|
||||||
|
}
|
||||||
|
List<BmsProduct> products = parseCountryCatalog(catalog);
|
||||||
|
if (products.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
|
||||||
|
}
|
||||||
|
products = dedupeCatalogProducts(products);
|
||||||
|
Map<String, PriceBlock> priceBlocks = parseCountryPriceBlocks(workbook);
|
||||||
|
|
||||||
|
int successNum = 0;
|
||||||
|
int skipNum = 0;
|
||||||
|
int countryNum = 0;
|
||||||
|
StringBuilder failureMsg = new StringBuilder();
|
||||||
|
String countryLabel = StringUtils.isNotEmpty(region.getCountryNameCn())
|
||||||
|
? region.getCountryNameCn() : region.getCountryCode2();
|
||||||
|
|
||||||
|
for (BmsProduct product : products)
|
||||||
|
{
|
||||||
|
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
|
||||||
|
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||||
|
TransactionStatus tx = transactionManager.getTransaction(def);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PriceBlock block = findPriceBlock(product.getProductName(), priceBlocks);
|
||||||
|
if (block == null || block.rows.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.countryNoPrice",
|
||||||
|
product.getProductName()));
|
||||||
|
}
|
||||||
|
product.setDestRegion(countryLabel);
|
||||||
|
product.setSourceSheet(block.sheetName);
|
||||||
|
if (StringUtils.isEmpty(product.getTaxMode()))
|
||||||
|
{
|
||||||
|
product.setTaxMode(resolveTaxMode(product.getProductName()));
|
||||||
|
}
|
||||||
|
List<BmsProductCountry> countries = stampSelectedCountry(block.rows, region, product.getEffectiveDate());
|
||||||
|
int[] nums = persistImportedProduct(product, countries, operName);
|
||||||
|
transactionManager.commit(tx);
|
||||||
|
successNum += nums[0];
|
||||||
|
skipNum += nums[1];
|
||||||
|
countryNum += nums[2];
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
transactionManager.rollback(tx);
|
||||||
|
String tip = e.getMessage();
|
||||||
|
if (StringUtils.isEmpty(tip))
|
||||||
|
{
|
||||||
|
tip = e.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
failureMsg.append("<br/>").append(product.getProductCode()).append("\uff1a").append(tip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failureMsg.length() > 0)
|
||||||
|
{
|
||||||
|
return MessageUtils.message("price.product.import.partialFail",
|
||||||
|
Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString());
|
||||||
|
}
|
||||||
|
return MessageUtils.message("price.product.import.countrySuccess",
|
||||||
|
countryLabel, Integer.valueOf(successNum), Integer.valueOf(skipNum), Integer.valueOf(countryNum));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @return int[]{added, skipped, countryRows} */
|
/** @return int[]{added, skipped, countryRows} */
|
||||||
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
|
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
|
||||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||||
@@ -1289,6 +1383,364 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Country cost sheet catalog: product id / name / service / cargo / note / update date.
|
||||||
|
* Destination country is not in the sheet.
|
||||||
|
*/
|
||||||
|
private List<BmsProduct> parseCountryCatalog(Sheet sheet)
|
||||||
|
{
|
||||||
|
List<BmsProduct> list = new ArrayList<>();
|
||||||
|
int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0");
|
||||||
|
if (header == null)
|
||||||
|
{
|
||||||
|
header = findHeaderCell(sheet, "\u4ea7\u54c1ID");
|
||||||
|
}
|
||||||
|
if (header == null)
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
Row head = sheet.getRow(header[0]);
|
||||||
|
int colCode = firstCol(head, "\u4ea7\u54c1ID", "\u4ea7\u54c1\u5bf9\u63a5\u4ee3\u7801", "\u4ea7\u54c1\u4ee3\u7801");
|
||||||
|
int colName = findColByHeader(head, "\u4ea7\u54c1\u540d\u79f0", 0);
|
||||||
|
int colService = firstCol(head, "\u670d\u52a1", "\u65f6\u6548\u7b49\u7ea7");
|
||||||
|
int colCargo = findColByHeader(head, "\u8d70\u8d27\u5c5e\u6027", 0);
|
||||||
|
int colNote = firstCol(head, "\u5907\u6ce8", "\u66f4\u65b0\u5185\u5bb9");
|
||||||
|
int colDate = firstCol(head, "\u66f4\u65b0\u65f6\u95f4", "\u751f\u6548\u65e5\u671f");
|
||||||
|
if (colCode < 0 || colName < 0)
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
int lastRow = sheet.getLastRowNum();
|
||||||
|
for (int i = header[0] + 1; i <= lastRow; i++)
|
||||||
|
{
|
||||||
|
Row row = sheet.getRow(i);
|
||||||
|
if (row == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String code = cellString(row.getCell(colCode));
|
||||||
|
if (StringUtils.isEmpty(code) || !code.matches("(?i)^[A-Z0-9_-]+$"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = cellString(row.getCell(colName));
|
||||||
|
if (StringUtils.isEmpty(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BmsProduct p = new BmsProduct();
|
||||||
|
p.setProductCode(code.trim());
|
||||||
|
p.setProductName(name.trim());
|
||||||
|
if (colService >= 0)
|
||||||
|
{
|
||||||
|
p.setServiceLevel(emptyToNull(cellString(row.getCell(colService))));
|
||||||
|
}
|
||||||
|
if (colCargo >= 0)
|
||||||
|
{
|
||||||
|
p.setCargoAttr(emptyToNull(cellString(row.getCell(colCargo))));
|
||||||
|
}
|
||||||
|
if (colNote >= 0)
|
||||||
|
{
|
||||||
|
p.setUpdateNote(emptyToNull(cellString(row.getCell(colNote))));
|
||||||
|
}
|
||||||
|
if (colDate >= 0)
|
||||||
|
{
|
||||||
|
Date eff = cellDate(row.getCell(colDate));
|
||||||
|
if (eff == null)
|
||||||
|
{
|
||||||
|
eff = parseLooseDate(cellString(row.getCell(colDate)));
|
||||||
|
}
|
||||||
|
p.setEffectiveDate(toDateOnly(eff));
|
||||||
|
}
|
||||||
|
p.setTaxMode(resolveTaxMode(p.getProductName()));
|
||||||
|
list.add(p);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, PriceBlock> parseCountryPriceBlocks(Workbook workbook)
|
||||||
|
{
|
||||||
|
Map<String, PriceBlock> map = new HashMap<>();
|
||||||
|
if (workbook == null)
|
||||||
|
{
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||||
|
{
|
||||||
|
String sheetName = workbook.getSheetName(i);
|
||||||
|
if (shouldSkipCountryPriceSheet(sheetName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Sheet sheet = workbook.getSheetAt(i);
|
||||||
|
int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0");
|
||||||
|
if (header == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Row headerRow = sheet.getRow(header[0]);
|
||||||
|
int colName = header[1];
|
||||||
|
int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", 0);
|
||||||
|
if (colWeight < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", 0);
|
||||||
|
int colRegister = findColByHeader(headerRow, "\u6302\u53f7", 0);
|
||||||
|
int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", 0);
|
||||||
|
int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", 0);
|
||||||
|
int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", 0);
|
||||||
|
|
||||||
|
String currentName = null;
|
||||||
|
String currentSize = null;
|
||||||
|
String currentLastMile = null;
|
||||||
|
String currentLeadTime = null;
|
||||||
|
int lastRow = sheet.getLastRowNum();
|
||||||
|
for (int r = header[0] + 1; r <= lastRow; r++)
|
||||||
|
{
|
||||||
|
Row row = sheet.getRow(r);
|
||||||
|
if (row == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String nameCell = cellString(row.getCell(colName));
|
||||||
|
if (StringUtils.isNotEmpty(nameCell))
|
||||||
|
{
|
||||||
|
if (isSectionTitle(nameCell))
|
||||||
|
{
|
||||||
|
currentName = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
currentName = nameCell.replace("\n", " ").replace("\r", " ").trim();
|
||||||
|
currentSize = colSize >= 0 ? emptyToNull(cellString(row.getCell(colSize))) : null;
|
||||||
|
currentLastMile = colLastMile >= 0 ? emptyToNull(cellString(row.getCell(colLastMile))) : null;
|
||||||
|
currentLeadTime = colLeadTime >= 0 ? emptyToNull(cellString(row.getCell(colLeadTime))) : null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(currentName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String weightLabel = cellString(row.getCell(colWeight));
|
||||||
|
BigDecimal[] range = parseWeightRange(weightLabel);
|
||||||
|
if (range == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (colSize >= 0 && StringUtils.isNotEmpty(cellString(row.getCell(colSize))))
|
||||||
|
{
|
||||||
|
currentSize = emptyToNull(cellString(row.getCell(colSize)));
|
||||||
|
}
|
||||||
|
if (colLastMile >= 0 && StringUtils.isNotEmpty(cellString(row.getCell(colLastMile))))
|
||||||
|
{
|
||||||
|
currentLastMile = emptyToNull(cellString(row.getCell(colLastMile)));
|
||||||
|
}
|
||||||
|
if (colLeadTime >= 0 && StringUtils.isNotEmpty(cellString(row.getCell(colLeadTime))))
|
||||||
|
{
|
||||||
|
currentLeadTime = emptyToNull(cellString(row.getCell(colLeadTime)));
|
||||||
|
}
|
||||||
|
BmsProductCountry c = new BmsProductCountry();
|
||||||
|
c.setWeightMin(range[0]);
|
||||||
|
c.setWeightMax(range[1]);
|
||||||
|
c.setFreightPrice(colFreight >= 0 ? cellDecimal(row.getCell(colFreight)) : null);
|
||||||
|
c.setRegistrationFee(colRegister >= 0 ? cellDecimal(row.getCell(colRegister)) : null);
|
||||||
|
c.setSizeLimit(currentSize);
|
||||||
|
c.setLastMile(currentLastMile);
|
||||||
|
c.setLeadTime(currentLeadTime);
|
||||||
|
PriceBlock block = map.get(currentName);
|
||||||
|
if (block == null)
|
||||||
|
{
|
||||||
|
block = new PriceBlock();
|
||||||
|
block.sheetName = sheetName;
|
||||||
|
map.put(currentName, block);
|
||||||
|
}
|
||||||
|
block.rows.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldSkipCountryPriceSheet(String name)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(name))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (CATALOG_SHEET.equals(name) || SKIP_SHEETS.contains(name))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return name.contains("\u90ae\u7f16") || name.contains("\u8d54\u507f");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSectionTitle(String name)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(name))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String n = name.trim();
|
||||||
|
return n.startsWith("\u4e00\u3001") || n.startsWith("\u4e8c\u3001") || n.startsWith("\u4e09\u3001")
|
||||||
|
|| n.startsWith("\u56db\u3001") || n.startsWith("\u4e94\u3001") || n.startsWith("\u516d\u3001")
|
||||||
|
|| n.startsWith("\u7279\u522b");
|
||||||
|
}
|
||||||
|
|
||||||
|
private PriceBlock findPriceBlock(String productName, Map<String, PriceBlock> blocks)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(productName) || blocks == null || blocks.isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String name = productName.trim();
|
||||||
|
PriceBlock exact = blocks.get(name);
|
||||||
|
if (exact != null)
|
||||||
|
{
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
String stripped = name.replace("\u6807\u51c6", "").trim();
|
||||||
|
exact = blocks.get(stripped);
|
||||||
|
if (exact != null)
|
||||||
|
{
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
String want = compactName(name);
|
||||||
|
PriceBlock hit = null;
|
||||||
|
int hits = 0;
|
||||||
|
for (Map.Entry<String, PriceBlock> entry : blocks.entrySet())
|
||||||
|
{
|
||||||
|
String key = compactName(entry.getKey());
|
||||||
|
if (key.equals(want) || (want.contains(key) && key.length() >= 4) || (key.contains(want) && want.length() >= 4))
|
||||||
|
{
|
||||||
|
hit = entry.getValue();
|
||||||
|
hits++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hits == 1 ? hit : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String compactName(String s)
|
||||||
|
{
|
||||||
|
if (s == null)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return s.trim().replace(" ", "").replace("\u3000", "")
|
||||||
|
.replace("-", "").replace("\u2013", "").replace("\u2014", "").replace("\uff0d", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BmsProductCountry> stampSelectedCountry(List<BmsProductCountry> rows, BmsRegion region, Date eff)
|
||||||
|
{
|
||||||
|
List<BmsProductCountry> out = new ArrayList<>();
|
||||||
|
if (rows == null)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
String countryName = region == null ? null : region.getCountryNameCn();
|
||||||
|
String countryCode = region == null ? null : region.getCountryCode2();
|
||||||
|
for (BmsProductCountry src : rows)
|
||||||
|
{
|
||||||
|
BmsProductCountry c = new BmsProductCountry();
|
||||||
|
c.setCountryName(countryName);
|
||||||
|
c.setCountryCode(countryCode);
|
||||||
|
c.setWeightMin(src.getWeightMin());
|
||||||
|
c.setWeightMax(src.getWeightMax());
|
||||||
|
c.setFreightPrice(src.getFreightPrice());
|
||||||
|
c.setRegistrationFee(src.getRegistrationFee());
|
||||||
|
c.setSizeLimit(src.getSizeLimit());
|
||||||
|
c.setLastMile(src.getLastMile());
|
||||||
|
c.setLeadTime(src.getLeadTime());
|
||||||
|
c.setEffectiveDate(eff);
|
||||||
|
out.add(c);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return int[]{added, skipped, countryRows} */
|
||||||
|
private int[] persistImportedProduct(BmsProduct product, List<BmsProductCountry> countries, String operName)
|
||||||
|
{
|
||||||
|
product.setCreateBy(operName);
|
||||||
|
product.setUpdateBy(operName);
|
||||||
|
if (StringUtils.isEmpty(product.getSourceSheet()))
|
||||||
|
{
|
||||||
|
product.setSourceSheet(product.getProductName());
|
||||||
|
}
|
||||||
|
Date newEff = toDateOnly(product.getEffectiveDate());
|
||||||
|
if (newEff == null)
|
||||||
|
{
|
||||||
|
newEff = toDateOnly(resolveImportEffectiveDate(countries));
|
||||||
|
}
|
||||||
|
product.setEffectiveDate(newEff);
|
||||||
|
if (newEff == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.effEmpty"));
|
||||||
|
}
|
||||||
|
BmsProduct existing = productMapper.selectBmsProductByCodeAndDate(product.getProductCode(), newEff);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
product.setProductId(existing.getProductId());
|
||||||
|
int countryRows = saveCountriesVersioned(product, countries);
|
||||||
|
return new int[] { 0, 1, countryRows };
|
||||||
|
}
|
||||||
|
applyProductStatus(product);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
productMapper.insertBmsProduct(product);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (isProductCodeUniqueViolation(e))
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.needCodeEffUnique"));
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
|
||||||
|
productMapper.refreshStatusByDates();
|
||||||
|
int countryRows = saveCountriesVersioned(product, countries);
|
||||||
|
return new int[] { 1, 0, countryRows };
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date parseLooseDate(String text)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(text))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String s = text.trim();
|
||||||
|
if (s.length() > 20)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Matcher ymd = EFFECTIVE_DATE_YMD.matcher(s);
|
||||||
|
if (ymd.matches())
|
||||||
|
{
|
||||||
|
return parseYmd(ymd.group(1), ymd.group(2), ymd.group(3));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int firstCol(Row row, String... keywords)
|
||||||
|
{
|
||||||
|
if (keywords == null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
for (String keyword : keywords)
|
||||||
|
{
|
||||||
|
int col = findColByHeader(row, keyword, 0);
|
||||||
|
if (col >= 0)
|
||||||
|
{
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PriceBlock
|
||||||
|
{
|
||||||
|
private String sheetName;
|
||||||
|
private final List<BmsProductCountry> rows = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
private static Date cellDate(Cell cell)
|
private static Date cellDate(Cell cell)
|
||||||
{
|
{
|
||||||
if (cell == null)
|
if (cell == null)
|
||||||
|
|||||||
+84
@@ -1039,6 +1039,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
|||||||
// (见 TmsPriceQuoteServiceImpl.tryFixed)。
|
// (见 TmsPriceQuoteServiceImpl.tryFixed)。
|
||||||
fillProductCountryId(detail);
|
fillProductCountryId(detail);
|
||||||
snapshotWeightAndDates(detail);
|
snapshotWeightAndDates(detail);
|
||||||
|
assertNotBelowImportedPrice(detail);
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(detail.getRoundMode()))
|
if (StringUtils.isEmpty(detail.getRoundMode()))
|
||||||
{
|
{
|
||||||
@@ -1059,6 +1060,89 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Package freight / registration must not go below the imported product country price. */
|
||||||
|
private void assertNotBelowImportedPrice(PrcPriceSetDetail detail)
|
||||||
|
{
|
||||||
|
BmsProductCountry base = findImportedPrice(detail);
|
||||||
|
if (base == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (detail.getFvValue() != null && base.getFreightPrice() != null
|
||||||
|
&& detail.getFvValue().compareTo(base.getFreightPrice()) < 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.set.detail.freight.belowBase",
|
||||||
|
stripTrailingZeros(base.getFreightPrice())));
|
||||||
|
}
|
||||||
|
if (detail.getFvRegistrationFee() != null && base.getRegistrationFee() != null
|
||||||
|
&& detail.getFvRegistrationFee().compareTo(base.getRegistrationFee()) < 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.set.detail.registration.belowBase",
|
||||||
|
stripTrailingZeros(base.getRegistrationFee())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BmsProductCountry findImportedPrice(PrcPriceSetDetail detail)
|
||||||
|
{
|
||||||
|
if (detail == null || StringUtils.isEmpty(detail.getProductCode()))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BmsProductCountry query = new BmsProductCountry();
|
||||||
|
query.setProductCode(detail.getProductCode());
|
||||||
|
List<BmsProductCountry> rows = productCountryMapper.selectBmsProductCountryList(query);
|
||||||
|
if (rows == null || rows.isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (detail.getProductCountryId() != null)
|
||||||
|
{
|
||||||
|
for (BmsProductCountry row : rows)
|
||||||
|
{
|
||||||
|
if (row != null && detail.getProductCountryId().equals(row.getDetailId()))
|
||||||
|
{
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (BmsProductCountry row : rows)
|
||||||
|
{
|
||||||
|
if (row == null || row.getExpireDate() != null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!matchDetailCountry(detail, row))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String label = formatWeightLabel(row.getWeightMin(), row.getWeightMax());
|
||||||
|
if (StringUtils.isNotEmpty(detail.getWeightLabel()) && detail.getWeightLabel().equals(label))
|
||||||
|
{
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
if (detail.getWeightMin() != null && row.getWeightMin() != null
|
||||||
|
&& detail.getWeightMin().compareTo(row.getWeightMin()) == 0
|
||||||
|
&& sameWeightMax(detail.getWeightMax(), row.getWeightMax()))
|
||||||
|
{
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameWeightMax(java.math.BigDecimal a, java.math.BigDecimal b)
|
||||||
|
{
|
||||||
|
if (a == null && b == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (a == null || b == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return a.compareTo(b) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When product country rows for this country have zones, zone is required.
|
* When product country rows for this country have zones, zone is required.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.extra" class="import-extra">
|
||||||
|
<slot name="extra" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 上传拖拽区 -->
|
<!-- 上传拖拽区 -->
|
||||||
<el-upload
|
<el-upload
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
@@ -138,6 +142,14 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
extraQuery: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
beforeSubmit: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
// 确定按钮文字
|
// 确定按钮文字
|
||||||
submitText: {
|
submitText: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -157,7 +169,16 @@ const validateError = ref('')
|
|||||||
const headers = { Authorization: 'Bearer ' + getToken() }
|
const headers = { Authorization: 'Bearer ' + getToken() }
|
||||||
|
|
||||||
const uploadUrl = computed(() => {
|
const uploadUrl = computed(() => {
|
||||||
return import.meta.env.VITE_APP_BASE_API + props.action + '?updateSupport=' + (updateSupport.value ? 1 : 0)
|
const query = new URLSearchParams()
|
||||||
|
query.set('updateSupport', updateSupport.value ? '1' : '0')
|
||||||
|
const extra = props.extraQuery || {}
|
||||||
|
Object.keys(extra).forEach(key => {
|
||||||
|
const value = extra[key]
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
query.set(key, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return import.meta.env.VITE_APP_BASE_API + props.action + '?' + query.toString()
|
||||||
})
|
})
|
||||||
|
|
||||||
const templateUrl = computed(() => !!props.templateAction)
|
const templateUrl = computed(() => !!props.templateAction)
|
||||||
@@ -256,6 +277,13 @@ function handleSuccess(response) {
|
|||||||
|
|
||||||
// 提交上传
|
// 提交上传
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
|
if (typeof props.beforeSubmit === 'function') {
|
||||||
|
const msg = props.beforeSubmit()
|
||||||
|
if (msg) {
|
||||||
|
proxy.$modal.msgError(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
const file = selectedFile.value
|
const file = selectedFile.value
|
||||||
const err = checkFile(file)
|
const err = checkFile(file)
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -440,6 +468,10 @@ defineExpose({ open })
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.import-extra {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.file-progress {
|
.file-progress {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const columns = [
|
|||||||
{ prop: 'effectiveDate', label: '生效日期', width: 110, align: 'center' },
|
{ prop: 'effectiveDate', label: '生效日期', width: 110, align: 'center' },
|
||||||
{ prop: 'expireDate', label: '失效日期', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' },
|
{ prop: 'expireDate', label: '失效日期', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' },
|
||||||
{ prop: 'status', label: '状态', width: 90, align: 'center', dict: 'bms_product_effect_status' },
|
{ prop: 'status', label: '状态', width: 90, align: 'center', dict: 'bms_product_effect_status' },
|
||||||
{ prop: 'action', label: '操作', width: 70, fixed: 'right', slot: 'action' }
|
// { prop: 'action', label: '操作', width: 70, fixed: 'right', slot: 'action' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
class="mb16"
|
class="mb16"
|
||||||
title="Excel 导入:相同产品代码+生效日期不覆盖;不同生效日期新增一条产品。同时入库国家×重量段(运费/挂号费/尺寸/尾程/时效)。"
|
title="多国家价表用「导入 Excel」。单国专线成本价用「按国家导入」:先选目的国家,目录里的产品都记到该国。相同产品代码+生效日期不覆盖主数据。"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<basic-table
|
<basic-table
|
||||||
@@ -25,6 +25,9 @@
|
|||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['price:product:import']">导入 Excel</el-button>
|
<el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['price:product:import']">导入 Excel</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="info" plain icon="Upload" @click="handleCountryImport" v-hasPermi="['price:product:import']">按国家导入</el-button>
|
||||||
|
</el-col>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #col-cargoAttr="{ row }">
|
<template #col-cargoAttr="{ row }">
|
||||||
@@ -53,6 +56,32 @@
|
|||||||
:show-update-support="false"
|
:show-update-support="false"
|
||||||
@success="() => tableRef.refresh()"
|
@success="() => tableRef.refresh()"
|
||||||
/>
|
/>
|
||||||
|
<excel-import-dialog
|
||||||
|
ref="countryImportRef"
|
||||||
|
title="按国家导入"
|
||||||
|
action="/price/product/importCountryData"
|
||||||
|
width="560px"
|
||||||
|
:show-update-support="false"
|
||||||
|
:extra-query="countryImportQuery"
|
||||||
|
:before-submit="validateCountryImport"
|
||||||
|
:import-desc="countryImportDesc"
|
||||||
|
@success="() => tableRef.refresh()"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<el-form label-width="90px">
|
||||||
|
<el-form-item label="目的国家" required>
|
||||||
|
<el-select v-model="importCountryCode" filterable clearable placeholder="请选择国家" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in regionOptions"
|
||||||
|
:key="item.countryCode2"
|
||||||
|
:label="regionLabel(item)"
|
||||||
|
:value="item.countryCode2"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
</excel-import-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -62,6 +91,7 @@ import ProductFormDialog from './components/ProductFormDialog.vue'
|
|||||||
import ProductCountryDrawer from './components/ProductCountryDrawer.vue'
|
import ProductCountryDrawer from './components/ProductCountryDrawer.vue'
|
||||||
import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue'
|
import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue'
|
||||||
import { listProduct } from '@/api/price/product'
|
import { listProduct } from '@/api/price/product'
|
||||||
|
import { listRegionOptions } from '@/api/data/region'
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
||||||
@@ -71,6 +101,14 @@ const formRef = ref()
|
|||||||
const countryRef = ref()
|
const countryRef = ref()
|
||||||
const surchargeRef = ref()
|
const surchargeRef = ref()
|
||||||
const importRef = ref()
|
const importRef = ref()
|
||||||
|
const countryImportRef = ref()
|
||||||
|
const importCountryCode = ref('')
|
||||||
|
const regionOptions = ref([])
|
||||||
|
const countryImportQuery = computed(() => ({ countryCode: importCountryCode.value }))
|
||||||
|
const countryImportDesc = [
|
||||||
|
'选择目的国家后上传专线成本价表,目录中的产品都会记到该国家。',
|
||||||
|
'邮编、赔偿标准等 sheet 不导入。相同产品代码+生效日期不覆盖主数据,重量段按生效日写入。'
|
||||||
|
]
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ prop: 'productCode', label: '产品代码', minWidth: 110 },
|
{ prop: 'productCode', label: '产品代码', minWidth: 110 },
|
||||||
@@ -120,6 +158,32 @@ function handleUpdate(row) {
|
|||||||
function handleImport() {
|
function handleImport() {
|
||||||
importRef.value.open()
|
importRef.value.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function regionLabel(item) {
|
||||||
|
const name = item.countryNameCn || item.countryNameEn || ''
|
||||||
|
const code = item.countryCode2 || ''
|
||||||
|
return name && code ? name + ' (' + code + ')' : (name || code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadRegionOptions() {
|
||||||
|
if (regionOptions.value.length) return
|
||||||
|
listRegionOptions().then(res => {
|
||||||
|
regionOptions.value = res.data || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCountryImport() {
|
||||||
|
importCountryCode.value = ''
|
||||||
|
loadRegionOptions()
|
||||||
|
countryImportRef.value.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCountryImport() {
|
||||||
|
if (!importCountryCode.value) {
|
||||||
|
return '请先选择目的国家'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -98,9 +98,6 @@ const columns = [
|
|||||||
{ label: '订单号', prop: 'orderNo', minWidth: 140, showOverflowTooltip: true },
|
{ label: '订单号', prop: 'orderNo', minWidth: 140, showOverflowTooltip: true },
|
||||||
{ label: '运单号', prop: 'trackingNo', minWidth: 140, showOverflowTooltip: true },
|
{ label: '运单号', prop: 'trackingNo', minWidth: 140, showOverflowTooltip: true },
|
||||||
{ label: '转单号', prop: 'transferNo', minWidth: 140, showOverflowTooltip: true },
|
{ label: '转单号', prop: 'transferNo', minWidth: 140, showOverflowTooltip: true },
|
||||||
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
|
|
||||||
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
|
|
||||||
{ label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
|
|
||||||
{ label: '客户', prop: 'customerName', minWidth: 140, showOverflowTooltip: true },
|
{ label: '客户', prop: 'customerName', minWidth: 140, showOverflowTooltip: true },
|
||||||
{ label: '客户代码', prop: 'customerCode', width: 120 },
|
{ label: '客户代码', prop: 'customerCode', width: 120 },
|
||||||
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
|
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
|
||||||
@@ -113,6 +110,9 @@ const columns = [
|
|||||||
{ label: '客户生效', prop: 'customerActiveStatus', align: 'center', width: 100, slot: 'customerActiveStatus' },
|
{ label: '客户生效', prop: 'customerActiveStatus', align: 'center', width: 100, slot: 'customerActiveStatus' },
|
||||||
{ label: '额度欠费', prop: 'creditArrearsStatus', align: 'center', width: 100, slot: 'creditArrearsStatus' },
|
{ label: '额度欠费', prop: 'creditArrearsStatus', align: 'center', width: 100, slot: 'creditArrearsStatus' },
|
||||||
{ label: '下单时间', prop: 'orderTime', align: 'center', width: 170, parseTime: true },
|
{ label: '下单时间', prop: 'orderTime', align: 'center', width: 170, parseTime: true },
|
||||||
|
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
|
||||||
|
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
|
||||||
|
// { label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
|
||||||
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
|
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
const emit = defineEmits(['update:modelValue', 'pick-surcharge', 'row-change'])
|
const emit = defineEmits(['update:modelValue', 'pick-surcharge', 'row-change'])
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const editingKey = ref('')
|
const editingKey = ref('')
|
||||||
const committing = ref(false)
|
const committing = ref(false)
|
||||||
@@ -109,7 +111,7 @@ const title = computed(() => {
|
|||||||
const name = props.productName || ''
|
const name = props.productName || ''
|
||||||
return code + ' · ' + name + ' · 国家服务价格'
|
return code + ' · ' + name + ' · 国家服务价格'
|
||||||
})
|
})
|
||||||
const hint = '黄色行=已改价;点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
|
const hint = '黄色行=已改价;点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。不能低于导入基准价。'
|
||||||
const phSearch = '搜索国家/分区'
|
const phSearch = '搜索国家/分区'
|
||||||
const colCountry = '国家'
|
const colCountry = '国家'
|
||||||
const colZone = '分区'
|
const colZone = '分区'
|
||||||
@@ -136,6 +138,8 @@ function setEditRef(el, row, field) {
|
|||||||
|
|
||||||
function startEdit(row, field) {
|
function startEdit(row, field) {
|
||||||
if (!row) return
|
if (!row) return
|
||||||
|
row._editPrevFv = row.fvValue
|
||||||
|
row._editPrevReg = row.fvRegistrationFee
|
||||||
editingKey.value = cellKey(row, field)
|
editingKey.value = cellKey(row, field)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const el = editInputRefs[editingKey.value]
|
const el = editInputRefs[editingKey.value]
|
||||||
@@ -143,13 +147,36 @@ function startEdit(row, field) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isBelowFloor(value, floor) {
|
||||||
|
if (value == null || value === '' || floor == null || floor === '') return false
|
||||||
|
const n = Number(value)
|
||||||
|
const f = Number(floor)
|
||||||
|
if (Number.isNaN(n) || Number.isNaN(f)) return false
|
||||||
|
return n + 1e-8 < f
|
||||||
|
}
|
||||||
|
|
||||||
|
function belowFloorMessage(field, floor) {
|
||||||
|
const name = field === 'fv' ? '\u5957\u9910\u8fd0\u8d39' : '\u5957\u9910\u6302\u53f7\u8d39'
|
||||||
|
const prec = field === 'fv' ? 4 : 2
|
||||||
|
return name + '\u4e0d\u80fd\u4f4e\u4e8e\u5bfc\u5165\u57fa\u51c6\u4ef7 ' + formatNum(floor, prec)
|
||||||
|
}
|
||||||
|
|
||||||
async function commitEdit(row, field) {
|
async function commitEdit(row, field) {
|
||||||
if (committing.value) return
|
if (committing.value) return
|
||||||
if (editingKey.value !== cellKey(row, field)) return
|
if (editingKey.value !== cellKey(row, field)) return
|
||||||
editingKey.value = ''
|
|
||||||
if (!row || !row.psdId) return
|
|
||||||
committing.value = true
|
committing.value = true
|
||||||
|
editingKey.value = ''
|
||||||
try {
|
try {
|
||||||
|
if (!row) return
|
||||||
|
const floor = field === 'fv' ? row._floorFreight : row._floorReg
|
||||||
|
const value = field === 'fv' ? row.fvValue : row.fvRegistrationFee
|
||||||
|
if (isBelowFloor(value, floor)) {
|
||||||
|
if (field === 'fv') row.fvValue = row._editPrevFv
|
||||||
|
else row.fvRegistrationFee = row._editPrevReg
|
||||||
|
proxy.$modal.msgError(belowFloorMessage(field, floor))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!row.psdId) return
|
||||||
emit('row-change', row)
|
emit('row-change', row)
|
||||||
} finally {
|
} finally {
|
||||||
committing.value = false
|
committing.value = false
|
||||||
|
|||||||
@@ -268,6 +268,8 @@ function countryToDetail(item, productCode, productName) {
|
|||||||
const baseReg = item.registrationFee != null ? Number(item.registrationFee) : undefined
|
const baseReg = item.registrationFee != null ? Number(item.registrationFee) : undefined
|
||||||
row._baseFreight = baseFv
|
row._baseFreight = baseFv
|
||||||
row._baseReg = baseReg
|
row._baseReg = baseReg
|
||||||
|
row._floorFreight = baseFv
|
||||||
|
row._floorReg = baseReg
|
||||||
row.fvValue = baseFv
|
row.fvValue = baseFv
|
||||||
row.fvRegistrationFee = baseReg
|
row.fvRegistrationFee = baseReg
|
||||||
row.effectiveDate = item.effectiveDate
|
row.effectiveDate = item.effectiveDate
|
||||||
@@ -293,7 +295,9 @@ function openPrice(row) {
|
|||||||
return r
|
return r
|
||||||
})
|
})
|
||||||
editGroup.value = row
|
editGroup.value = row
|
||||||
priceVisible.value = true
|
Promise.resolve(stampPriceFloors(editRows.value, row.productCode)).finally(() => {
|
||||||
|
priceVisible.value = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 抽屉里点附加费:带行 = 行级(只改该行,国家+重量段),不带 = 产品级(覆盖全部明细行) */
|
/** 抽屉里点附加费:带行 = 行级(只改该行,国家+重量段),不带 = 产品级(覆盖全部明细行) */
|
||||||
@@ -319,10 +323,45 @@ async function persistDetail(row, withSurcharge) {
|
|||||||
/** drawer field change -> immediate DB update */
|
/** drawer field change -> immediate DB update */
|
||||||
async function onRowPriceChange(row) {
|
async function onRowPriceChange(row) {
|
||||||
if (!row || !row.psdId) return
|
if (!row || !row.psdId) return
|
||||||
|
const prevFv = row._editPrevFv
|
||||||
|
const prevReg = row._editPrevReg
|
||||||
try {
|
try {
|
||||||
await persistDetail(row, false)
|
await persistDetail(row, false)
|
||||||
proxy.$modal.msgSuccess('价格已更新')
|
proxy.$modal.msgSuccess('价格已更新')
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
if (prevFv !== undefined) row.fvValue = prevFv
|
||||||
|
if (prevReg !== undefined) row.fvRegistrationFee = prevReg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyImportedFloor(row, hit) {
|
||||||
|
if (!row || !hit) return
|
||||||
|
if (hit.freightPrice != null) row._floorFreight = Number(hit.freightPrice)
|
||||||
|
if (hit.registrationFee != null) row._floorReg = Number(hit.registrationFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
function findImportedCountry(rows, row) {
|
||||||
|
if (!rows || !rows.length || !row) return null
|
||||||
|
if (row.productCountryId != null) {
|
||||||
|
const byId = rows.find(item => Number(item.detailId) === Number(row.productCountryId))
|
||||||
|
if (byId) return byId
|
||||||
|
}
|
||||||
|
return rows.find(item => {
|
||||||
|
const sameCountry = (row.countryCode && item.countryCode
|
||||||
|
&& String(row.countryCode).toUpperCase() === String(item.countryCode).toUpperCase())
|
||||||
|
|| (!row.countryCode && row.countryName && item.countryName && row.countryName === item.countryName)
|
||||||
|
if (!sameCountry) return false
|
||||||
|
if (row.weightMin != null && item.weightMin != null && Number(row.weightMin) !== Number(item.weightMin)) return false
|
||||||
|
if (row.weightMax != null && item.weightMax != null && Number(row.weightMax) !== Number(item.weightMax)) return false
|
||||||
|
return row.weightMin != null || row.weightMax != null
|
||||||
|
}) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function stampPriceFloors(rows, productCode) {
|
||||||
|
if (!props.refData || !productCode || !rows || !rows.length) return Promise.resolve()
|
||||||
|
return props.refData.ensureCountries(productCode, props.effectiveTime).then(list => {
|
||||||
|
rows.forEach(row => applyImportedFloor(row, findImportedCountry(list, row)))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveDiscount() {
|
async function saveDiscount() {
|
||||||
|
|||||||
Reference in New Issue
Block a user