feat(backend): 出入库搜索重构 + 商品编码搜索 + KPI/确认售价/详细搜索

- 搜索 keyword 命中改为 单号/酒名(名+拼音)/商品编码,去掉往来单位匹配
- applyStockOrderDetailFilters 合并明细过滤为单条子查询(同一明细语义)+ 新增 product_code
- 新增出入库 KPI summary、出库确认售价(confirm-sale) 及路由
- 补充/更新对应单元测试

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-02 08:47:44 +08:00
parent 2c2bb7ccb3
commit 998bbab546
7 changed files with 421 additions and 21 deletions
+63
View File
@@ -163,6 +163,12 @@ type CostConfirmItem struct {
UnitPrice float64 `json:"unit_price"`
}
// SaleConfirmItem 出库「确认售价」的一条明细(先出后定价)。
type SaleConfirmItem struct {
ItemID uint64 `json:"item_id"`
SalePrice float64 `json:"sale_price"`
}
// ConfirmStockInCost 「确认进价」:调货等场景以 0 价(暂估)入库并已审核/已出库后,
// 价格确定时补填真实进价。不反审核、不撤销任何已发生的动作,而是在一个事务内
// 前向写补偿:
@@ -262,6 +268,63 @@ func (s *StockService) ConfirmStockInCost(shopID, orderID, userID uint64, role s
})
}
// ConfirmStockOutSale 出库「先出后定价」:对 sale_price<=0 的明细写真实售价,
// 重算应收(total_amount=Σ售价×数量)并补应收差额流水。仅 admin/superadmin、已审核单可确认。
func (s *StockService) ConfirmStockOutSale(shopID, orderID, userID uint64, role string, items []SaleConfirmItem) error {
if role != "admin" && role != "superadmin" {
return ErrForbidden
}
return s.db.Transaction(func(tx *gorm.DB) error {
var order model.StockOutOrder
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.SalePrice
}
now := time.Now()
var totalDiff float64
for i := range order.Items {
it := &order.Items[i]
newPrice, ok := want[it.ID]
if !ok || newPrice == it.SalePrice {
continue
}
diff := (newPrice - it.SalePrice) * it.Quantity
totalDiff += diff
if err := tx.Model(it).Update("sale_price", newPrice).Error; err != nil {
return err
}
}
if totalDiff == 0 {
return nil
}
// 应收总额按差额重算
if err := tx.Model(&order).
Update("total_amount", gorm.Expr("total_amount + ?", totalDiff)).Error; err != nil {
return err
}
// 应收差额调整流水(滚动余额,与出库审核建应收同口径 last+amount
bal := partnerLastBalance(tx, shopID, order.PartnerID) + totalDiff
oid := order.ID
return tx.Create(&model.FinanceRecord{
ShopID: shopID, PartnerID: order.PartnerID, Type: "receivable",
Amount: totalDiff, Balance: bal, Status: "open",
RefType: "stock_out_sale_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 {