更新
This commit is contained in:
1 parent
5718a00e31
commit
1850ac640e
226 files changed
+37772
-12071
No files matched your search
@@ -0,0 +1,189 @@
|
||||
package erp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// Movement 一次库存变动的入参。库存结存只能由本引擎经流水驱动。
|
||||
type Movement struct {
|
||||
TenantID string
|
||||
ProductID uint64
|
||||
WarehouseID uint64
|
||||
BatchNo string
|
||||
Direction int8 // models.ErpStockIn / ErpStockOut
|
||||
Qty float64
|
||||
UnitCost float64 // 入库=进价/成本;出库传 0 表示按当前加权成本
|
||||
DocType string
|
||||
DocID uint64
|
||||
DocNo string
|
||||
DocItemID uint64
|
||||
OperatorID string
|
||||
OperatorName string
|
||||
Remark string
|
||||
// 批次效期随入库流水落地,出库/调拨沿用同批次快照
|
||||
ProductionDate *time.Time
|
||||
ExpireDate *time.Time
|
||||
}
|
||||
|
||||
// StockError 库存不足等业务错误(调用方可据 ErrInsufficientStock 区分文案)。
|
||||
type StockError struct {
|
||||
ProductName string
|
||||
Requested float64
|
||||
Available float64
|
||||
Insufficient bool
|
||||
}
|
||||
|
||||
func (e *StockError) Error() string {
|
||||
if e.Insufficient {
|
||||
return fmt.Sprintf("商品「%s」库存不足:可用 %.4f,本次需要 %.4f", e.ProductName, e.Available, e.Requested)
|
||||
}
|
||||
return fmt.Sprintf("商品「%s」库存异常", e.ProductName)
|
||||
}
|
||||
|
||||
// ApplyMovement 在事务内执行一条库存变动:
|
||||
// 1. 定位/创建结存行(tenant+product+warehouse+batch 唯一);
|
||||
// 2. 出库时校验可用量(负库存开关取自基础设置);
|
||||
// 3. 入库按移动加权平均重算单位成本;出库成本=出库时加权成本(记入流水,毛利口径);
|
||||
// 4. 写库存流水 + 更新结存。
|
||||
//
|
||||
// 返回出库时点的单位成本(销售成本/退货回冲依据)。
|
||||
func ApplyMovement(o orm.QueryExecutor, mv *Movement) (float64, error) {
|
||||
if mv.Qty <= 0 {
|
||||
return 0, fmt.Errorf("库存变动数量必须大于0")
|
||||
}
|
||||
if mv.WarehouseID == 0 || mv.ProductID == 0 {
|
||||
return 0, fmt.Errorf("库存变动缺少商品或仓库")
|
||||
}
|
||||
|
||||
settings := GetSettings(mv.TenantID)
|
||||
|
||||
var product models.ErpProduct
|
||||
if err := o.QueryTable(new(models.ErpProduct)).
|
||||
Filter("id", mv.ProductID).Filter("tenant_id", mv.TenantID).One(&product); err != nil {
|
||||
return 0, fmt.Errorf("商品不存在(id=%d)", mv.ProductID)
|
||||
}
|
||||
|
||||
var stock models.ErpStock
|
||||
err := o.QueryTable(new(models.ErpStock)).
|
||||
Filter("tenant_id", mv.TenantID).
|
||||
Filter("product_id", mv.ProductID).
|
||||
Filter("warehouse_id", mv.WarehouseID).
|
||||
Filter("batch_no", mv.BatchNo).
|
||||
ForUpdate().One(&stock)
|
||||
if err == orm.ErrNoRows {
|
||||
stock = models.ErpStock{
|
||||
TenantID: mv.TenantID, ProductID: mv.ProductID,
|
||||
WarehouseID: mv.WarehouseID, BatchNo: mv.BatchNo,
|
||||
Quantity: 0, CostPrice: product.PurchasePrice,
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
id, ierr := o.Insert(&stock)
|
||||
if ierr != nil {
|
||||
return 0, fmt.Errorf("初始化库存结存失败: %v", ierr)
|
||||
}
|
||||
stock.ID = uint64(id)
|
||||
} else if err != nil {
|
||||
return 0, fmt.Errorf("查询库存失败: %v", err)
|
||||
}
|
||||
|
||||
before := stock.Quantity
|
||||
dir := mv.Direction
|
||||
unitCost := mv.UnitCost
|
||||
outCost := stock.CostPrice // 出库成本口径:当前加权成本
|
||||
|
||||
if dir == models.ErpStockOut {
|
||||
if !settings.NegativeAllowed && before-mv.Qty < -0.000001 {
|
||||
return 0, &StockError{
|
||||
ProductName: product.ProductName,
|
||||
Requested: mv.Qty,
|
||||
Available: before,
|
||||
Insufficient: true,
|
||||
}
|
||||
}
|
||||
after := before - mv.Qty
|
||||
if unitCost <= 0 {
|
||||
unitCost = outCost
|
||||
}
|
||||
if err := writeLedger(o, mv, dir, before, after, unitCost); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err := o.QueryTable(new(models.ErpStock)).Filter("id", stock.ID).
|
||||
Update(orm.Params{"quantity": after, "update_time": time.Now()}); err != nil {
|
||||
return 0, fmt.Errorf("更新库存结存失败: %v", err)
|
||||
}
|
||||
return unitCost, nil
|
||||
}
|
||||
|
||||
// 入库:移动加权平均
|
||||
if unitCost <= 0 {
|
||||
unitCost = product.PurchasePrice
|
||||
}
|
||||
after := before + mv.Qty
|
||||
newCost := unitCost
|
||||
if after > 0.000001 {
|
||||
newCost = (before*stock.CostPrice + mv.Qty*unitCost) / after
|
||||
}
|
||||
if err := writeLedger(o, mv, dir, before, after, unitCost); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err := o.QueryTable(new(models.ErpStock)).Filter("id", stock.ID).
|
||||
Update(orm.Params{"quantity": after, "cost_price": newCost, "update_time": time.Now()}); err != nil {
|
||||
return 0, fmt.Errorf("更新库存结存失败: %v", err)
|
||||
}
|
||||
return unitCost, nil
|
||||
}
|
||||
|
||||
func writeLedger(o orm.QueryExecutor, mv *Movement, dir int8, before, after, unitCost float64) error {
|
||||
now := time.Now()
|
||||
ledger := models.ErpStockLedger{
|
||||
TenantID: mv.TenantID, ProductID: mv.ProductID, WarehouseID: mv.WarehouseID,
|
||||
BatchNo: mv.BatchNo, Direction: dir, Quantity: mv.Qty,
|
||||
BeforeQty: before, AfterQty: after,
|
||||
UnitCost: unitCost, TotalCost: round2(unitCost * mv.Qty),
|
||||
DocType: mv.DocType, DocID: mv.DocID, DocNo: mv.DocNo, DocItemID: mv.DocItemID,
|
||||
ProductionDate: mv.ProductionDate, ExpireDate: mv.ExpireDate,
|
||||
OperatorID: mv.OperatorID, OperatorName: mv.OperatorName,
|
||||
Remark: mv.Remark, CreateTime: now,
|
||||
}
|
||||
if _, err := o.Insert(&ledger); err != nil {
|
||||
return fmt.Errorf("写入库存流水失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefOutboundCost 取某条出库流水记录的出库成本(销售退货回冲入库成本用)。
|
||||
func RefOutboundCost(o orm.QueryExecutor, tenantID string, docItemID uint64) float64 {
|
||||
if docItemID == 0 {
|
||||
return 0
|
||||
}
|
||||
var led models.ErpStockLedger
|
||||
if err := o.QueryTable(new(models.ErpStockLedger)).
|
||||
Filter("tenant_id", tenantID).
|
||||
Filter("doc_item_id", docItemID).
|
||||
Filter("direction", models.ErpStockOut).
|
||||
OrderBy("-id").One(&led); err != nil {
|
||||
return 0
|
||||
}
|
||||
return led.UnitCost
|
||||
}
|
||||
|
||||
func round2(v float64) float64 {
|
||||
return math.Round(v*100) / 100
|
||||
}
|
||||
|
||||
func round4(v float64) float64 {
|
||||
return math.Round(v*10000) / 10000
|
||||
}
|
||||
|
||||
func absf(v float64) float64 {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
Reference in new issue
Block a user