feat(backend): 出入库编码搜索双路匹配+建单填快照;finance 趋势/日期区间;partner/product 拼音搜索
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -51,14 +52,16 @@ func (h *StockOutHandler) List(c *gin.Context) {
|
||||
// 酒名「product 主数据名 OR 明细快照名」任一命中——不能用 COALESCE 二选一:历史导入单的
|
||||
// 明细 product_id 统一指向占位商品「历史导入占位」(p.name 非空),COALESCE 会被占位名劫持,
|
||||
// 永远取不到真实快照 product_name,导致导入单按真实酒名搜不到。拼音只在 products 上(快照无拼音列)。
|
||||
// 编码同理「主数据 p.code OR 快照 it.product_code」双匹配:存量应用建单的明细快照为空串,
|
||||
// 只查快照会「页面看得到编码、搜索搜不到」。
|
||||
query = query.Where(
|
||||
"order_no LIKE ?"+
|
||||
" OR id IN (SELECT it.order_id FROM stock_out_items it"+
|
||||
" LEFT JOIN products p ON p.id = it.product_id AND p.shop_id = it.shop_id"+
|
||||
" WHERE it.shop_id = ? AND it.deleted_at IS NULL"+
|
||||
" AND (p.name LIKE ? OR it.product_name LIKE ? OR it.product_code LIKE ?"+
|
||||
" OR p.name_pinyin LIKE ? OR p.name_initials LIKE ?))",
|
||||
like, shopID, like, like, like, like, like,
|
||||
" OR p.code LIKE ? OR p.name_pinyin LIKE ? OR p.name_initials LIKE ?))",
|
||||
like, shopID, like, like, like, like, like, like,
|
||||
)
|
||||
}
|
||||
// 详细搜索多字段(各参数独立可选、组合为 AND)
|
||||
@@ -147,6 +150,44 @@ func (h *StockOutHandler) Get(c *gin.Context) {
|
||||
|
||||
|
||||
// Create POST /api/v1/stock-out/orders
|
||||
// fillStockOutItemSnapshots 按 product_id 从商品主数据拷明细快照列(编码/名称/系列/规格/批次/生产日期)。
|
||||
// 明细 = product 引用 + 快照(历史保真:商品日后改名/删除,单据仍能还原当时信息;搜索/退单提示读快照)。
|
||||
// 入库建单在 createIndependentProduct 后即填快照,出库同样必须在建单/改单时填齐。
|
||||
func fillStockOutItemSnapshots(db *gorm.DB, shopID uint64, items []model.StockOutItem) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]uint64, 0, len(items))
|
||||
for i := range items {
|
||||
ids = append(ids, items[i].ProductID)
|
||||
}
|
||||
var prods []model.Product
|
||||
if err := db.Where("shop_id = ? AND id IN ?", shopID, ids).Find(&prods).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
byID := make(map[uint64]*model.Product, len(prods))
|
||||
for i := range prods {
|
||||
byID[prods[i].ID] = &prods[i]
|
||||
}
|
||||
for i := range items {
|
||||
p, ok := byID[items[i].ProductID]
|
||||
if !ok {
|
||||
return fmt.Errorf("明细第 %d 行商品不存在", i+1)
|
||||
}
|
||||
items[i].ProductCode = p.Code
|
||||
items[i].ProductName = p.Name
|
||||
items[i].Series = p.Series
|
||||
items[i].Spec = p.Spec
|
||||
if items[i].BatchNo == "" {
|
||||
items[i].BatchNo = p.BatchNo
|
||||
}
|
||||
if items[i].ProductionDate == nil {
|
||||
items[i].ProductionDate = p.ProductionDate
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *StockOutHandler) Create(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
operatorID := middleware.GetUserID(c)
|
||||
@@ -186,6 +227,11 @@ func (h *StockOutHandler) Create(c *gin.Context) {
|
||||
}
|
||||
req.TotalAmount = total
|
||||
|
||||
if err := fillStockOutItemSnapshots(h.db, shopID, req.Items); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Create(&req).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -222,6 +268,9 @@ func (h *StockOutHandler) Update(c *gin.Context) {
|
||||
req.Items[i].TotalPrice = req.Items[i].Quantity * req.Items[i].UnitPrice
|
||||
total += req.Items[i].Quantity * req.Items[i].SalePrice
|
||||
}
|
||||
if err := fillStockOutItemSnapshots(tx, shopID, req.Items); err != nil {
|
||||
return err
|
||||
}
|
||||
updates := map[string]interface{}{
|
||||
"warehouse_id": req.WarehouseID,
|
||||
"partner_id": req.PartnerID,
|
||||
|
||||
Reference in New Issue
Block a user