Enhance product import validation by adding checks for duplicate country and weight band entries. Update related error messages in properties file. Introduce new method to delete auto fees while retaining manual entries during rebill operations. Update front-end components to manage build mode changes and ensure proper handling of product lists during price set creation.
This commit is contained in:
@@ -47,4 +47,7 @@ public interface PrcIncomeBillFeeMapper
|
||||
public int deleteByFeeId(String feeId);
|
||||
|
||||
public int deleteByBillId(String billId);
|
||||
|
||||
/** Rebill: delete auto fees only; keep manual_flag=1 rows. */
|
||||
public int deleteAutoFeesByBillId(String billId);
|
||||
}
|
||||
|
||||
+134
@@ -309,6 +309,9 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||
// Pre-check: same product cannot have duplicate country + weight band in Excel
|
||||
assertImportProductsNoDupCountryWeight(workbook, products, regionByNameCn, euRegions,
|
||||
zoneByCountryAndCode);
|
||||
|
||||
int successNum = 0;
|
||||
int skipNum = 0;
|
||||
@@ -460,6 +463,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
product.setTaxMode(resolveTaxMode(product.getProductName()));
|
||||
}
|
||||
List<BmsProductCountry> countries = stampSelectedCountry(block.rows, region, forcedEff);
|
||||
assertNoDuplicateCountryWeightRows(product, countries);
|
||||
int[] nums = persistImportedProduct(product, countries, operName);
|
||||
transactionManager.commit(tx);
|
||||
successNum += nums[0];
|
||||
@@ -597,6 +601,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
assertNoDuplicateCountryWeightRows(product, rows);
|
||||
String code = product.getProductCode().trim().toUpperCase(Locale.ROOT);
|
||||
out.put(code, rows);
|
||||
}
|
||||
@@ -632,6 +637,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
|
||||
productSheet.getSheetName()));
|
||||
}
|
||||
assertNoDuplicateCountryWeightRows(product, countries);
|
||||
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
|
||||
Date newEff = requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
|
||||
: product.getEffectiveDate());
|
||||
@@ -1988,6 +1994,134 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workbook pre-check for full import: reject before any write when a product sheet
|
||||
* has duplicate country + weight band (same zone) rows.
|
||||
*/
|
||||
private void assertImportProductsNoDupCountryWeight(Workbook workbook, List<BmsProduct> products,
|
||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||
Map<String, PrcZone> zoneByCountryAndCode)
|
||||
{
|
||||
if (workbook == null || products == null || products.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<String> tips = new ArrayList<String>();
|
||||
Set<String> unmatchedCountries = new HashSet<String>();
|
||||
for (BmsProduct product : products)
|
||||
{
|
||||
if (product == null || StringUtils.isEmpty(product.getProductCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Sheet productSheet = findProductSheet(workbook, product);
|
||||
if (productSheet == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<BmsProductCountry> rows = parseProductSheet(productSheet, product,
|
||||
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
|
||||
String tip = buildDuplicateCountryWeightTip(product, rows);
|
||||
if (StringUtils.isNotEmpty(tip))
|
||||
{
|
||||
tips.add(tip);
|
||||
}
|
||||
}
|
||||
if (!tips.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.product.import.dupCountry",
|
||||
String.join("<br/>", tips)));
|
||||
}
|
||||
}
|
||||
|
||||
/** Fail when Excel rows for one product contain duplicate country + weight band. */
|
||||
private void assertNoDuplicateCountryWeightRows(BmsProduct product, List<BmsProductCountry> rows)
|
||||
{
|
||||
String tip = buildDuplicateCountryWeightTip(product, rows);
|
||||
if (StringUtils.isNotEmpty(tip))
|
||||
{
|
||||
throw new ServiceException(tip);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildDuplicateCountryWeightTip(BmsProduct product, List<BmsProductCountry> rows)
|
||||
{
|
||||
List<String> dups = findDuplicateCountryWeightLabels(rows);
|
||||
if (dups.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String code = product == null || StringUtils.isEmpty(product.getProductCode())
|
||||
? "-" : product.getProductCode();
|
||||
String name = product == null || StringUtils.isEmpty(product.getProductName())
|
||||
? "-" : product.getProductName();
|
||||
return MessageUtils.message("price.product.import.dupCountry.item",
|
||||
code, name, String.join("\u3001", dups));
|
||||
}
|
||||
|
||||
private List<String> findDuplicateCountryWeightLabels(List<BmsProductCountry> rows)
|
||||
{
|
||||
List<String> dups = new ArrayList<String>();
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
return dups;
|
||||
}
|
||||
Map<String, Integer> counts = new LinkedHashMap<String, Integer>();
|
||||
Map<String, String> labels = new LinkedHashMap<String, String>();
|
||||
for (BmsProductCountry c : rows)
|
||||
{
|
||||
if (c == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String key = countryWeightDupKey(c);
|
||||
Integer n = counts.get(key);
|
||||
counts.put(key, n == null ? 1 : n.intValue() + 1);
|
||||
if (!labels.containsKey(key))
|
||||
{
|
||||
labels.put(key, formatCountryWeightTip(c));
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, Integer> e : counts.entrySet())
|
||||
{
|
||||
if (e.getValue() != null && e.getValue().intValue() > 1)
|
||||
{
|
||||
dups.add(labels.get(e.getKey()));
|
||||
}
|
||||
}
|
||||
return dups;
|
||||
}
|
||||
|
||||
/** Align with uk_product_country_zone_weight_eff (country + zone + weight min/max). */
|
||||
private String countryWeightDupKey(BmsProductCountry c)
|
||||
{
|
||||
String country = StringUtils.isNotEmpty(c.getCountryCode())
|
||||
? c.getCountryCode().trim().toUpperCase(Locale.ROOT)
|
||||
: (c.getCountryName() == null ? "" : c.getCountryName().trim());
|
||||
String zone = c.getZoneCode() == null ? "" : c.getZoneCode().trim().toUpperCase(Locale.ROOT);
|
||||
return country + "|" + zone + "|" + decimalKey(c.getWeightMin()) + "|" + decimalKey(c.getWeightMax());
|
||||
}
|
||||
|
||||
private String formatCountryWeightTip(BmsProductCountry c)
|
||||
{
|
||||
String name = StringUtils.isNotEmpty(c.getCountryName()) ? c.getCountryName()
|
||||
: (StringUtils.isNotEmpty(c.getCountryCode()) ? c.getCountryCode() : "-");
|
||||
if (StringUtils.isNotEmpty(c.getZoneCode()))
|
||||
{
|
||||
name = name + "(" + c.getZoneCode() + ")";
|
||||
}
|
||||
return name + " " + decimalKey(c.getWeightMin()) + "~" + decimalKey(c.getWeightMax());
|
||||
}
|
||||
|
||||
private String decimalKey(BigDecimal v)
|
||||
{
|
||||
if (v == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return v.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
/** Stamp product + all country detail rows with the same effective date. */
|
||||
private void applyForcedEffectiveDate(BmsProduct product, List<BmsProductCountry> countries, Date newEff)
|
||||
{
|
||||
|
||||
+72
-2
@@ -8,8 +8,10 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -1115,18 +1117,39 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
else
|
||||
{
|
||||
orderBillMapper.updatePrcOrderBill(bill);
|
||||
incomeBillFeeMapper.deleteByBillId(bill.getBillId());
|
||||
// Keep manually added fees (manual_flag=1) across rebill
|
||||
incomeBillFeeMapper.deleteAutoFeesByBillId(bill.getBillId());
|
||||
// Payable not in scope yet: skip prc_pay_bill_fee cleanup/write.
|
||||
// payBillFeeMapper.deleteByBillId(bill.getBillId());
|
||||
}
|
||||
PrcIncomeBill incomeBill = persistIncomeBill(bill, result);
|
||||
List<PrcIncomeBillFee> fees = toFeeRows(bill.getBillId(),
|
||||
incomeBill == null ? null : incomeBill.getIncomeBillId(), result);
|
||||
if (existed != null)
|
||||
{
|
||||
fees = filterAutoFeesAgainstManual(bill.getBillId(), fees);
|
||||
}
|
||||
if (!fees.isEmpty())
|
||||
{
|
||||
incomeBillFeeMapper.insertBatch(fees);
|
||||
}
|
||||
orderBillMapper.refreshArBillStatusByBillIds(Arrays.asList(bill.getBillId()));
|
||||
if (existed != null)
|
||||
{
|
||||
// Quote totals were written above; recalc so retained manual fees join income/credit
|
||||
TmsPriceQuoteResult refreshed = refreshBillIncomeTotals(bill);
|
||||
if (refreshed != null)
|
||||
{
|
||||
result.setSurchargeAmount(refreshed.getSurchargeAmount());
|
||||
result.setIncomeTotal(refreshed.getIncomeTotal());
|
||||
result.setPayableTotal(refreshed.getPayableTotal());
|
||||
result.setCreditArrearsStatus(refreshed.getCreditArrearsStatus());
|
||||
result.setFeeDetails(refreshed.getFeeDetails());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
orderBillMapper.refreshArBillStatusByBillIds(Arrays.asList(bill.getBillId()));
|
||||
}
|
||||
// Payable not in scope yet: do not write prc_pay_bill / prc_pay_bill_fee.
|
||||
// PrcPayBill payBill = persistPayBill(bill, result);
|
||||
// List<PrcPayBillFee> payFees = toPayFeeRows(bill.getBillId(),
|
||||
@@ -1141,6 +1164,53 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
billSearchStore.save(EsLogType.BILL, EsBizLogSupport.billBizId(request), requestJson, toResponseJson(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop auto fee lines that collide with retained manual rows on uk_bill_fee_item
|
||||
* (bill_id, fee_type, sc_id). Manual fees win on rebill.
|
||||
*/
|
||||
private List<PrcIncomeBillFee> filterAutoFeesAgainstManual(String billId, List<PrcIncomeBillFee> autoFees)
|
||||
{
|
||||
if (autoFees == null || autoFees.isEmpty() || StringUtils.isEmpty(billId))
|
||||
{
|
||||
return autoFees == null ? new ArrayList<PrcIncomeBillFee>() : autoFees;
|
||||
}
|
||||
List<PrcIncomeBillFee> existing = incomeBillFeeMapper.selectByBillId(billId);
|
||||
if (existing == null || existing.isEmpty())
|
||||
{
|
||||
return autoFees;
|
||||
}
|
||||
Set<String> manualKeys = new HashSet<String>();
|
||||
for (PrcIncomeBillFee row : existing)
|
||||
{
|
||||
if (row != null && "1".equals(row.getManualFlag()))
|
||||
{
|
||||
manualKeys.add(feeUniqueKey(row.getFeeType(), row.getScId()));
|
||||
}
|
||||
}
|
||||
if (manualKeys.isEmpty())
|
||||
{
|
||||
return autoFees;
|
||||
}
|
||||
List<PrcIncomeBillFee> out = new ArrayList<PrcIncomeBillFee>();
|
||||
for (PrcIncomeBillFee fee : autoFees)
|
||||
{
|
||||
if (fee == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!manualKeys.contains(feeUniqueKey(fee.getFeeType(), fee.getScId())))
|
||||
{
|
||||
out.add(fee);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private String feeUniqueKey(String feeType, Long scId)
|
||||
{
|
||||
return StringUtils.nvl(feeType, "") + "|" + (scId == null ? "0" : String.valueOf(scId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Occupy/release credit by income total delta (bizType=ORDER).
|
||||
* Missing/disabled customer: skip occupy, return NORMAL.
|
||||
|
||||
+32
@@ -2733,6 +2733,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
version.setExpireTime(db.getExpireTime());
|
||||
}
|
||||
// Draft + build mode changed: wipe old product/detail/surcharge before mode switch
|
||||
clearDraftVersionChildrenIfBuildModeChanged(db, version);
|
||||
versionMapper.updatePrcPriceSetVersion(version);
|
||||
}
|
||||
set.setVersion(version);
|
||||
@@ -2749,6 +2751,36 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
refreshVersionStatus(set.getPriceSetId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Draft only: when priceBuildMode changes (PULL/IMPORT), delete this version's
|
||||
* product / detail / surcharge rows. Same mode or non-draft: no-op.
|
||||
*/
|
||||
private void clearDraftVersionChildrenIfBuildModeChanged(PrcPriceSetVersion db, PrcPriceSetVersion incoming)
|
||||
{
|
||||
if (db == null || incoming == null || db.getPsvId() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!VER_DRAFT.equals(db.getStatus()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String newMode = StringUtils.trim(incoming.getPriceBuildMode());
|
||||
if (StringUtils.isEmpty(newMode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String oldMode = StringUtils.trim(db.getPriceBuildMode());
|
||||
if (newMode.equalsIgnoreCase(StringUtils.nvl(oldMode, "")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Long psvId = db.getPsvId();
|
||||
versionSurchargeMapper.deleteByPsvId(psvId);
|
||||
detailMapper.deletePrcPriceSetDetailByPsvId(psvId);
|
||||
setProductMapper.deleteByPsvId(psvId);
|
||||
}
|
||||
|
||||
/** Prefer same effective_time, else draft, else first version of the set. */
|
||||
private PrcPriceSetVersion findReusableVersion(Long priceSetId, Date effectiveTime)
|
||||
{
|
||||
|
||||
@@ -180,4 +180,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<delete id="deleteByBillId" parameterType="String">
|
||||
delete from prc_income_bill_fee where bill_id = #{billId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteAutoFeesByBillId" parameterType="String">
|
||||
delete from prc_income_bill_fee
|
||||
where bill_id = #{billId}
|
||||
and (manual_flag is null or manual_flag <> '1')
|
||||
</delete>
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user