feat(backend): 入库「确认进价」前向补偿(暂估价→真实价)
调货 0 价入库并已审核/出库后,价格确定时补填真实进价:一事务内 回填入库明细/库存批次/已出库成本快照,重算入库单总额,按差额补 应付流水,同步参考进价。按 product_id 精确命中(序列号 1:1), 不动售价/应收。仅 approved 单、管理员/超管,差额幂等可重复确认。 新增 POST /stock-in/orders/:id/confirm-cost 及 4 个测试用例。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,111 @@ func (s *StockService) ApproveStockIn(shopID, orderID, reviewerID uint64) error
|
||||
})
|
||||
}
|
||||
|
||||
// CostConfirmItem 是「确认进价」的单行入参:把某入库明细的进价补成真实值。
|
||||
type CostConfirmItem struct {
|
||||
ItemID uint64 `json:"item_id"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
}
|
||||
|
||||
// ConfirmStockInCost 「确认进价」:调货等场景以 0 价(暂估)入库并已审核/已出库后,
|
||||
// 价格确定时补填真实进价。不反审核、不撤销任何已发生的动作,而是在一个事务内
|
||||
// 前向写补偿:
|
||||
// 1. 入库明细 UnitPrice / TotalPrice 改为真实值
|
||||
// 2. 该明细对应的剩余库存批次 Inventory.UnitPrice 由 NULL→真实值
|
||||
// 3. 该 product 已出库行 StockOutItem.UnitPrice/TotalPrice(成本快照)回填真实值
|
||||
// (product↔入库明细 1:1,按 product_id 精确命中;不动 SalePrice/应收)
|
||||
// 4. 入库单 TotalAmount 按差额重算
|
||||
// 5. 对供应商应付按差额补一条调整流水(滚动余额)
|
||||
//
|
||||
// 仅 approved 单可确认(draft 直接编辑即可);权限:管理员/超管。
|
||||
// 以「旧价→新价」差额计算补偿,支持多次修正(再次确认按新旧差额补)。
|
||||
func (s *StockService) ConfirmStockInCost(shopID, orderID, userID uint64, role string, items []CostConfirmItem) error {
|
||||
if role != "admin" && role != "superadmin" {
|
||||
return ErrForbidden
|
||||
}
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.StockInOrder
|
||||
if err := tx.Preload("Items").Where("id = ? AND shop_id = ?", orderID, shopID).
|
||||
First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Status != "approved" {
|
||||
return errors.New("只有已审核单据可确认进价")
|
||||
}
|
||||
|
||||
want := make(map[uint64]float64, len(items))
|
||||
for _, it := range items {
|
||||
want[it.ItemID] = it.UnitPrice
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var totalDiff float64
|
||||
|
||||
for i := range order.Items {
|
||||
it := &order.Items[i]
|
||||
newPrice, ok := want[it.ID]
|
||||
if !ok || newPrice == it.UnitPrice {
|
||||
continue
|
||||
}
|
||||
oldPrice := it.UnitPrice
|
||||
diff := (newPrice - oldPrice) * it.Quantity
|
||||
totalDiff += diff
|
||||
|
||||
// 1. 入库明细
|
||||
if err := tx.Model(it).Updates(map[string]interface{}{
|
||||
"unit_price": newPrice,
|
||||
"total_price": newPrice * it.Quantity,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 该明细的剩余库存批次(已全部出库则无行,跳过)
|
||||
if err := tx.Model(&model.Inventory{}).
|
||||
Where("shop_id = ? AND stock_in_item_id = ? AND deleted_at IS NULL", shopID, it.ID).
|
||||
Update("unit_price", newPrice).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 已出库行成本快照回填(按 product_id 精确命中;product_id=0 的历史导入单不传播)
|
||||
if it.ProductID != 0 {
|
||||
if err := tx.Model(&model.StockOutItem{}).
|
||||
Where("shop_id = ? AND product_id = ?", shopID, it.ProductID).
|
||||
Updates(map[string]interface{}{
|
||||
"unit_price": newPrice,
|
||||
"total_price": gorm.Expr("? * quantity", newPrice),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 同步 product 参考进价
|
||||
if it.ProductID != 0 {
|
||||
tx.Model(&model.Product{}).Where("id = ? AND shop_id = ?", it.ProductID, shopID).
|
||||
Update("purchase_price", newPrice)
|
||||
}
|
||||
}
|
||||
|
||||
if totalDiff == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. 入库单总额按差额重算
|
||||
if err := tx.Model(&order).
|
||||
Update("total_amount", gorm.Expr("total_amount + ?", totalDiff)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 应付差额调整流水(滚动余额)
|
||||
bal := partnerLastBalance(tx, shopID, order.PartnerID) + totalDiff
|
||||
oid := order.ID
|
||||
return tx.Create(&model.FinanceRecord{
|
||||
ShopID: shopID, PartnerID: order.PartnerID, Type: "payable",
|
||||
Amount: totalDiff, Balance: bal, Status: "open",
|
||||
RefType: "stock_in_cost_adjust", RefID: &oid, OperatorID: userID, RecordDate: now,
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// ApproveStockOut 审核出库单,FIFO 扣减批次库存
|
||||
func (s *StockService) ApproveStockOut(shopID, orderID, reviewerID uint64) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
Reference in New Issue
Block a user