feat(backend): 出入库定价字段消歧 + 总利润落库 + 成本仅管理员可见

- 字段重命名(旧列弃用保留,启动幂等回填 BackfillPricingColumns):
  入库 unit_price/total_price/total_amount → cost_price/cost_amount/cost_total;
  出库同前缀 + 新增 sale_amount(售价小计)/ sale_total(应收)/ profit_total(总利润)
- 建单落三值;确认售价联动重算 sale_amount/sale_total/profit_total;
  确认进价回填成本后联动重算受影响出库单利润(recalcStockOutProfit)
- 出库 List/Get 对 operator/readonly 抹零成本与利润(stripStockOutCost 服务端兜底)
- 兼容一版:Create/Update 接受旧 key unit_price 回落(v1.0.87 及之前客户端)
- SUM 聚合/价格趋势/导入/种子工具同步切新列;测试全量改名 + 6 个新回归

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 12:44:22 +08:00
parent 607b8aa763
commit 4fd0bb8b83
24 changed files with 601 additions and 135 deletions
+7 -6
View File
@@ -497,7 +497,7 @@ func (ic *importCtx) writeOrder(
TenantBase: model.TenantBase{ShopID: ic.shopID},
OrderNo: orderNo, Type: orderType, WarehouseID: ic.whID,
PartnerID: partnerID, OperatorID: operatorID, CreatorID: creatorID,
ReviewerID: reviewerID, Status: status, OrderDate: orderDate, TotalAmount: totalAmount,
ReviewerID: reviewerID, Status: status, OrderDate: orderDate, CostTotal: totalAmount,
}
if err := tx.Create(&o).Error; err != nil {
return err
@@ -510,8 +510,8 @@ func (ic *importCtx) writeOrder(
Series: get(dr, dCols, "系列"),
Spec: get(dr, dCols, "规格"),
Quantity: parseFloatLoose(get(dr, dCols, "数量")),
UnitPrice: parseFloatLoose(get(dr, dCols, "单价")),
TotalPrice: parseFloatLoose(get(dr, dCols, "金额")),
CostPrice: parseFloatLoose(get(dr, dCols, "单价")),
CostAmount: parseFloatLoose(get(dr, dCols, "金额")),
BatchNo: get(dr, dCols, "批次号"),
}
it.ProductionDate = parseDatePtr(get(dr, dCols, "生产日期"))
@@ -529,7 +529,8 @@ func (ic *importCtx) writeOrder(
TenantBase: model.TenantBase{ShopID: ic.shopID},
OrderNo: orderNo, Type: orderType, WarehouseID: ic.whID,
PartnerID: partnerID, OperatorID: operatorID, CreatorID: creatorID,
ReviewerID: reviewerID, Status: status, OrderDate: orderDate, TotalAmount: totalAmount,
// 忠实导入:源「金额」合计沿旧行为落应收合计(sale_price=0 待定价)
ReviewerID: reviewerID, Status: status, OrderDate: orderDate, SaleTotal: totalAmount,
}
if err := tx.Create(&o).Error; err != nil {
return err
@@ -542,8 +543,8 @@ func (ic *importCtx) writeOrder(
Series: get(dr, dCols, "系列"),
Spec: get(dr, dCols, "规格"),
Quantity: parseFloatLoose(get(dr, dCols, "数量")),
UnitPrice: parseFloatLoose(get(dr, dCols, "单价")),
TotalPrice: parseFloatLoose(get(dr, dCols, "金额")),
CostPrice: parseFloatLoose(get(dr, dCols, "单价")),
CostAmount: parseFloatLoose(get(dr, dCols, "金额")),
BatchNo: get(dr, dCols, "批次号"),
}
it.ProductionDate = parseDatePtr(get(dr, dCols, "生产日期"))
+18 -15
View File
@@ -408,14 +408,14 @@ func main() {
// 财务记录(与已审核出库单对应的应收款,按售价口径)
// ═══════════════════════════════════════════════════
createFinanceRecord(db, shop.ID, partners["CUS001"].ID, admin.ID,
"receivable", out1.order.TotalAmount, out1.order.TotalAmount, "stock_out", out1.order.ID,
"receivable", out1.order.SaleTotal, out1.order.SaleTotal, "stock_out", out1.order.ID,
d(3), "君悦大酒店4月供货应收款")
createFinanceRecord(db, shop.ID, partners["CUS002"].ID, admin.ID,
"receivable", out2.order.TotalAmount, out2.order.TotalAmount, "stock_out", out2.order.ID,
"receivable", out2.order.SaleTotal, out2.order.SaleTotal, "stock_out", out2.order.ID,
d(2), "外滩华尔道夫进口酒应收款")
// 模拟一笔已收款
createFinanceRecord(db, shop.ID, partners["CUS001"].ID, admin.ID,
"receipt", out1.order.TotalAmount, 0, "stock_out", out1.order.ID,
"receipt", out1.order.SaleTotal, 0, "stock_out", out1.order.ID,
d(1), "君悦大酒店回款,结清")
// ═══════════════════════════════════════════════════
@@ -556,19 +556,19 @@ func (s *seeder) createStockIn(
Series: g.series,
Spec: g.spec,
Quantity: ln.qty,
UnitPrice: ln.price,
TotalPrice: ln.qty * ln.price,
CostPrice: ln.price,
CostAmount: ln.qty * ln.price,
BatchNo: ln.batch,
ProductionDate: ln.prodDate,
Remark: ln.remark,
}
s.db.Create(&item)
total += item.TotalPrice
total += item.CostAmount
items = append(items, item)
prods = append(prods, p)
}
s.db.Model(&o).Update("total_amount", total)
o.TotalAmount = total
s.db.Model(&o).Update("cost_total", total)
o.CostTotal = total
fmt.Printf("✅ 入库单:%s [%s] 供应商ID=%d 仓库=%s 行数=%d 金额=%.0f\n",
orderNo, status, partnerID, whName, len(lines), total)
@@ -604,7 +604,7 @@ func (s *seeder) applyStockInToInventory(r stockInResult) {
whIDCopy := r.order.WarehouseID
pidCopy := it.ProductID
itemIDCopy := it.ID
price := it.UnitPrice
price := it.CostPrice
inv := model.Inventory{
ShopID: s.shopID,
WarehouseID: &whIDCopy,
@@ -664,7 +664,7 @@ func (s *seeder) createStockOut(
s.db.Create(&o)
var (
total float64 // 应收 = Σ 售价×数量
total, profit float64 // 应收 = Σ 售价×数量;利润 = Σ(售价-成本)×数量
items []model.StockOutItem
)
for _, ln := range lines {
@@ -678,18 +678,21 @@ func (s *seeder) createStockOut(
Series: p.Series,
Spec: p.Spec,
Quantity: ln.qty,
UnitPrice: p.PurchasePrice, // 成本单价(快照)
SalePrice: ln.sale, // 售价
TotalPrice: ln.qty * p.PurchasePrice, // 成本小计
CostPrice: p.PurchasePrice, // 成本单价(快照)
SalePrice: ln.sale, // 售价
CostAmount: ln.qty * p.PurchasePrice, // 成本小计
SaleAmount: ln.qty * ln.sale, // 售价小计
BatchNo: p.BatchNo,
Remark: ln.remark,
}
s.db.Create(&item)
total += ln.qty * ln.sale
profit += (ln.sale - p.PurchasePrice) * ln.qty
items = append(items, item)
}
s.db.Model(&o).Update("total_amount", total)
o.TotalAmount = total
s.db.Model(&o).Updates(map[string]interface{}{"sale_total": total, "profit_total": profit})
o.SaleTotal = total
o.ProfitTotal = profit
fmt.Printf("✅ 出库单:%s [%s] 客户ID=%d 仓库ID=%d 行数=%d 应收=%.0f\n",
orderNo, status, partnerID, whID, len(lines), total)