@@ -44,7 +44,6 @@ import java.util.HashSet;
import java.util.LinkedHashMap ;
import java.util.List ;
import java.util.Map ;
import java.util.Objects ;
import java.util.Set ;
import java.util.stream.Collectors ;
@@ -231,8 +230,10 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
String outboundType = resolveOutboundType ( dto . getOutboundType ( ) ) ;
List < CostSplit > splits = deductPurchaseStockFifo ( dto . getProductId ( ) , dto . getMerchantId ( ) , dto . getQuantity ( ) ) ;
for ( CostSplit split : splits ) {
BigDecimal saleUnitCost = ( dto . getUnitCost ( ) ! = null & & dto . getUnitCost ( ) . compareTo ( BigDecimal . ZERO ) > 0 )
? dto . getUnitCost ( ) : split . unitCost ;
insertSaleRecord ( dto . getProductId ( ) , dto . getMerchantId ( ) , outboundType ,
- split . quantity , split . u nitCost , dto . getRemark ( ) , null , null , null , split . purchaseRecordId ) ;
- split . quantity , saleU nitCost , dto . getRemark ( ) , null , null , null , split . purchaseRecordId ) ;
}
adjustShopProductStock ( dto . getProductId ( ) , - dto . getQuantity ( ) ) ;
return ;
@@ -244,12 +245,13 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
}
// 采购:跨商户调拨 = 来源方按成本批次多条出库 + 本商户一条采购(成本取接口传值)
// 灵枢→众秒:灵枢出库 outbound_type=zm; 众秒→灵枢: 众秒出库 outbound_type=ls
if ( isCrossMerchantTransfer ( dto . getMerchantId ( ) , dto . getSource ( ) ) ) {
int sourceMerchantId = sourceToMerchantId ( dto . getSource ( ) ) ;
String saleTargetSource = merchantIdToSource ( dto . getMerchantId ( ) ) ;
List < CostSplit > splits = deductPurchaseStockFifo ( dto . getProductId ( ) , sourceMerchantId , dto . getQuantity ( ) ) ;
for ( CostSplit split : splits ) {
insertRecord ( dto . getProductId ( ) , sourceMerchantId , saleTargetSource , null ,
insertRecord ( dto . getProductId ( ) , sourceMerchantId , saleTargetSource , saleTargetSource ,
InvStockRecord . BIZ_TYPE_SALE , - split . quantity , split . unitCost , dto . getRemark ( ) ,
null , null , null , split . purchaseRecordId , null ) ;
}
@@ -1166,7 +1168,6 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
return new ArrayList < > ( ) ;
}
SimpleDateFormat sdf = new SimpleDateFormat ( " yyyy-MM-dd HH:mm:ss " ) ;
Map < Long , Integer > setPurchaseQtyCache = new HashMap < > ( ) ;
Map < Long , List < InvStockRecord > > bindCache = new HashMap < > ( ) ;
List < Map < String , Object > > result = new ArrayList < > ( ) ;
for ( Map < String , Object > item : list ) {
@@ -1175,10 +1176,11 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
continue ;
}
if ( recordBizType = = InvStockRecord . BIZ_TYPE_SALE ) {
// 套书绑定内部出库不进入售出明细导出
if ( isSetBookBindOutbound ( item . get ( " outboundType " ) ) ) {
continue ;
}
result . addAll ( expandSaleRecordForExport ( item , sdf , setPurchaseQtyCache , bindCache ) ) ;
result . addAll ( expandSaleRecordForExport ( item , sdf , bindCache ) ) ;
} else if ( recordBizType = = InvStockRecord . BIZ_TYPE_PURCHASE ) {
result . add ( buildPurchaseExportRow ( item , sdf ) ) ;
}
@@ -1209,50 +1211,93 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
row . put ( " orderSn " , item . get ( " orderSn " ) = = null ? " " : item . get ( " orderSn " ) ) ;
row . put ( " createTime " , formatExportTime ( item . get ( " createTime " ) , sdf ) ) ;
row . put ( " come " , item . get ( " come " ) ) ;
row . put ( " setProductId " , " " ) ;
row . put ( " setProductName " , " " ) ;
row . put ( " stockRecordId " , item . get ( " id " ) ) ;
return row ;
}
private List < Map < String , Object > > expandSaleRecordForExport ( Map < String , Object > item , SimpleDateFormat sdf ,
Map < Long , Integer > setPurchaseQtyCache ,
Map < Long , List < InvStockRecord > > bindCache ) {
Integer productId = toIntegerSafe ( item . get ( " productId " ) ) ;
Long purchaseRecordId = toLongSafe ( item . get ( " purchaseRecordId " ) ) ;
if ( productId ! = null & & purchaseRecordId ! = null & & isSetBookProductByProductId ( productId ) ) {
List < InvStockRecord > binds = bindCache . computeIfAbsent ( purchaseRecordId , this : : listSetBookComponentOutbounds ) ;
if ( ! binds . isEmpty ( ) ) {
int saleQty = Math . abs ( toIntValue ( item . get ( " quantity " ) ) ) ;
int setPurchaseQty = setPurchaseQtyCache . computeIfAbsent ( purchaseRecordId , this : : resolveSetPurchaseQuantity ) ;
return buildSetBookExpandedSaleRows ( item , binds , saleQty , setPurchaseQty , sdf ) ;
}
if ( productId = = null | | ! isSetBookProductByProductId ( productId ) ) {
return Collections . singletonList ( buildSaleExportRow ( item , sdf ) ) ;
}
return Collections . singletonList ( buildSaleExportRow ( item , sdf ) ) ;
List < ShopProduct > components = listSetBookComponentProducts ( productId ) ;
if ( components . isEmpty ( ) ) {
return Collections . singletonList ( buildSaleExportRow ( item , sdf ) ) ;
}
int saleQty = Math . abs ( toIntValue ( item . get ( " quantity " ) ) ) ;
Integer merchantId = toIntegerSafe ( item . get ( " merchantId " ) ) ;
// 套书售出流水的 purchase_record_id = 套书采购流水 id, 对应单品出库的 set_purchase_record_id
Long setPurchaseRecordId = toLongSafe ( item . get ( " purchaseRecordId " ) ) ;
Map < Integer , BigDecimal > costByProductId = Collections . emptyMap ( ) ;
if ( setPurchaseRecordId ! = null ) {
List < InvStockRecord > binds = bindCache . computeIfAbsent ( setPurchaseRecordId , this : : listSetBookComponentOutbounds ) ;
costByProductId = resolveComponentCostFromBinds ( binds ) ;
}
String setProductName = item . get ( " productName " ) = = null ? " " : item . get ( " productName " ) . toString ( ) ;
return buildSetBookExpandedSaleRows ( item , components , saleQty , merchantId , costByProductId ,
productId , setProductName , sdf ) ;
}
private List < Map < String , Object > > buildSetBookExpandedSaleRows ( Map < String , Object > parent ,
List < InvStockRecord > binds ,
int saleQty , int setPurchaseQty ,
SimpleDateFormat sdf ) {
Map < Integer , String > productNames = loadProductNameMap (
binds . stream ( ) . map ( InvStockRecord : : getProductId ) . filter ( Objects : : nonNull ) . collect ( Collectors . toSet ( ) ) ) ;
List < Map < String , Obje ct> > row s = new ArrayList < > ( ) ;
for ( InvStockRecord bind : binds ) {
int bindQty = bind . getQuantity ( ) = = null ? 0 : Math . abs ( bind . getQuantity ( ) ) ;
int exportQty = setPurchaseQty > 0
? ( int ) Math . round ( bindQty * ( double ) saleQty / setPurchaseQty )
: saleQty ;
if ( exportQty < = 0 ) {
/** 套书关联图书 → 只取「只绑了 1 本书」的单品商品 */
private List < ShopProduct > listSetBookComponentProducts ( Integer setProductId ) {
List < Integer > bookIds = listBookIdsByProductId ( setProductId ) ;
if ( bookIds . isEmpty ( ) ) {
return Collections . emptyList ( ) ;
}
List < ShopProdu ct> component s = new ArrayList < > ( ) ;
Set < Integer > seen = new HashSet < > ( ) ;
for ( Integer bookId : bookIds ) {
ShopProduct component = findComponentProductByBookId ( bookId , setProductId ) ;
if ( component = = null | | component . getProductId ( ) = = null ) {
continue ;
}
// findComponentProductByBookId 已优先只绑 1 本书;再兜底确认
if ( listBookIdsByProductId ( component . getProductId ( ) ) . size ( ) ! = 1 ) {
continue ;
}
if ( seen . add ( component . getProductId ( ) ) ) {
components . add ( component ) ;
}
}
return components ;
}
/**
* 套书售出导出:按套内单品展开,数量=套书出库数量;
* 成本优先:单品 books 出库.purchase_record_id → 入库记录.unit_cost;
* 无 set_purchase_record_id / 无绑定时: inv_product_cost.cost_price。
*/
private List < Map < String , Object > > buildSetBookExpandedSaleRows ( Map < String , Object > parent ,
List < ShopProduct > components ,
int saleQty ,
Integer merchantId ,
Map < Integer , BigDecimal > costByProductId ,
Integer setProductId ,
String setProductName ,
SimpleDateFormat sdf ) {
List < Map < String , Object > > rows = new ArrayList < > ( ) ;
for ( ShopProduct component : components ) {
Integer componentId = component . getProductId ( ) ;
BigDecimal unitCost = costByProductId = = null ? null : costByProductId . get ( componentId ) ;
if ( unitCost = = null ) {
unitCost = lookupInvProductCostPrice ( componentId , merchantId ) ;
}
Map < String , Object > row = new LinkedHashMap < > ( ) ;
row . put ( " exportCategory " , " sale " ) ;
row . put ( " productId " , bind . getProductId ( ) ) ;
row . put ( " productName " , p roductNames . getOrDefault ( bind . getProductId ( ) , " " ) ) ;
row . put ( " productId " , componentId ) ;
row . put ( " productName " , component . getP roductName( ) = = null ? " " : component . getProductName ( ) ) ;
row . put ( " outboundTypeLabel " , parent . get ( " outboundTypeLabel " ) ) ;
row . put ( " unitCost " , bind . getU nitCost( ) );
row . put ( " quantity " , export Qty) ;
row . put ( " unitCost " , u nitCost) ;
row . put ( " quantity " , sale Qty) ;
row . put ( " orderSn " , parent . get ( " orderSn " ) = = null ? " " : parent . get ( " orderSn " ) ) ;
row . put ( " createTime " , formatExportTime ( parent . get ( " createTime " ) , sdf ) ) ;
row . put ( " come " , parent . get ( " come " ) ) ;
row . put ( " setProductId " , setProductId ) ;
row . put ( " setProductName " , setProductName = = null ? " " : setProductName ) ;
row . put ( " stockRecordId " , parent . get ( " id " ) ) ;
rows . add ( row ) ;
}
if ( rows . isEmpty ( ) ) {
@@ -1261,15 +1306,76 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
return rows ;
}
private int resolveSetPurchaseQuantity ( Long setPurchaseRecordId ) {
if ( setP urchaseR ecordId = = null ) {
return 0 ;
/**
* 根据套书绑定的单品出库流水: p urchase_r ecord_id → 入库记录取 unit_cost;
* 同一单品多条 FIFO 按数量加权平均。
*/
private Map < Integer , BigDecimal > resolveComponentCostFromBinds ( List < InvStockRecord > binds ) {
if ( binds = = null | | binds . isEmpty ( ) ) {
return Collections . emptyMap ( ) ;
}
InvStockRecord purchase = invStockRecordDao . selectById ( setPurchaseRecordId ) ;
if ( purchase = = null | | purchase . getQuantity ( ) = = null ) {
return 0 ;
Map < Integer , BigDecimal > costSum = new HashMap < > ( ) ;
Map < Integer , Integer > qtySum = new HashMap < > ( ) ;
Map < Long , BigDecimal > purchaseCostCache = new HashMap < > ( ) ;
for ( InvStockRecord bind : binds ) {
Integer componentId = bind . getProductId ( ) ;
if ( componentId = = null ) {
continue ;
}
int qty = bind . getQuantity ( ) = = null ? 0 : Math . abs ( bind . getQuantity ( ) ) ;
if ( qty < = 0 ) {
continue ;
}
BigDecimal unitCost = resolveInboundUnitCost ( bind . getPurchaseRecordId ( ) , purchaseCostCache ) ;
if ( unitCost = = null ) {
unitCost = bind . getUnitCost ( ) ;
}
if ( unitCost = = null ) {
continue ;
}
costSum . merge ( componentId , unitCost . multiply ( BigDecimal . valueOf ( qty ) ) , BigDecimal : : add ) ;
qtySum . merge ( componentId , qty , Integer : : sum ) ;
}
return Math . abs ( purchase . getQuantity ( ) ) ;
Map < Integer , BigDecimal > result = new HashMap < > ( ) ;
for ( Map . Entry < Integer , BigDecimal > entry : costSum . entrySet ( ) ) {
int qty = qtySum . getOrDefault ( entry . getKey ( ) , 0 ) ;
if ( qty < = 0 ) {
continue ;
}
result . put ( entry . getKey ( ) ,
entry . getValue ( ) . divide ( BigDecimal . valueOf ( qty ) , 2 , java . math . RoundingMode . HALF_UP ) ) ;
}
return result ;
}
private BigDecimal resolveInboundUnitCost ( Long purchaseRecordId , Map < Long , BigDecimal > cache ) {
if ( purchaseRecordId = = null ) {
return null ;
}
if ( cache . containsKey ( purchaseRecordId ) ) {
return cache . get ( purchaseRecordId ) ;
}
InvStockRecord purchase = invStockRecordDao . selectById ( purchaseRecordId ) ;
BigDecimal unitCost = null ;
if ( purchase ! = null & & purchase . getBizType ( ) ! = null
& & purchase . getBizType ( ) = = InvStockRecord . BIZ_TYPE_PURCHASE ) {
unitCost = purchase . getUnitCost ( ) ;
}
cache . put ( purchaseRecordId , unitCost ) ;
return unitCost ;
}
private BigDecimal lookupInvProductCostPrice ( Integer productId , Integer merchantId ) {
if ( productId = = null | | merchantId = = null ) {
return null ;
}
InvProductCost cost = invProductCostDao . selectOne (
new LambdaQueryWrapper < InvProductCost > ( )
. eq ( InvProductCost : : getProductId , productId )
. eq ( InvProductCost : : getMerchantId , merchantId )
. last ( " LIMIT 1 " )
) ;
return cost = = null ? null : cost . getCostPrice ( ) ;
}
private List < InvStockRecord > listSetBookComponentOutbounds ( Long setPurchaseRecordId ) {
@@ -1290,26 +1396,6 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
return listBookIdsByProductId ( productId ) . size ( ) > 1 ;
}
private Map < Integer , String > loadProductNameMap ( Set < Integer > productIds ) {
if ( productIds = = null | | productIds . isEmpty ( ) ) {
return Collections . emptyMap ( ) ;
}
List < ShopProduct > products = shopProductDao . selectList (
new LambdaQueryWrapper < ShopProduct > ( )
. select ( ShopProduct : : getProductId , ShopProduct : : getProductName )
. in ( ShopProduct : : getProductId , productIds )
) ;
Map < Integer , String > map = new HashMap < > ( ) ;
if ( products ! = null ) {
for ( ShopProduct product : products ) {
if ( product . getProductId ( ) ! = null ) {
map . put ( product . getProductId ( ) , product . getProductName ( ) ) ;
}
}
}
return map ;
}
private String formatExportTime ( Object createTime , SimpleDateFormat sdf ) {
if ( createTime instanceof Date ) {
return sdf . format ( ( Date ) createTime ) ;