Files
jiu/backend/internal/model/stock.go
T
wangjia 73955a139c feat(backend): 库存三态落库(stock/on_sale/sold)——挂售可控 + 卖光留痕
- inventories 加 status 枚举列(默认 stock,AutoMigrate 存量即全部「库存」)+ 索引
- 出库审核扣光置 sold 不再软删(留痕台账);退单恢复行置回 stock;盘亏仍软删
- 新接口 PUT /inventory/:id/status(仅收 stock/on_sale,已售行拒改,租户校验)
- List 默认排除已售(显式 status=sold 才显示)+ status 多值筛选
- Summary:SKU/货值/数量排除已售,新增 sold_count
- 公开店铺页/API 只列 on_sale——酒单从全量裸奔改为人工精选
- 测试:卖光置售/部分不改/退单回库/接口边界/跨租户/汇总口径 8 用例
- 设计方案 docs/design/inventory-sale-status.html + db-schema/CLAUDE.md 同步

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-07 13:35:03 +08:00

209 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package model
import "time"
// -------- 入库单 --------
type StockInOrder struct {
TenantBase
OrderNo string `gorm:"size:50;uniqueIndex:uk_shop_order_no" json:"order_no"`
Type string `gorm:"size:30;default:'purchase'" json:"type"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id" binding:"required,min=1"`
PartnerID *uint64 `json:"partner_id"`
OperatorID uint64 `gorm:"not null" json:"operator_id"`
CreatorID *uint64 `json:"creator_id"`
ReviewerID *uint64 `json:"reviewer_id"`
Status string `gorm:"type:enum('draft','pending','approved','rejected');default:'draft'" json:"status"`
OrderDate Date `gorm:"type:date" json:"order_date"`
// 应付合计 = Σ 明细总进价(2026-07 定价字段消歧:旧列 total_amount 弃用,启动回填)
CostTotal float64 `gorm:"column:cost_total;type:decimal(16,2);default:0" json:"cost_total"`
ReviewedAt *time.Time `json:"reviewed_at"`
// 退单状态:none=无 / partial=部分退单 / full=已全退(仅 approved 单可退)
ReturnState string `gorm:"size:20;default:'none'" json:"return_state"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
Items []StockInItem `gorm:"foreignKey:OrderID" json:"items,omitempty"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseID" json:"warehouse,omitempty"`
Partner *Partner `gorm:"foreignKey:PartnerID" json:"partner,omitempty"`
Operator *User `gorm:"foreignKey:OperatorID" json:"operator,omitempty"`
Creator *User `gorm:"foreignKey:CreatorID" json:"creator,omitempty"`
Reviewer *User `gorm:"foreignKey:ReviewerID" json:"reviewer,omitempty"`
}
type StockInItem struct {
Base
OrderID uint64 `gorm:"not null;index" json:"order_id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
ProductID uint64 `gorm:"not null" json:"product_id"`
// 历史导入快照列:源明细自带的商品信息,不依赖 product 主数据(product_id 可为 0)。
ProductCode string `gorm:"size:50" json:"product_code"`
ProductName string `gorm:"size:255" json:"product_name"`
Series string `gorm:"size:100" json:"series"`
Spec string `gorm:"size:100" json:"spec"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
// 进价(单瓶成本)。2026-07 定价字段消歧:unit_price → cost_price(旧列弃用,启动回填)
CostPrice float64 `gorm:"column:cost_price;type:decimal(16,2);default:0" json:"cost_price"`
// 兼容一版:旧客户端(≤1.0.87)建单仍发 unit_price,入参回落到 CostPrice(不落库不出参)
LegacyUnitPrice *float64 `gorm:"-" json:"unit_price,omitempty"`
// 参考售价:仅用于入库建产品时写入 product.SalePrice,不落 stock_in_items 表(gorm:"-")。
SalePrice float64 `gorm:"-" json:"sale_price"`
// 总进价 = quantity × cost_price(旧列 total_price 弃用)
CostAmount float64 `gorm:"column:cost_amount;type:decimal(16,2);default:0" json:"cost_amount"`
// 已退数量:0=未退,=Quantity 表示整行已退单(整行退,不做部分数量)
ReturnedQuantity float64 `gorm:"type:decimal(12,3);default:0" json:"returned_quantity"`
BatchNo string `gorm:"size:50" json:"batch_no"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:255" json:"remark"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
Order *StockInOrder `gorm:"foreignKey:OrderID" json:"order,omitempty"`
}
// -------- 出库单 --------
type StockOutOrder struct {
TenantBase
OrderNo string `gorm:"size:50;uniqueIndex:uk_shop_order_no" json:"order_no"`
Type string `gorm:"size:30;default:'sale'" json:"type"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id" binding:"required,min=1"`
PartnerID *uint64 `json:"partner_id"`
OperatorID uint64 `gorm:"not null" json:"operator_id"`
CreatorID *uint64 `json:"creator_id"`
ReviewerID *uint64 `json:"reviewer_id"`
Status string `gorm:"type:enum('draft','pending','approved','rejected');default:'draft'" json:"status"`
OrderDate Date `gorm:"type:date" json:"order_date"`
// 应收合计 = Σ 明细售价小计(2026-07 定价字段消歧:旧列 total_amount 弃用,启动回填)
SaleTotal float64 `gorm:"column:sale_total;type:decimal(16,2);default:0" json:"sale_total"`
// 总利润 = Σ(sale_price>0 ? (sale_price-cost_price)×qty : 0),建单落库,
// 确认售价 / 确认进价(成本回填)时联动重算
ProfitTotal float64 `gorm:"column:profit_total;type:decimal(16,2);default:0" json:"profit_total"`
ReviewedAt *time.Time `json:"reviewed_at"`
// 退单状态:none / partial / full
ReturnState string `gorm:"size:20;default:'none'" json:"return_state"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
Items []StockOutItem `gorm:"foreignKey:OrderID" json:"items,omitempty"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseID" json:"warehouse,omitempty"`
Partner *Partner `gorm:"foreignKey:PartnerID" json:"partner,omitempty"`
Operator *User `gorm:"foreignKey:OperatorID" json:"operator,omitempty"`
Creator *User `gorm:"foreignKey:CreatorID" json:"creator,omitempty"`
Reviewer *User `gorm:"foreignKey:ReviewerID" json:"reviewer,omitempty"`
}
type StockOutItem struct {
Base
OrderID uint64 `gorm:"not null;index" json:"order_id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
ProductID uint64 `gorm:"not null" json:"product_id"`
// 历史导入快照列:源明细自带的商品信息,不依赖 product 主数据(product_id 可为 0)。
ProductCode string `gorm:"size:50" json:"product_code"`
ProductName string `gorm:"size:255" json:"product_name"`
Series string `gorm:"size:100" json:"series"`
Spec string `gorm:"size:100" json:"spec"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
// 成本单价(入库成本快照)。2026-07 消歧:unit_price → cost_price(旧列弃用)
CostPrice float64 `gorm:"column:cost_price;type:decimal(16,2);default:0" json:"cost_price"`
// 兼容一版:旧客户端(≤1.0.87)建单仍发 unit_price,入参回落到 CostPrice(不落库不出参)
LegacyUnitPrice *float64 `gorm:"-" json:"unit_price,omitempty"`
// 售价:出库实际销售单价(可编辑),应收账款按 售价×数量 计;与 CostPrice(入库成本) 区分
SalePrice float64 `gorm:"type:decimal(16,2);default:0" json:"sale_price"`
// 成本小计 = quantity × cost_price(旧列 total_price 弃用)
CostAmount float64 `gorm:"column:cost_amount;type:decimal(16,2);default:0" json:"cost_amount"`
// 售价小计 = quantity × sale_price(待定价=0;确认售价时重算)
SaleAmount float64 `gorm:"column:sale_amount;type:decimal(16,2);default:0" json:"sale_amount"`
// 已退数量:0=未退,=Quantity 表示整行已退单
ReturnedQuantity float64 `gorm:"type:decimal(12,3);default:0" json:"returned_quantity"`
BatchNo string `gorm:"size:50" json:"batch_no"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:255" json:"remark"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
}
// -------- 实时库存(批次模式:每条记录代表一个批次/批次) --------
type Inventory struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
WarehouseID *uint64 `json:"warehouse_id"`
ProductID *uint64 `json:"product_id"`
StockInItemID *uint64 `json:"stock_in_item_id"`
InventoryCheckID *uint64 `json:"inventory_check_id"`
Quantity float64 `gorm:"type:decimal(12,3);not null;default:0" json:"quantity"`
// Status 库存行三态(2026-07-07):stock=库存(默认,不公开) / on_sale=在售(公开酒单展示) /
// sold=已售(出库审核扣光自动置入,留痕不软删)。stock⇄on_sale 走抽屉 switchsold 仅出库流程写入。
Status string `gorm:"type:enum('stock','on_sale','sold');not null;default:'stock';index:idx_inv_status" json:"status"`
// Snapshot fields: copied from product/warehouse/stock-in at approval time.
// They reflect the state at the moment of stock-in and are NOT updated when the
// referenced product or warehouse record is later modified.
ProductCode string `gorm:"size:50" json:"product_code"`
ProductName string `gorm:"size:200" json:"product_name"`
Series string `gorm:"size:100" json:"series"`
Spec string `gorm:"size:100" json:"spec"`
Unit string `gorm:"size:20" json:"unit"`
WarehouseName string `gorm:"size:100" json:"warehouse_name"`
UnitPrice *float64 `gorm:"type:decimal(16,2)" json:"unit_price"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
BatchNo string `gorm:"size:50" json:"batch_no"`
SupplierName string `gorm:"size:200" json:"supplier_name"`
Remark string `gorm:"size:500" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `gorm:"index" json:"-"`
StockInItem *StockInItem `gorm:"foreignKey:StockInItemID" json:"stock_in_item,omitempty"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseID" json:"warehouse,omitempty"`
}
// -------- 库存流水 --------
type InventoryLog struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
ShopID uint64 `gorm:"not null;index:idx_shop_product" json:"shop_id"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id"`
ProductID uint64 `gorm:"not null;index:idx_shop_product" json:"product_id"`
Direction string `gorm:"type:enum('in','out')" json:"direction"`
Quantity float64 `gorm:"type:decimal(12,3)" json:"quantity"`
QtyBefore float64 `gorm:"type:decimal(12,3)" json:"qty_before"`
QtyAfter float64 `gorm:"type:decimal(12,3)" json:"qty_after"`
RefType string `gorm:"size:30" json:"ref_type"`
RefID uint64 `json:"ref_id"`
OperatorID *uint64 `json:"operator_id"`
CreatedAt time.Time `json:"created_at"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseID" json:"warehouse,omitempty"`
}
// -------- 库存盘点 --------
type InventoryCheck struct {
TenantBase
CheckNo string `gorm:"size:50;uniqueIndex:uk_shop_check_no" json:"check_no"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id"`
OperatorID uint64 `gorm:"not null" json:"operator_id"`
Status string `gorm:"type:enum('draft','completed');default:'draft'" json:"status"`
CheckDate Date `gorm:"type:date" json:"check_date"`
Remark string `gorm:"size:500" json:"remark"`
Items []InventoryCheckItem `gorm:"foreignKey:CheckID" json:"items,omitempty"`
}
type InventoryCheckItem struct {
Base
CheckID uint64 `gorm:"not null;index" json:"check_id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
ProductID uint64 `gorm:"not null" json:"product_id"`
SystemQty float64 `gorm:"type:decimal(12,3)" json:"system_qty"`
ActualQty float64 `gorm:"type:decimal(12,3)" json:"actual_qty"`
DiffQty float64 `gorm:"->" json:"diff_qty"` // generated column,只读
Remark string `gorm:"size:255" json:"remark"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
}