增加导入模版逻辑判断
This commit is contained in:
+577
-6
@@ -383,12 +383,16 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
|
|
||||||
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
||||||
{
|
{
|
||||||
|
assertNotSingleCountryQuoteForMultiImport(workbook);
|
||||||
|
if (workbookHasInlineProductColumns(workbook))
|
||||||
|
{
|
||||||
|
return importProductExcelByInline(workbook, bytes, filename, operName, taskId, forcedEff);
|
||||||
|
}
|
||||||
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||||
if (catalog == null)
|
if (catalog == null)
|
||||||
{
|
{
|
||||||
throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound"));
|
throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound"));
|
||||||
}
|
}
|
||||||
assertNotSingleCountryQuoteForMultiImport(workbook);
|
|
||||||
|
|
||||||
List<BmsProduct> products = parseCatalog(catalog);
|
List<BmsProduct> products = parseCatalog(catalog);
|
||||||
if (products.isEmpty())
|
if (products.isEmpty())
|
||||||
@@ -433,7 +437,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
int[] nums = importOneProductWithDeadlockRetry(workbook, product, operName, regionByNameCn,
|
int[] nums = importOneProductWithDeadlockRetry(workbook, product, operName, regionByNameCn,
|
||||||
euRegions, zoneByCountryAndCode, unmatchedCountries, forcedEff, importPsvId);
|
euRegions, zoneByCountryAndCode, unmatchedCountries, forcedEff, importPsvId, null);
|
||||||
successNum += nums[0];
|
successNum += nums[0];
|
||||||
skipNum += nums[1];
|
skipNum += nums[1];
|
||||||
countryNum += nums[2];
|
countryNum += nums[2];
|
||||||
@@ -684,12 +688,18 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
}
|
}
|
||||||
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
||||||
{
|
{
|
||||||
|
assertNotSingleCountryQuoteForMultiImport(workbook);
|
||||||
|
// Detail sheets with inline product-code / product-name column: identify by those
|
||||||
|
// columns, not by catalog sheet-name matching (variants like RM/IP/EV share one sheet).
|
||||||
|
if (workbookHasInlineProductColumns(workbook))
|
||||||
|
{
|
||||||
|
return readQuoteWorkbookByInlineProductCode(workbook);
|
||||||
|
}
|
||||||
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||||
if (catalog == null)
|
if (catalog == null)
|
||||||
{
|
{
|
||||||
throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound"));
|
throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound"));
|
||||||
}
|
}
|
||||||
assertNotSingleCountryQuoteForMultiImport(workbook);
|
|
||||||
List<BmsProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
|
List<BmsProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
|
||||||
if (products.isEmpty())
|
if (products.isEmpty())
|
||||||
{
|
{
|
||||||
@@ -791,6 +801,543 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
private Map<String, List<BmsProductCountry>> byCode;
|
private Map<String, List<BmsProductCountry>> byCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formal product import when price sheets carry inline product-code / product-name columns.
|
||||||
|
* Identifies products from detail rows (not catalog sheet-name matching).
|
||||||
|
*/
|
||||||
|
private String importProductExcelByInline(Workbook workbook, byte[] bytes, String filename,
|
||||||
|
String operName, String taskId, Date forcedEff) throws Exception
|
||||||
|
{
|
||||||
|
QuoteWorkbook parsed = readQuoteWorkbookByInlineProductCode(workbook);
|
||||||
|
List<BmsProduct> products = enrichInlineProductsFromCatalog(workbook, parsed.products);
|
||||||
|
if (products.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
|
||||||
|
}
|
||||||
|
reportImportProgress(taskId, 20, "parsing excel");
|
||||||
|
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
|
||||||
|
|
||||||
|
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
|
||||||
|
if (regionByNameCn.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.regionEmpty"));
|
||||||
|
}
|
||||||
|
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||||
|
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||||
|
|
||||||
|
Long importPsvId = forcedEff == null
|
||||||
|
? catalogVersionSupport.ensureDraftVersion(PriceType.PRODUCT, null, false).getPsvId()
|
||||||
|
: catalogVersionSupport.requirePsvId(PriceType.PRODUCT, null, forcedEff);
|
||||||
|
priceExcelFileService.saveImportExcel(
|
||||||
|
PrcPriceExcelFile.TYPE_PRODUCT, forcedEff, bytes, filename, operName, importPsvId);
|
||||||
|
|
||||||
|
int successNum = 0;
|
||||||
|
int skipNum = 0;
|
||||||
|
int countryNum = 0;
|
||||||
|
Set<String> unmatchedCountries = new HashSet<String>();
|
||||||
|
StringBuilder failureMsg = new StringBuilder();
|
||||||
|
int total = products.size();
|
||||||
|
int index = 0;
|
||||||
|
for (BmsProduct product : products)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
if (product == null || StringUtils.isEmpty(product.getProductCode()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String code = product.getProductCode().trim().toUpperCase(Locale.ROOT);
|
||||||
|
reportImportProductProgress(taskId, index, total, product.getProductCode());
|
||||||
|
List<BmsProductCountry> countries = parsed.byCode.get(code);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int[] nums = importOneProductWithDeadlockRetry(workbook, product, operName, regionByNameCn,
|
||||||
|
euRegions, zoneByCountryAndCode, unmatchedCountries, forcedEff, importPsvId, countries);
|
||||||
|
successNum += nums[0];
|
||||||
|
skipNum += nums[1];
|
||||||
|
countryNum += nums[2];
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
String tip = e.getMessage();
|
||||||
|
if (StringUtils.isEmpty(tip))
|
||||||
|
{
|
||||||
|
tip = e.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
failureMsg.append("<br/>").append(product.getProductCode()).append(":").append(tip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reportImportProgress(taskId, 95, "finishing");
|
||||||
|
if (failureMsg.length() > 0)
|
||||||
|
{
|
||||||
|
return MessageUtils.message("price.product.import.partialFail",
|
||||||
|
Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString());
|
||||||
|
}
|
||||||
|
String msg = MessageUtils.message("price.product.import.success", successNum, skipNum, countryNum)
|
||||||
|
+ "<br/>" + MessageUtils.message("price.product.import.success.detail1")
|
||||||
|
+ "<br/>" + MessageUtils.message("price.product.import.success.detail2");
|
||||||
|
if (!unmatchedCountries.isEmpty())
|
||||||
|
{
|
||||||
|
msg += "<br/>" + MessageUtils.message("price.product.import.success.unmatched",
|
||||||
|
String.join("、", unmatchedCountries));
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prefer catalog metadata when code matches; keep detail product name for variants. */
|
||||||
|
private List<BmsProduct> enrichInlineProductsFromCatalog(Workbook workbook, List<BmsProduct> inlineProducts)
|
||||||
|
{
|
||||||
|
Map<String, BmsProduct> catalogByCode = new HashMap<String, BmsProduct>();
|
||||||
|
Sheet catalog = workbook == null ? null : workbook.getSheet(CATALOG_SHEET);
|
||||||
|
if (catalog != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (BmsProduct p : dedupeCatalogProducts(parseCatalog(catalog)))
|
||||||
|
{
|
||||||
|
if (p == null || StringUtils.isEmpty(p.getProductCode()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catalogByCode.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ignored)
|
||||||
|
{
|
||||||
|
// Catalog optional for inline import.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<BmsProduct> out = new ArrayList<BmsProduct>();
|
||||||
|
if (inlineProducts == null)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (BmsProduct inline : inlineProducts)
|
||||||
|
{
|
||||||
|
if (inline == null || StringUtils.isEmpty(inline.getProductCode()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String code = inline.getProductCode().trim().toUpperCase(Locale.ROOT);
|
||||||
|
BmsProduct cat = catalogByCode.get(code);
|
||||||
|
if (cat == null)
|
||||||
|
{
|
||||||
|
out.add(inline);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BmsProduct merged = new BmsProduct();
|
||||||
|
merged.setProductCode(inline.getProductCode());
|
||||||
|
merged.setProductName(StringUtils.isNotEmpty(inline.getProductName())
|
||||||
|
? inline.getProductName() : cat.getProductName());
|
||||||
|
merged.setServiceLevel(cat.getServiceLevel());
|
||||||
|
merged.setCargoAttr(cat.getCargoAttr());
|
||||||
|
merged.setDestRegion(cat.getDestRegion());
|
||||||
|
merged.setUpdateNote(cat.getUpdateNote());
|
||||||
|
merged.setEffectiveDate(cat.getEffectiveDate());
|
||||||
|
merged.setTaxMode(StringUtils.isNotEmpty(cat.getTaxMode())
|
||||||
|
? cat.getTaxMode() : resolveTaxMode(merged.getProductName()));
|
||||||
|
out.add(merged);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when any multi-country price sheet has an inline product-code or product-name column
|
||||||
|
* (产品代码 / 产品对接代码 / 产品名称).
|
||||||
|
*/
|
||||||
|
private boolean workbookHasInlineProductColumns(Workbook workbook)
|
||||||
|
{
|
||||||
|
if (workbook == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||||
|
{
|
||||||
|
String sheetName = workbook.getSheetName(i);
|
||||||
|
if (shouldSkipCountryPriceSheet(sheetName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Sheet sheet = workbook.getSheetAt(i);
|
||||||
|
if (sheet == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int[] countryHeader = findHeaderCell(sheet, "国家");
|
||||||
|
if (countryHeader == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Row head = sheet.getRow(countryHeader[0]);
|
||||||
|
if (head == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (firstCol(head, "产品代码", "产品对接代码") >= 0
|
||||||
|
|| findColByHeader(head, "产品名称", 0) >= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-country parse keyed by inline product-code column on price sheets.
|
||||||
|
* Does not use 产品目录 sheet matching.
|
||||||
|
*/
|
||||||
|
private QuoteWorkbook readQuoteWorkbookByInlineProductCode(Workbook workbook) throws Exception
|
||||||
|
{
|
||||||
|
Map<String, String> catalogCodeByName = buildCatalogCodeByName(workbook);
|
||||||
|
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
|
||||||
|
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||||
|
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||||
|
Set<String> unmatchedCountries = new HashSet<String>();
|
||||||
|
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<String, List<BmsProductCountry>>();
|
||||||
|
Map<String, BmsProduct> productMeta = new LinkedHashMap<String, BmsProduct>();
|
||||||
|
List<String> tips = new ArrayList<String>();
|
||||||
|
|
||||||
|
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||||
|
{
|
||||||
|
String sheetName = workbook.getSheetName(i);
|
||||||
|
if (shouldSkipCountryPriceSheet(sheetName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Sheet sheet = workbook.getSheetAt(i);
|
||||||
|
if (sheet == null || findHeaderCell(sheet, "国家") == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, List<BmsProductCountry>> sheetByCode = parseProductSheetByInlineCode(
|
||||||
|
sheet, catalogCodeByName, regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
|
||||||
|
if (sheetByCode == null || sheetByCode.isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, List<BmsProductCountry>> e : sheetByCode.entrySet())
|
||||||
|
{
|
||||||
|
if (e == null || StringUtils.isEmpty(e.getKey()) || e.getValue() == null || e.getValue().isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String code = e.getKey();
|
||||||
|
List<BmsProductCountry> rows = e.getValue();
|
||||||
|
List<BmsProductCountry> existing = out.get(code);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
out.put(code, new ArrayList<BmsProductCountry>(rows));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.addAll(rows);
|
||||||
|
}
|
||||||
|
if (!productMeta.containsKey(code))
|
||||||
|
{
|
||||||
|
BmsProduct p = new BmsProduct();
|
||||||
|
p.setProductCode(code);
|
||||||
|
String name = null;
|
||||||
|
for (BmsProductCountry r : rows)
|
||||||
|
{
|
||||||
|
if (r != null && StringUtils.isNotEmpty(r.getProductName()))
|
||||||
|
{
|
||||||
|
name = r.getProductName();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.setProductName(StringUtils.isNotEmpty(name) ? name : code);
|
||||||
|
p.setTaxMode(resolveTaxMode(p.getProductName()));
|
||||||
|
productMeta.put(code, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<BmsProductCountry>> e : out.entrySet())
|
||||||
|
{
|
||||||
|
BmsProduct p = productMeta.get(e.getKey());
|
||||||
|
String tip = buildDuplicateCountryWeightTip(p, e.getValue());
|
||||||
|
if (StringUtils.isNotEmpty(tip))
|
||||||
|
{
|
||||||
|
tips.add(tip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tips.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.dupCountry",
|
||||||
|
String.join("<br/>", tips)));
|
||||||
|
}
|
||||||
|
if (out.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
|
||||||
|
}
|
||||||
|
QuoteWorkbook parsed = new QuoteWorkbook();
|
||||||
|
parsed.products = new ArrayList<BmsProduct>(productMeta.values());
|
||||||
|
parsed.byCode = out;
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one multi-country price sheet grouped by inline product code / product name.
|
||||||
|
* Prefer product-code column; if only product-name exists, resolve code via catalog name map.
|
||||||
|
* Country / product-code / product-name / size / last-mile / lead-time carry forward
|
||||||
|
* when subsequent weight-band rows leave those cells blank.
|
||||||
|
*/
|
||||||
|
private Map<String, List<BmsProductCountry>> parseProductSheetByInlineCode(Sheet sheet,
|
||||||
|
Map<String, String> catalogCodeByName, Map<String, BmsRegion> regionByNameCn,
|
||||||
|
List<BmsRegion> euRegions, Map<String, PrcZone> zoneByCountryAndCode,
|
||||||
|
Set<String> unmatchedCountries)
|
||||||
|
{
|
||||||
|
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<String, List<BmsProductCountry>>();
|
||||||
|
if (sheet == null)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
Date sheetEffective = parseSheetEffectiveDate(sheet);
|
||||||
|
int[] countryHeader = findHeaderCell(sheet, "国家");
|
||||||
|
if (countryHeader == null)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
int headerRowIdx = countryHeader[0];
|
||||||
|
Row headerRow = sheet.getRow(headerRowIdx);
|
||||||
|
int colCountry = countryHeader[1];
|
||||||
|
int colProductCode = firstCol(headerRow, "产品代码", "产品对接代码");
|
||||||
|
int colProductName = findColByHeader(headerRow, "产品名称", 0);
|
||||||
|
if (colProductCode < 0 && colProductName < 0)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
int colWeight = findColByHeader(headerRow, "重量", colCountry + 1);
|
||||||
|
int colFreight = findColByHeader(headerRow, "运费", colCountry + 1);
|
||||||
|
int colRegister = findColByHeader(headerRow, "挂号", colCountry + 1);
|
||||||
|
int colSize = findColByHeader(headerRow, "尺寸", colCountry + 1);
|
||||||
|
int colLastMile = findColByHeader(headerRow, "尾程", colCountry + 1);
|
||||||
|
int colLeadTime = findColByHeader(headerRow, "时效", colCountry + 1);
|
||||||
|
if (colWeight < 0 || colFreight < 0)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentCountryRaw = null;
|
||||||
|
String currentProductCode = null;
|
||||||
|
String currentProductName = null;
|
||||||
|
String currentSize = null;
|
||||||
|
String currentLastMile = null;
|
||||||
|
String currentLeadTime = null;
|
||||||
|
int lastRow = sheet.getLastRowNum();
|
||||||
|
for (int i = headerRowIdx + 1; i <= lastRow; i++)
|
||||||
|
{
|
||||||
|
Row row = sheet.getRow(i);
|
||||||
|
if (row == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String countryCell = cellString(row.getCell(colCountry));
|
||||||
|
String weightLabel = cellString(row.getCell(colWeight));
|
||||||
|
if ("国家".equals(countryCell)
|
||||||
|
|| (weightLabel != null && weightLabel.contains("重量段")))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(countryCell))
|
||||||
|
{
|
||||||
|
currentCountryRaw = countryCell;
|
||||||
|
}
|
||||||
|
boolean productChanged = false;
|
||||||
|
if (colProductCode >= 0)
|
||||||
|
{
|
||||||
|
String codeCell = normalizeImportProductCode(cellString(row.getCell(colProductCode)));
|
||||||
|
if (StringUtils.isNotEmpty(codeCell))
|
||||||
|
{
|
||||||
|
String newCode = codeCell.trim().toUpperCase(Locale.ROOT);
|
||||||
|
if (currentProductCode == null || !newCode.equals(currentProductCode))
|
||||||
|
{
|
||||||
|
productChanged = true;
|
||||||
|
currentProductCode = newCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (colProductName >= 0)
|
||||||
|
{
|
||||||
|
String nameCell = cellString(row.getCell(colProductName));
|
||||||
|
if (StringUtils.isNotEmpty(nameCell) && !isSectionTitle(nameCell))
|
||||||
|
{
|
||||||
|
String newName = nameCell.replace("\n", " ").replace("\r", " ").trim();
|
||||||
|
if (currentProductName == null || !newName.equals(currentProductName))
|
||||||
|
{
|
||||||
|
productChanged = true;
|
||||||
|
currentProductName = newName;
|
||||||
|
// Name-only sheets: resolve product code from catalog by product name.
|
||||||
|
if (colProductCode < 0)
|
||||||
|
{
|
||||||
|
String mapped = resolveCodeByProductName(currentProductName, catalogCodeByName);
|
||||||
|
if (StringUtils.isNotEmpty(mapped))
|
||||||
|
{
|
||||||
|
currentProductCode = mapped;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentProductCode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (productChanged)
|
||||||
|
{
|
||||||
|
currentSize = null;
|
||||||
|
currentLastMile = null;
|
||||||
|
currentLeadTime = null;
|
||||||
|
}
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(currentCountryRaw) || StringUtils.isEmpty(currentProductCode))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal[] range = parseWeightRange(weightLabel);
|
||||||
|
if (range == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal freight = cellDecimal(row.getCell(colFreight));
|
||||||
|
BigDecimal registration = colRegister >= 0 ? cellDecimal(row.getCell(colRegister)) : null;
|
||||||
|
Date eff = sheetEffective;
|
||||||
|
|
||||||
|
ZoneCodeAbbrevs.CountryZoneParts parts = ZoneCodeAbbrevs.splitCountryZone(currentCountryRaw);
|
||||||
|
if (parts == null || StringUtils.isEmpty(parts.countryName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String baseCountry = parts.countryName;
|
||||||
|
String zoneLabel = parts.zoneLabel;
|
||||||
|
List<BmsProductCountry> bucket = out.get(currentProductCode);
|
||||||
|
if (bucket == null)
|
||||||
|
{
|
||||||
|
bucket = new ArrayList<BmsProductCountry>();
|
||||||
|
out.put(currentProductCode, bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
BmsRegion region = resolveRegion(baseCountry, regionByNameCn);
|
||||||
|
if (region != null)
|
||||||
|
{
|
||||||
|
BmsProductCountry c = buildCountryRow(region.getCountryNameCn(), region.getCountryCode2(),
|
||||||
|
zoneLabel, zoneByCountryAndCode,
|
||||||
|
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
|
||||||
|
c.setProductCode(currentProductCode);
|
||||||
|
c.setProductName(currentProductName);
|
||||||
|
bucket.add(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isEuZoneName(baseCountry) && StringUtils.isEmpty(zoneLabel))
|
||||||
|
{
|
||||||
|
if (euRegions == null || euRegions.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.euEmpty"));
|
||||||
|
}
|
||||||
|
for (BmsRegion eu : euRegions)
|
||||||
|
{
|
||||||
|
BmsProductCountry c = buildCountryRow(eu.getCountryNameCn(), eu.getCountryCode2(),
|
||||||
|
null, zoneByCountryAndCode,
|
||||||
|
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
|
||||||
|
c.setProductCode(currentProductCode);
|
||||||
|
c.setProductName(currentProductName);
|
||||||
|
bucket.add(c);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
unmatchedCountries.add(currentCountryRaw.trim().replace("\n", " ").replace("\r", " "));
|
||||||
|
BmsProductCountry c = buildCountryRow(baseCountry, null, zoneLabel, zoneByCountryAndCode,
|
||||||
|
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
|
||||||
|
c.setProductCode(currentProductCode);
|
||||||
|
c.setProductName(currentProductName);
|
||||||
|
bucket.add(c);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build productName -> productCode from 产品目录 (first row per name wins). */
|
||||||
|
private Map<String, String> buildCatalogCodeByName(Workbook workbook)
|
||||||
|
{
|
||||||
|
Map<String, String> map = new LinkedHashMap<String, String>();
|
||||||
|
if (workbook == null)
|
||||||
|
{
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||||
|
if (catalog == null)
|
||||||
|
{
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<BmsProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
|
||||||
|
for (BmsProduct p : products)
|
||||||
|
{
|
||||||
|
if (p == null || StringUtils.isEmpty(p.getProductCode()) || StringUtils.isEmpty(p.getProductName()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String nameKey = compactName(p.getProductName());
|
||||||
|
if (StringUtils.isEmpty(nameKey) || map.containsKey(nameKey))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
map.put(nameKey, p.getProductCode().trim().toUpperCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ignored)
|
||||||
|
{
|
||||||
|
// Catalog optional for inline identity; ignore parse errors.
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve product code by product name; exact compact match, then base name before '(' / '('. */
|
||||||
|
private String resolveCodeByProductName(String productName, Map<String, String> catalogCodeByName)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(productName) || catalogCodeByName == null || catalogCodeByName.isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String key = compactName(productName);
|
||||||
|
String hit = catalogCodeByName.get(key);
|
||||||
|
if (StringUtils.isNotEmpty(hit))
|
||||||
|
{
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
String base = productName.trim();
|
||||||
|
int idx = base.indexOf('(');
|
||||||
|
int idx2 = base.indexOf('(');
|
||||||
|
int cut = -1;
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
cut = idx;
|
||||||
|
}
|
||||||
|
if (idx2 >= 0 && (cut < 0 || idx2 < cut))
|
||||||
|
{
|
||||||
|
cut = idx2;
|
||||||
|
}
|
||||||
|
if (cut > 0)
|
||||||
|
{
|
||||||
|
hit = catalogCodeByName.get(compactName(base.substring(0, cut)));
|
||||||
|
if (StringUtils.isNotEmpty(hit))
|
||||||
|
{
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multi-country import expects price sheets with a "country" column.
|
* Multi-country import expects price sheets with a "country" column.
|
||||||
* Single-country dedicated-line sheets use product-name + weight blocks instead.
|
* Single-country dedicated-line sheets use product-name + weight blocks instead.
|
||||||
@@ -1293,7 +1840,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
*/
|
*/
|
||||||
private int[] importOneProductWithDeadlockRetry(Workbook workbook, BmsProduct product, String operName,
|
private int[] importOneProductWithDeadlockRetry(Workbook workbook, BmsProduct product, String operName,
|
||||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||||
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries, Date forcedEff, Long importPsvId)
|
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries, Date forcedEff, Long importPsvId,
|
||||||
|
List<BmsProductCountry> preParsedCountries)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
int max = 8;
|
int max = 8;
|
||||||
@@ -1310,8 +1858,10 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
{
|
{
|
||||||
product.setPsvId(importPsvId);
|
product.setPsvId(importPsvId);
|
||||||
}
|
}
|
||||||
int[] nums = importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
|
int[] nums = preParsedCountries != null
|
||||||
zoneByCountryAndCode, unmatchedCountries, forcedEff);
|
? importOneProductFromRows(product, preParsedCountries, operName, forcedEff)
|
||||||
|
: importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
|
||||||
|
zoneByCountryAndCode, unmatchedCountries, forcedEff);
|
||||||
transactionManager.commit(tx);
|
transactionManager.commit(tx);
|
||||||
return nums;
|
return nums;
|
||||||
}
|
}
|
||||||
@@ -1461,6 +2011,27 @@ public class BmsProductServiceImpl implements IBmsProductService
|
|||||||
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
|
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
|
||||||
productSheet.getSheetName()));
|
productSheet.getSheetName()));
|
||||||
}
|
}
|
||||||
|
return importOneProductFromRows(product, countries, operName, forcedEffectiveDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Import using pre-parsed country rows (inline product-code / product-name path). */
|
||||||
|
private int[] importOneProductFromRows(BmsProduct product, List<BmsProductCountry> countries,
|
||||||
|
String operName, Date forcedEffectiveDate)
|
||||||
|
{
|
||||||
|
if (product != null)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(product.getSourceSheet()))
|
||||||
|
{
|
||||||
|
product.setSourceSheet(product.getProductName());
|
||||||
|
}
|
||||||
|
product.setCreateBy(operName);
|
||||||
|
product.setUpdateBy(operName);
|
||||||
|
}
|
||||||
|
if (countries == null || countries.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
|
||||||
|
product == null ? "" : product.getProductCode()));
|
||||||
|
}
|
||||||
assertNoDuplicateCountryWeightRows(product, countries);
|
assertNoDuplicateCountryWeightRows(product, countries);
|
||||||
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
|
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
|
||||||
Date newEff = forcedEffectiveDate == null && product.getEffectiveDate() == null
|
Date newEff = forcedEffectiveDate == null && product.getEffectiveDate() == null
|
||||||
|
|||||||
+604
-2
@@ -362,6 +362,10 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
|||||||
|
|
||||||
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
||||||
{
|
{
|
||||||
|
if (workbookHasInlineProductColumns(workbook))
|
||||||
|
{
|
||||||
|
return importRedlineExcelByInline(workbook, bytes, filename, operName, taskId, forcedEff);
|
||||||
|
}
|
||||||
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||||
if (catalog == null)
|
if (catalog == null)
|
||||||
{
|
{
|
||||||
@@ -665,6 +669,10 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
|||||||
}
|
}
|
||||||
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
||||||
{
|
{
|
||||||
|
if (workbookHasInlineProductColumns(workbook))
|
||||||
|
{
|
||||||
|
return readQuoteWorkbookByInlineProductCode(workbook).byCode;
|
||||||
|
}
|
||||||
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||||
if (catalog == null)
|
if (catalog == null)
|
||||||
{
|
{
|
||||||
@@ -712,9 +720,15 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
|||||||
@Override
|
@Override
|
||||||
public Map<String, Object> previewImportCatalog(byte[] bytes, String filename) throws Exception
|
public Map<String, Object> previewImportCatalog(byte[] bytes, String filename) throws Exception
|
||||||
{
|
{
|
||||||
Map<String, List<BmsRedlineProductCountry>> byCode = parseQuoteExcelByProduct(bytes, filename);
|
|
||||||
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
||||||
{
|
{
|
||||||
|
if (workbookHasInlineProductColumns(workbook))
|
||||||
|
{
|
||||||
|
RedlineQuoteWorkbook parsed = readQuoteWorkbookByInlineProductCode(workbook);
|
||||||
|
List<BmsRedlineProduct> products = enrichInlineProductsFromCatalog(workbook, parsed.products);
|
||||||
|
return buildCatalogPreview(products, parsed.byCode);
|
||||||
|
}
|
||||||
|
Map<String, List<BmsRedlineProductCountry>> byCode = parseQuoteExcelByProduct(bytes, filename);
|
||||||
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||||
List<BmsRedlineProduct> catalogProducts = dedupeCatalogProducts(parseCatalog(catalog));
|
List<BmsRedlineProduct> catalogProducts = dedupeCatalogProducts(parseCatalog(catalog));
|
||||||
return buildCatalogPreview(catalogProducts, byCode);
|
return buildCatalogPreview(catalogProducts, byCode);
|
||||||
@@ -1205,8 +1219,28 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
|||||||
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
|
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
|
||||||
productSheet.getSheetName()));
|
productSheet.getSheetName()));
|
||||||
}
|
}
|
||||||
|
return importOneProductFromRows(product, countries, operName, forcedEffectiveDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Import using pre-parsed country rows (inline product-code / product-name path). */
|
||||||
|
private int[] importOneProductFromRows(BmsRedlineProduct product, List<BmsRedlineProductCountry> countries,
|
||||||
|
String operName, Date forcedEffectiveDate)
|
||||||
|
{
|
||||||
|
if (product != null)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(product.getSourceSheet()))
|
||||||
|
{
|
||||||
|
product.setSourceSheet(product.getProductName());
|
||||||
|
}
|
||||||
|
product.setCreateBy(operName);
|
||||||
|
product.setUpdateBy(operName);
|
||||||
|
}
|
||||||
|
if (countries == null || countries.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
|
||||||
|
product == null ? "" : product.getProductCode()));
|
||||||
|
}
|
||||||
assertNoDuplicateCountryWeightRows(product, countries);
|
assertNoDuplicateCountryWeightRows(product, countries);
|
||||||
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
|
|
||||||
Date newEff = forcedEffectiveDate == null && product.getEffectiveDate() == null
|
Date newEff = forcedEffectiveDate == null && product.getEffectiveDate() == null
|
||||||
? null
|
? null
|
||||||
: requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
|
: requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
|
||||||
@@ -2447,6 +2481,574 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static final class RedlineQuoteWorkbook
|
||||||
|
{
|
||||||
|
private List<BmsRedlineProduct> products;
|
||||||
|
private Map<String, List<BmsRedlineProductCountry>> byCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] importOneProductInline(BmsRedlineProduct product, List<BmsRedlineProductCountry> countries,
|
||||||
|
String operName, Date forcedEff, Long importPsvId) throws Exception
|
||||||
|
{
|
||||||
|
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
|
||||||
|
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||||
|
TransactionStatus tx = transactionManager.getTransaction(def);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
product.setEffectiveDate(forcedEff);
|
||||||
|
if (importPsvId != null)
|
||||||
|
{
|
||||||
|
product.setPsvId(importPsvId);
|
||||||
|
}
|
||||||
|
int[] nums = importOneProductFromRows(product, countries, operName, forcedEff);
|
||||||
|
transactionManager.commit(tx);
|
||||||
|
return nums;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
try { transactionManager.rollback(tx); } catch (Exception ignore) { }
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formal redline import when price sheets carry inline product-code / product-name columns.
|
||||||
|
* Identifies products from detail rows (not catalog sheet-name matching).
|
||||||
|
*/
|
||||||
|
private String importRedlineExcelByInline(Workbook workbook, byte[] bytes, String filename,
|
||||||
|
String operName, String taskId, Date forcedEff) throws Exception
|
||||||
|
{
|
||||||
|
RedlineQuoteWorkbook parsed = readQuoteWorkbookByInlineProductCode(workbook);
|
||||||
|
List<BmsRedlineProduct> products = enrichInlineProductsFromCatalog(workbook, parsed.products);
|
||||||
|
if (products.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
|
||||||
|
}
|
||||||
|
reportImportProgress(taskId, 20, "parsing excel");
|
||||||
|
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
|
||||||
|
|
||||||
|
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
|
||||||
|
if (regionByNameCn.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.regionEmpty"));
|
||||||
|
}
|
||||||
|
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||||
|
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||||
|
|
||||||
|
Long importPsvId = forcedEff == null
|
||||||
|
? catalogVersionSupport.ensureDraftVersion(PriceType.REDLINE, null, false).getPsvId()
|
||||||
|
: catalogVersionSupport.requirePsvId(PriceType.REDLINE, null, forcedEff);
|
||||||
|
priceExcelFileService.saveImportExcel(
|
||||||
|
PrcPriceExcelFile.TYPE_REDLINE, forcedEff, bytes, filename, operName, importPsvId);
|
||||||
|
|
||||||
|
int successNum = 0;
|
||||||
|
int skipNum = 0;
|
||||||
|
int countryNum = 0;
|
||||||
|
Set<String> unmatchedCountries = new HashSet<String>();
|
||||||
|
StringBuilder failureMsg = new StringBuilder();
|
||||||
|
int total = products.size();
|
||||||
|
int index = 0;
|
||||||
|
for (BmsRedlineProduct product : products)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
if (product == null || StringUtils.isEmpty(product.getProductCode()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String code = product.getProductCode().trim().toUpperCase(Locale.ROOT);
|
||||||
|
reportImportProductProgress(taskId, index, total, product.getProductCode());
|
||||||
|
List<BmsRedlineProductCountry> countries = parsed.byCode.get(code);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int[] nums = importOneProductInline(product, countries, operName, forcedEff, importPsvId);
|
||||||
|
successNum += nums[0];
|
||||||
|
skipNum += nums[1];
|
||||||
|
countryNum += nums[2];
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
String tip = e.getMessage();
|
||||||
|
if (StringUtils.isEmpty(tip))
|
||||||
|
{
|
||||||
|
tip = e.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
failureMsg.append("<br/>").append(product.getProductCode()).append(":").append(tip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reportImportProgress(taskId, 95, "finishing");
|
||||||
|
if (failureMsg.length() > 0)
|
||||||
|
{
|
||||||
|
return MessageUtils.message("price.product.import.partialFail",
|
||||||
|
Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString());
|
||||||
|
}
|
||||||
|
String msg = MessageUtils.message("price.product.import.success", successNum, skipNum, countryNum)
|
||||||
|
+ "<br/>" + MessageUtils.message("price.product.import.success.detail1")
|
||||||
|
+ "<br/>" + MessageUtils.message("price.product.import.success.detail2");
|
||||||
|
if (!unmatchedCountries.isEmpty())
|
||||||
|
{
|
||||||
|
msg += "<br/>" + MessageUtils.message("price.product.import.success.unmatched",
|
||||||
|
String.join("、", unmatchedCountries));
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prefer catalog metadata when code matches; keep detail product name for variants. */
|
||||||
|
private List<BmsRedlineProduct> enrichInlineProductsFromCatalog(Workbook workbook, List<BmsRedlineProduct> inlineProducts)
|
||||||
|
{
|
||||||
|
Map<String, BmsRedlineProduct> catalogByCode = new HashMap<String, BmsRedlineProduct>();
|
||||||
|
Sheet catalog = workbook == null ? null : workbook.getSheet(CATALOG_SHEET);
|
||||||
|
if (catalog != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (BmsRedlineProduct p : dedupeCatalogProducts(parseCatalog(catalog)))
|
||||||
|
{
|
||||||
|
if (p == null || StringUtils.isEmpty(p.getProductCode()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catalogByCode.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ignored)
|
||||||
|
{
|
||||||
|
// Catalog optional for inline import.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<BmsRedlineProduct> out = new ArrayList<BmsRedlineProduct>();
|
||||||
|
if (inlineProducts == null)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (BmsRedlineProduct inline : inlineProducts)
|
||||||
|
{
|
||||||
|
if (inline == null || StringUtils.isEmpty(inline.getProductCode()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String code = inline.getProductCode().trim().toUpperCase(Locale.ROOT);
|
||||||
|
BmsRedlineProduct cat = catalogByCode.get(code);
|
||||||
|
if (cat == null)
|
||||||
|
{
|
||||||
|
out.add(inline);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BmsRedlineProduct merged = new BmsRedlineProduct();
|
||||||
|
merged.setProductCode(inline.getProductCode());
|
||||||
|
merged.setProductName(StringUtils.isNotEmpty(inline.getProductName())
|
||||||
|
? inline.getProductName() : cat.getProductName());
|
||||||
|
merged.setServiceLevel(cat.getServiceLevel());
|
||||||
|
merged.setCargoAttr(cat.getCargoAttr());
|
||||||
|
merged.setDestRegion(cat.getDestRegion());
|
||||||
|
merged.setUpdateNote(cat.getUpdateNote());
|
||||||
|
merged.setEffectiveDate(cat.getEffectiveDate());
|
||||||
|
merged.setTaxMode(StringUtils.isNotEmpty(cat.getTaxMode())
|
||||||
|
? cat.getTaxMode() : resolveTaxMode(merged.getProductName()));
|
||||||
|
out.add(merged);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when any multi-country price sheet has an inline product-code or product-name column
|
||||||
|
* (产品代码 / 产品对接代码 / 产品名称).
|
||||||
|
*/
|
||||||
|
private boolean workbookHasInlineProductColumns(Workbook workbook)
|
||||||
|
{
|
||||||
|
if (workbook == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||||
|
{
|
||||||
|
String sheetName = workbook.getSheetName(i);
|
||||||
|
if (shouldSkipCountryPriceSheet(sheetName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Sheet sheet = workbook.getSheetAt(i);
|
||||||
|
if (sheet == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int[] countryHeader = findHeaderCell(sheet, "国家");
|
||||||
|
if (countryHeader == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Row head = sheet.getRow(countryHeader[0]);
|
||||||
|
if (head == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (firstCol(head, "产品代码", "产品对接代码") >= 0
|
||||||
|
|| findColByHeader(head, "产品名称", 0) >= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-country parse keyed by inline product-code column on price sheets.
|
||||||
|
* Does not use 产品目录 sheet matching.
|
||||||
|
*/
|
||||||
|
private RedlineQuoteWorkbook readQuoteWorkbookByInlineProductCode(Workbook workbook) throws Exception
|
||||||
|
{
|
||||||
|
Map<String, String> catalogCodeByName = buildCatalogCodeByName(workbook);
|
||||||
|
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
|
||||||
|
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||||
|
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||||
|
Set<String> unmatchedCountries = new HashSet<String>();
|
||||||
|
Map<String, List<BmsRedlineProductCountry>> out = new LinkedHashMap<String, List<BmsRedlineProductCountry>>();
|
||||||
|
Map<String, BmsRedlineProduct> productMeta = new LinkedHashMap<String, BmsRedlineProduct>();
|
||||||
|
List<String> tips = new ArrayList<String>();
|
||||||
|
|
||||||
|
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||||
|
{
|
||||||
|
String sheetName = workbook.getSheetName(i);
|
||||||
|
if (shouldSkipCountryPriceSheet(sheetName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Sheet sheet = workbook.getSheetAt(i);
|
||||||
|
if (sheet == null || findHeaderCell(sheet, "国家") == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, List<BmsRedlineProductCountry>> sheetByCode = parseProductSheetByInlineCode(
|
||||||
|
sheet, catalogCodeByName, regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
|
||||||
|
if (sheetByCode == null || sheetByCode.isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, List<BmsRedlineProductCountry>> e : sheetByCode.entrySet())
|
||||||
|
{
|
||||||
|
if (e == null || StringUtils.isEmpty(e.getKey()) || e.getValue() == null || e.getValue().isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String code = e.getKey();
|
||||||
|
List<BmsRedlineProductCountry> rows = e.getValue();
|
||||||
|
List<BmsRedlineProductCountry> existing = out.get(code);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
out.put(code, new ArrayList<BmsRedlineProductCountry>(rows));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.addAll(rows);
|
||||||
|
}
|
||||||
|
if (!productMeta.containsKey(code))
|
||||||
|
{
|
||||||
|
BmsRedlineProduct p = new BmsRedlineProduct();
|
||||||
|
p.setProductCode(code);
|
||||||
|
String name = null;
|
||||||
|
for (BmsRedlineProductCountry r : rows)
|
||||||
|
{
|
||||||
|
if (r != null && StringUtils.isNotEmpty(r.getProductName()))
|
||||||
|
{
|
||||||
|
name = r.getProductName();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.setProductName(StringUtils.isNotEmpty(name) ? name : code);
|
||||||
|
p.setTaxMode(resolveTaxMode(p.getProductName()));
|
||||||
|
productMeta.put(code, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<BmsRedlineProductCountry>> e : out.entrySet())
|
||||||
|
{
|
||||||
|
BmsRedlineProduct p = productMeta.get(e.getKey());
|
||||||
|
String tip = buildDuplicateCountryWeightTip(p, e.getValue());
|
||||||
|
if (StringUtils.isNotEmpty(tip))
|
||||||
|
{
|
||||||
|
tips.add(tip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tips.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.dupCountry",
|
||||||
|
String.join("<br/>", tips)));
|
||||||
|
}
|
||||||
|
if (out.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
|
||||||
|
}
|
||||||
|
RedlineQuoteWorkbook parsed = new RedlineQuoteWorkbook();
|
||||||
|
parsed.products = new ArrayList<BmsRedlineProduct>(productMeta.values());
|
||||||
|
parsed.byCode = out;
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one multi-country price sheet grouped by inline product code / product name.
|
||||||
|
* Prefer product-code column; if only product-name exists, resolve code via catalog name map.
|
||||||
|
* Country / product-code / product-name / size / last-mile / lead-time carry forward
|
||||||
|
* when subsequent weight-band rows leave those cells blank.
|
||||||
|
*/
|
||||||
|
private Map<String, List<BmsRedlineProductCountry>> parseProductSheetByInlineCode(Sheet sheet,
|
||||||
|
Map<String, String> catalogCodeByName, Map<String, BmsRegion> regionByNameCn,
|
||||||
|
List<BmsRegion> euRegions, Map<String, PrcZone> zoneByCountryAndCode,
|
||||||
|
Set<String> unmatchedCountries)
|
||||||
|
{
|
||||||
|
Map<String, List<BmsRedlineProductCountry>> out = new LinkedHashMap<String, List<BmsRedlineProductCountry>>();
|
||||||
|
if (sheet == null)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
Date sheetEffective = parseSheetEffectiveDate(sheet);
|
||||||
|
int[] countryHeader = findHeaderCell(sheet, "国家");
|
||||||
|
if (countryHeader == null)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
int headerRowIdx = countryHeader[0];
|
||||||
|
Row headerRow = sheet.getRow(headerRowIdx);
|
||||||
|
int colCountry = countryHeader[1];
|
||||||
|
int colProductCode = firstCol(headerRow, "产品代码", "产品对接代码");
|
||||||
|
int colProductName = findColByHeader(headerRow, "产品名称", 0);
|
||||||
|
if (colProductCode < 0 && colProductName < 0)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
int colWeight = findColByHeader(headerRow, "重量", colCountry + 1);
|
||||||
|
int colFreight = findColByHeader(headerRow, "运费", colCountry + 1);
|
||||||
|
int colRegister = findColByHeader(headerRow, "挂号", colCountry + 1);
|
||||||
|
int colSize = findColByHeader(headerRow, "尺寸", colCountry + 1);
|
||||||
|
int colLastMile = findColByHeader(headerRow, "尾程", colCountry + 1);
|
||||||
|
int colLeadTime = findColByHeader(headerRow, "时效", colCountry + 1);
|
||||||
|
if (colWeight < 0 || colFreight < 0)
|
||||||
|
{
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentCountryRaw = null;
|
||||||
|
String currentProductCode = null;
|
||||||
|
String currentProductName = null;
|
||||||
|
String currentSize = null;
|
||||||
|
String currentLastMile = null;
|
||||||
|
String currentLeadTime = null;
|
||||||
|
int lastRow = sheet.getLastRowNum();
|
||||||
|
for (int i = headerRowIdx + 1; i <= lastRow; i++)
|
||||||
|
{
|
||||||
|
Row row = sheet.getRow(i);
|
||||||
|
if (row == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String countryCell = cellString(row.getCell(colCountry));
|
||||||
|
String weightLabel = cellString(row.getCell(colWeight));
|
||||||
|
if ("国家".equals(countryCell)
|
||||||
|
|| (weightLabel != null && weightLabel.contains("重量段")))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(countryCell))
|
||||||
|
{
|
||||||
|
currentCountryRaw = countryCell;
|
||||||
|
}
|
||||||
|
boolean productChanged = false;
|
||||||
|
if (colProductCode >= 0)
|
||||||
|
{
|
||||||
|
String codeCell = normalizeImportProductCode(cellString(row.getCell(colProductCode)));
|
||||||
|
if (StringUtils.isNotEmpty(codeCell))
|
||||||
|
{
|
||||||
|
String newCode = codeCell.trim().toUpperCase(Locale.ROOT);
|
||||||
|
if (currentProductCode == null || !newCode.equals(currentProductCode))
|
||||||
|
{
|
||||||
|
productChanged = true;
|
||||||
|
currentProductCode = newCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (colProductName >= 0)
|
||||||
|
{
|
||||||
|
String nameCell = cellString(row.getCell(colProductName));
|
||||||
|
if (StringUtils.isNotEmpty(nameCell) && !isSectionTitle(nameCell))
|
||||||
|
{
|
||||||
|
String newName = nameCell.replace("\n", " ").replace("\r", " ").trim();
|
||||||
|
if (currentProductName == null || !newName.equals(currentProductName))
|
||||||
|
{
|
||||||
|
productChanged = true;
|
||||||
|
currentProductName = newName;
|
||||||
|
// Name-only sheets: resolve product code from catalog by product name.
|
||||||
|
if (colProductCode < 0)
|
||||||
|
{
|
||||||
|
String mapped = resolveCodeByProductName(currentProductName, catalogCodeByName);
|
||||||
|
if (StringUtils.isNotEmpty(mapped))
|
||||||
|
{
|
||||||
|
currentProductCode = mapped;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentProductCode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (productChanged)
|
||||||
|
{
|
||||||
|
currentSize = null;
|
||||||
|
currentLastMile = null;
|
||||||
|
currentLeadTime = null;
|
||||||
|
}
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(currentCountryRaw) || StringUtils.isEmpty(currentProductCode))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal[] range = parseWeightRange(weightLabel);
|
||||||
|
if (range == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal freight = cellDecimal(row.getCell(colFreight));
|
||||||
|
BigDecimal registration = colRegister >= 0 ? cellDecimal(row.getCell(colRegister)) : null;
|
||||||
|
Date eff = sheetEffective;
|
||||||
|
|
||||||
|
ZoneCodeAbbrevs.CountryZoneParts parts = ZoneCodeAbbrevs.splitCountryZone(currentCountryRaw);
|
||||||
|
if (parts == null || StringUtils.isEmpty(parts.countryName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String baseCountry = parts.countryName;
|
||||||
|
String zoneLabel = parts.zoneLabel;
|
||||||
|
List<BmsRedlineProductCountry> bucket = out.get(currentProductCode);
|
||||||
|
if (bucket == null)
|
||||||
|
{
|
||||||
|
bucket = new ArrayList<BmsRedlineProductCountry>();
|
||||||
|
out.put(currentProductCode, bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
BmsRegion region = resolveRegion(baseCountry, regionByNameCn);
|
||||||
|
if (region != null)
|
||||||
|
{
|
||||||
|
BmsRedlineProductCountry c = buildCountryRow(region.getCountryNameCn(), region.getCountryCode2(),
|
||||||
|
zoneLabel, zoneByCountryAndCode,
|
||||||
|
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
|
||||||
|
c.setProductCode(currentProductCode);
|
||||||
|
c.setProductName(currentProductName);
|
||||||
|
bucket.add(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isEuZoneName(baseCountry) && StringUtils.isEmpty(zoneLabel))
|
||||||
|
{
|
||||||
|
if (euRegions == null || euRegions.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException(MessageUtils.message("price.product.import.euEmpty"));
|
||||||
|
}
|
||||||
|
for (BmsRegion eu : euRegions)
|
||||||
|
{
|
||||||
|
BmsRedlineProductCountry c = buildCountryRow(eu.getCountryNameCn(), eu.getCountryCode2(),
|
||||||
|
null, zoneByCountryAndCode,
|
||||||
|
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
|
||||||
|
c.setProductCode(currentProductCode);
|
||||||
|
c.setProductName(currentProductName);
|
||||||
|
bucket.add(c);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
unmatchedCountries.add(currentCountryRaw.trim().replace("\n", " ").replace("\r", " "));
|
||||||
|
BmsRedlineProductCountry c = buildCountryRow(baseCountry, null, zoneLabel, zoneByCountryAndCode,
|
||||||
|
range, freight, registration, currentSize, currentLastMile, currentLeadTime, eff);
|
||||||
|
c.setProductCode(currentProductCode);
|
||||||
|
c.setProductName(currentProductName);
|
||||||
|
bucket.add(c);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build productName -> productCode from 产品目录 (first row per name wins). */
|
||||||
|
private Map<String, String> buildCatalogCodeByName(Workbook workbook)
|
||||||
|
{
|
||||||
|
Map<String, String> map = new LinkedHashMap<String, String>();
|
||||||
|
if (workbook == null)
|
||||||
|
{
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||||
|
if (catalog == null)
|
||||||
|
{
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<BmsRedlineProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
|
||||||
|
for (BmsRedlineProduct p : products)
|
||||||
|
{
|
||||||
|
if (p == null || StringUtils.isEmpty(p.getProductCode()) || StringUtils.isEmpty(p.getProductName()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String nameKey = compactName(p.getProductName());
|
||||||
|
if (StringUtils.isEmpty(nameKey) || map.containsKey(nameKey))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
map.put(nameKey, p.getProductCode().trim().toUpperCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ignored)
|
||||||
|
{
|
||||||
|
// Catalog optional for inline identity; ignore parse errors.
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve product code by product name; exact compact match, then base name before '(' / '('. */
|
||||||
|
private String resolveCodeByProductName(String productName, Map<String, String> catalogCodeByName)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(productName) || catalogCodeByName == null || catalogCodeByName.isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String key = compactName(productName);
|
||||||
|
String hit = catalogCodeByName.get(key);
|
||||||
|
if (StringUtils.isNotEmpty(hit))
|
||||||
|
{
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
String base = productName.trim();
|
||||||
|
int idx = base.indexOf('(');
|
||||||
|
int idx2 = base.indexOf('(');
|
||||||
|
int cut = -1;
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
cut = idx;
|
||||||
|
}
|
||||||
|
if (idx2 >= 0 && (cut < 0 || idx2 < cut))
|
||||||
|
{
|
||||||
|
cut = idx2;
|
||||||
|
}
|
||||||
|
if (cut > 0)
|
||||||
|
{
|
||||||
|
hit = catalogCodeByName.get(compactName(base.substring(0, cut)));
|
||||||
|
if (StringUtils.isNotEmpty(hit))
|
||||||
|
{
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static boolean shouldSkipCountryPriceSheet(String name)
|
private static boolean shouldSkipCountryPriceSheet(String name)
|
||||||
{
|
{
|
||||||
if (StringUtils.isEmpty(name))
|
if (StringUtils.isEmpty(name))
|
||||||
|
|||||||
@@ -118,7 +118,20 @@ const colSurcharge = '附加费'
|
|||||||
const colAction = '操作'
|
const colAction = '操作'
|
||||||
|
|
||||||
function cellKey(row, field) {
|
function cellKey(row, field) {
|
||||||
return String(row && row.psdId != null ? row.psdId : row && row.productCountryId) + ':' + field
|
if (!row) return 'none:' + field
|
||||||
|
// Prefer DB id; local Excel draft uses _localKey (psdId cleared in applyLocalDraft).
|
||||||
|
if (row.psdId != null && row.psdId !== '') return String(row.psdId) + ':' + field
|
||||||
|
if (row._localKey) return String(row._localKey) + ':' + field
|
||||||
|
const parts = [
|
||||||
|
row.productCode,
|
||||||
|
row.countryCode || row.countryName,
|
||||||
|
row.zoneCode || row.zoneName,
|
||||||
|
row.weightLabel,
|
||||||
|
row.weightMin,
|
||||||
|
row.weightMax,
|
||||||
|
row.productCountryId
|
||||||
|
]
|
||||||
|
return parts.map(v => (v == null ? '' : String(v))).join('|') + ':' + field
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNum(v, precision) {
|
function formatNum(v, precision) {
|
||||||
@@ -150,7 +163,8 @@ async function commitEdit(row, field) {
|
|||||||
editingKey.value = ''
|
editingKey.value = ''
|
||||||
try {
|
try {
|
||||||
if (!row) return
|
if (!row) return
|
||||||
if (!row.psdId) return
|
// Always emit: create/IMPORT_FRESH local draft has no psdId until finish;
|
||||||
|
// parent persist path decides DB write vs local-only.
|
||||||
emit('row-change', row)
|
emit('row-change', row)
|
||||||
} finally {
|
} finally {
|
||||||
committing.value = false
|
committing.value = false
|
||||||
|
|||||||
@@ -890,7 +890,10 @@ async function onRowPriceChange(row) {
|
|||||||
proxy.$modal.msgSuccess('价格已更新(完成时统一落库)')
|
proxy.$modal.msgSuccess('价格已更新(完成时统一落库)')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!row.psdId) return
|
if (!row.psdId) {
|
||||||
|
proxy.$modal.msgWarning('明细尚未落库,请先点「完成」保存后再改价')
|
||||||
|
return
|
||||||
|
}
|
||||||
const prevFv = row._editPrevFv
|
const prevFv = row._editPrevFv
|
||||||
const prevReg = row._editPrevReg
|
const prevReg = row._editPrevReg
|
||||||
try {
|
try {
|
||||||
@@ -899,6 +902,7 @@ async function onRowPriceChange(row) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (prevFv !== undefined) row.fvValue = prevFv
|
if (prevFv !== undefined) row.fvValue = prevFv
|
||||||
if (prevReg !== undefined) row.fvRegistrationFee = prevReg
|
if (prevReg !== undefined) row.fvRegistrationFee = prevReg
|
||||||
|
proxy.$modal.msgError((e && e.message) ? e.message : '价格更新失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user