更新
This commit is contained in:
1 parent
5718a00e31
commit
1850ac640e
226 files changed
+37772
-12071
No files matched your search
@@ -0,0 +1,568 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// ERP 报表/看板核心(两端共用):统计口径只认「已生效」单据与库存流水,
|
||||
// 草稿/审批中/作废一律不参与,这样 Web 端与手机端看到的金额、毛利、库存预警数完全一致。
|
||||
|
||||
// erpReportDimension 归一购销汇总维度:只认 product|supplier|customer|month,
|
||||
// 其它一律回落到商品,出参的 dimension 字段即后端真实使用的分组。
|
||||
func erpReportDimension(raw string) string {
|
||||
switch g := strings.TrimSpace(raw); g {
|
||||
case "supplier", "customer", "month":
|
||||
return g
|
||||
default:
|
||||
return "product"
|
||||
}
|
||||
}
|
||||
|
||||
// erpWarningCounts 库存预警各类计数(看板角标用,不含明细)
|
||||
func erpWarningCounts(tid string) map[string]int {
|
||||
var products []models.ErpProduct
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpProduct)).
|
||||
Filter("tenant_id", tid).Filter("status", 1).Filter("is_draft", 0).
|
||||
Filter("delete_time__isnull", true).All(&products, "id", "safety_qty", "max_qty")
|
||||
var stocks []models.ErpStock
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpStock)).Filter("tenant_id", tid).
|
||||
All(&stocks, "product_id", "quantity")
|
||||
qtyMap := map[uint64]float64{}
|
||||
for _, s := range stocks {
|
||||
qtyMap[s.ProductID] += s.Quantity
|
||||
}
|
||||
low, over, out := 0, 0, 0
|
||||
for _, p := range products {
|
||||
q := qtyMap[p.ID]
|
||||
if p.SafetyQty > 0 && q < p.SafetyQty {
|
||||
low++
|
||||
if q <= 0.000001 {
|
||||
out++
|
||||
}
|
||||
}
|
||||
if p.MaxQty > 0 && q > p.MaxQty {
|
||||
over++
|
||||
}
|
||||
}
|
||||
return map[string]int{"low": low, "over": over, "out": out, "expiring": len(erpExpiringBatches(tid, 30))}
|
||||
}
|
||||
|
||||
// erpDocAmount 区间内已生效单据的金额合计与单数
|
||||
func erpDocAmount(tid string, docTypes []string, from, to time.Time) (float64, int64) {
|
||||
ids := erpApprovedDocIDs(tid, docTypes, from, to, 0)
|
||||
if len(ids) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
var docs []models.ErpDocument
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpDocument)).
|
||||
Filter("id__in", ids).All(&docs, "id", "actual_amount")
|
||||
var sum float64
|
||||
for _, d := range docs {
|
||||
sum += d.ActualAmount
|
||||
}
|
||||
return round2f(sum), int64(len(docs))
|
||||
}
|
||||
|
||||
// erpDashboardTrend 近 6 个月采购/销售趋势
|
||||
type erpDashboardTrend struct {
|
||||
Month string `json:"month"`
|
||||
Buy float64 `json:"buy"`
|
||||
Sale float64 `json:"sale"`
|
||||
}
|
||||
|
||||
// erpDashboardCore 经营看板:本月购销/毛利、今日销售、库存价值、往来余额、待办、趋势、预警
|
||||
func erpDashboardCore(a erpActor) map[string]interface{} {
|
||||
tid := a.Tid
|
||||
monthFrom, monthTo := erpMonthRange(0)
|
||||
// 不能用 time.Now().Truncate(24*time.Hour):它按「自零时起算的绝对时长」取整,
|
||||
// 在东八区得到的是 UTC 零点(本地 08:00),会把当天 00:00–08:00 的单据漏掉。
|
||||
now := time.Now()
|
||||
todayFrom := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
|
||||
buyAmt, buyCnt := erpDocAmount(tid, []string{models.ErpDocPurchaseIn}, monthFrom, monthTo)
|
||||
saleAmt, saleCnt := erpDocAmount(tid, []string{models.ErpDocSalesOut}, monthFrom, monthTo)
|
||||
backAmt, _ := erpDocAmount(tid, []string{models.ErpDocSalesReturn}, monthFrom, monthTo)
|
||||
todaySale, todayCnt := erpDocAmount(tid, []string{models.ErpDocSalesOut}, todayFrom, todayFrom.AddDate(0, 0, 1))
|
||||
|
||||
// 本月毛利(出库流水成本口径)
|
||||
var ledgers []models.ErpStockLedger
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpStockLedger)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("doc_type__in", []string{models.ErpDocSalesOut, models.ErpDocSalesReturn}).
|
||||
Filter("create_time__gte", monthFrom).Filter("create_time__lt", monthTo).
|
||||
All(&ledgers, "direction", "total_cost")
|
||||
var cost float64
|
||||
for _, l := range ledgers {
|
||||
if l.Direction == models.ErpStockOut {
|
||||
cost += l.TotalCost
|
||||
} else {
|
||||
cost -= l.TotalCost
|
||||
}
|
||||
}
|
||||
profit := saleAmt - backAmt - cost
|
||||
|
||||
var stocks []models.ErpStock
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpStock)).Filter("tenant_id", tid).
|
||||
All(&stocks, "quantity", "cost_price")
|
||||
var stockValue, stockQty float64
|
||||
for _, s := range stocks {
|
||||
stockValue += s.Quantity * s.CostPrice
|
||||
stockQty += s.Quantity
|
||||
}
|
||||
|
||||
var bills []models.ErpPartnerBill
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpPartnerBill)).
|
||||
Filter("tenant_id", tid).Filter("status__lt", 2).
|
||||
All(&bills, "bill_type", "amount", "settled_amount")
|
||||
var recv, pay float64
|
||||
for _, b := range bills {
|
||||
if b.BillType == models.ErpBillReceivable {
|
||||
recv += b.Amount - b.SettledAmount
|
||||
} else {
|
||||
pay += b.Amount - b.SettledAmount
|
||||
}
|
||||
}
|
||||
|
||||
// 待办:待我审批的进销存单据 + 草稿单据数
|
||||
pendingTasks := erpErpPendingTaskCount(a.TidNum, a.UID)
|
||||
var draftCnt int64
|
||||
if n, e := models.Orm.QueryTable(new(models.ErpDocument)).
|
||||
Filter("tenant_id", tid).Filter("status", models.ErpDocStatusDraft).
|
||||
Filter("delete_time__isnull", true).Count(); e == nil {
|
||||
draftCnt = n
|
||||
}
|
||||
|
||||
trendList := make([]erpDashboardTrend, 0, 6)
|
||||
for i := 5; i >= 0; i-- {
|
||||
f, t := erpMonthRange(-i)
|
||||
b, _ := erpDocAmount(tid, []string{models.ErpDocPurchaseIn}, f, t)
|
||||
s, _ := erpDocAmount(tid, []string{models.ErpDocSalesOut}, f, t)
|
||||
trendList = append(trendList, erpDashboardTrend{Month: f.Format("2006-01"), Buy: b, Sale: s})
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"month": map[string]interface{}{
|
||||
"purchase_amount": buyAmt, "purchase_count": buyCnt,
|
||||
"sales_amount": saleAmt, "sales_count": saleCnt,
|
||||
"return_amount": backAmt, "cost": round2f(cost),
|
||||
"profit": round2f(profit),
|
||||
"profit_rate": func() float64 {
|
||||
if saleAmt-backAmt > 0.005 {
|
||||
return round2f(profit / (saleAmt - backAmt) * 100)
|
||||
}
|
||||
return 0
|
||||
}(),
|
||||
},
|
||||
"today": map[string]interface{}{"sales_amount": todaySale, "sales_count": todayCnt},
|
||||
"stock": map[string]interface{}{"value": round2f(stockValue), "qty": round4f(stockQty)},
|
||||
"partner_bill": map[string]interface{}{
|
||||
"receivable": round2f(recv), "payable": round2f(pay), "net": round2f(recv - pay),
|
||||
},
|
||||
"todo": map[string]interface{}{"approval_pending": pendingTasks, "draft_docs": draftCnt},
|
||||
"trend": trendList,
|
||||
"warning": erpWarningCounts(tid),
|
||||
}
|
||||
}
|
||||
|
||||
// erpReportRow 汇总报表行(按商品/往来单位/月份聚合后的结果)
|
||||
type erpReportRow struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Qty float64 `json:"qty"`
|
||||
Amount float64 `json:"amount"`
|
||||
Lines int64 `json:"lines"`
|
||||
}
|
||||
|
||||
// erpSummaryReportData 购销汇总:按商品/往来单位/月份三种维度聚合,退货行做负向冲减
|
||||
func erpSummaryReportData(group string, items []models.ErpDocumentItem,
|
||||
docPartner map[uint64]models.ErpDocument, returnItems []models.ErpDocumentItem) map[string]interface{} {
|
||||
|
||||
agg := map[string]*erpReportRow{}
|
||||
add := func(key, label string, qty, amount float64) {
|
||||
r := agg[key]
|
||||
if r == nil {
|
||||
r = &erpReportRow{Key: key, Label: label}
|
||||
agg[key] = r
|
||||
}
|
||||
r.Qty += qty
|
||||
r.Amount += amount
|
||||
r.Lines++
|
||||
}
|
||||
bucket := func(it models.ErpDocumentItem) (string, string) {
|
||||
d := docPartner[it.DocID]
|
||||
switch group {
|
||||
case "supplier", "customer":
|
||||
return fmt.Sprintf("%d", d.PartnerID), d.PartnerName
|
||||
case "month":
|
||||
return d.BizDate.Format("2006-01"), d.BizDate.Format("2006-01")
|
||||
default:
|
||||
return fmt.Sprintf("%d", it.ProductID), it.ProductName + " " + it.Spec
|
||||
}
|
||||
}
|
||||
for _, it := range items {
|
||||
k, lb := bucket(it)
|
||||
add(k, lb, it.Quantity, it.Amount)
|
||||
}
|
||||
// 退货按同维度负向冲减,净额与往来账口径一致
|
||||
for _, it := range returnItems {
|
||||
k, lb := bucket(it)
|
||||
if group == "supplier" || group == "customer" {
|
||||
d := docPartner[it.DocID]
|
||||
if d.PartnerID == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
add(k, lb, -it.Quantity, -it.Amount)
|
||||
}
|
||||
list := make([]*erpReportRow, 0, len(agg))
|
||||
var tQty, tAmount float64
|
||||
for _, r := range agg {
|
||||
r.Qty = round4f(r.Qty)
|
||||
r.Amount = round2f(r.Amount)
|
||||
if absFloat(r.Amount) < 0.005 && absFloat(r.Qty) < 0.000001 {
|
||||
continue
|
||||
}
|
||||
list = append(list, r)
|
||||
tQty += r.Qty
|
||||
tAmount += r.Amount
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].Amount != list[j].Amount {
|
||||
return list[i].Amount > list[j].Amount
|
||||
}
|
||||
return list[i].Key < list[j].Key // 金额相同(如对冲后同为 0)时按维度键定序,避免两次请求行序抖动
|
||||
})
|
||||
return map[string]interface{}{
|
||||
"list": list, "dimension": group,
|
||||
"summary": map[string]interface{}{"qty": round4f(tQty), "amount": round2f(tAmount)},
|
||||
}
|
||||
}
|
||||
|
||||
// erpSalesRankRow 热销/滞销排行行
|
||||
type erpSalesRankRow struct {
|
||||
ProductID uint64 `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductCode string `json:"product_code"`
|
||||
Spec string `json:"spec"`
|
||||
Unit string `json:"unit"`
|
||||
Qty float64 `json:"qty"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
// erpSalesRankCore 热销(desc)/滞销(asc)排行;滞销榜需要把「一件没卖」的在售商品也排进来
|
||||
func erpSalesRankCore(a erpActor, from, to time.Time, top int, asc bool) (map[string]interface{}, int, string) {
|
||||
if top < 1 || top > 50 {
|
||||
top = 10
|
||||
}
|
||||
tid := a.Tid
|
||||
ids := erpApprovedDocIDs(tid, []string{models.ErpDocSalesOut}, from, to, 0)
|
||||
items := erpItemsByDocs(tid, ids)
|
||||
agg := map[uint64]*erpSalesRankRow{}
|
||||
for _, it := range items {
|
||||
r := agg[it.ProductID]
|
||||
if r == nil {
|
||||
r = &erpSalesRankRow{ProductID: it.ProductID, ProductName: it.ProductName, Spec: it.Spec, Unit: it.Unit}
|
||||
agg[it.ProductID] = r
|
||||
}
|
||||
r.Qty += it.Quantity
|
||||
r.Amount += it.Amount
|
||||
}
|
||||
if asc {
|
||||
var ps []models.ErpProduct
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpProduct)).
|
||||
Filter("tenant_id", tid).Filter("status", 1).Filter("is_draft", 0).
|
||||
Filter("delete_time__isnull", true).All(&ps, "id", "product_name", "spec", "base_unit")
|
||||
for _, p := range ps {
|
||||
if agg[p.ID] == nil {
|
||||
agg[p.ID] = &erpSalesRankRow{ProductID: p.ID, ProductName: p.ProductName, Spec: p.Spec, Unit: p.BaseUnit}
|
||||
}
|
||||
}
|
||||
}
|
||||
pmap := erpProductMap(tid, keysOfU64(agg))
|
||||
list := make([]*erpSalesRankRow, 0, len(agg))
|
||||
for _, r := range agg {
|
||||
r.ProductCode = pmap[r.ProductID].ProductCode
|
||||
r.Qty = round4f(r.Qty)
|
||||
r.Amount = round2f(r.Amount)
|
||||
list = append(list, r)
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if asc {
|
||||
if list[i].Amount != list[j].Amount {
|
||||
return list[i].Amount < list[j].Amount
|
||||
}
|
||||
if list[i].Qty != list[j].Qty {
|
||||
return list[i].Qty < list[j].Qty
|
||||
}
|
||||
} else {
|
||||
if list[i].Amount != list[j].Amount {
|
||||
return list[i].Amount > list[j].Amount
|
||||
}
|
||||
if list[i].Qty != list[j].Qty {
|
||||
return list[i].Qty > list[j].Qty
|
||||
}
|
||||
}
|
||||
return list[i].ProductID < list[j].ProductID
|
||||
})
|
||||
if len(list) > top {
|
||||
list = list[:top]
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"list": list, "order": map[bool]string{true: "asc", false: "desc"}[asc],
|
||||
"range": []string{from.Format("2006-01-02"), to.AddDate(0, 0, -1).Format("2006-01-02")},
|
||||
}, 200, ""
|
||||
}
|
||||
|
||||
// erpSummaryPurchaseCore / erpSummarySalesCore 购销汇总取数:两端按维度调用同一份聚合
|
||||
func erpSummaryDocs(tid, docType, returnType string, from, to time.Time, partnerID uint64) (
|
||||
[]models.ErpDocumentItem, map[uint64]models.ErpDocument, []models.ErpDocumentItem) {
|
||||
ids := erpApprovedDocIDs(tid, []string{docType}, from, to, partnerID)
|
||||
rids := erpApprovedDocIDs(tid, []string{returnType}, from, to, partnerID)
|
||||
items := erpItemsByDocs(tid, ids)
|
||||
returnItems := erpItemsByDocs(tid, rids)
|
||||
return items, erpDocPartnerMap(append(append([]uint64{}, ids...), rids...)), returnItems
|
||||
}
|
||||
|
||||
// erpStockMoveRow 收发存汇总行:期末取自库存结存,本期收发取自库存流水,
|
||||
// 期初=期末-(入-出),保证与账实一致。
|
||||
type erpStockMoveRow struct {
|
||||
ProductID uint64 `json:"product_id"`
|
||||
ProductCode string `json:"product_code"`
|
||||
ProductName string `json:"product_name"`
|
||||
Spec string `json:"spec"`
|
||||
Unit string `json:"unit"`
|
||||
CategoryName string `json:"category_name"`
|
||||
WarehouseID uint64 `json:"warehouse_id"`
|
||||
WarehouseName string `json:"warehouse_name"`
|
||||
OpenQty float64 `json:"open_qty"`
|
||||
OpenAmount float64 `json:"open_amount"`
|
||||
InQty float64 `json:"in_qty"`
|
||||
InAmount float64 `json:"in_amount"`
|
||||
OutQty float64 `json:"out_qty"`
|
||||
OutAmount float64 `json:"out_amount"`
|
||||
EndQty float64 `json:"end_qty"`
|
||||
EndCost float64 `json:"end_cost"`
|
||||
EndAmount float64 `json:"end_amount"`
|
||||
}
|
||||
|
||||
// erpStockMoveCore 收发存汇总表(分页 + 全量合计)
|
||||
func erpStockMoveCore(a erpActor, from, to time.Time, warehouseID uint64, keyword string,
|
||||
page, pageSize int) ([]erpStockMoveRow, int64, map[string]float64, int, string) {
|
||||
|
||||
tid := a.Tid
|
||||
var stocks []models.ErpStock
|
||||
qs := models.Orm.QueryTable(new(models.ErpStock)).Filter("tenant_id", tid)
|
||||
if warehouseID > 0 {
|
||||
qs = qs.Filter("warehouse_id", warehouseID)
|
||||
}
|
||||
_, _ = qs.All(&stocks)
|
||||
|
||||
var ledgers []models.ErpStockLedger
|
||||
lqs := models.Orm.QueryTable(new(models.ErpStockLedger)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("create_time__gte", from).Filter("create_time__lt", to)
|
||||
if warehouseID > 0 {
|
||||
lqs = lqs.Filter("warehouse_id", warehouseID)
|
||||
}
|
||||
_, _ = lqs.All(&ledgers)
|
||||
|
||||
type key struct {
|
||||
productID, warehouseID uint64
|
||||
}
|
||||
endQty := map[key]float64{}
|
||||
endCost := map[key]float64{}
|
||||
endValue := map[key]float64{}
|
||||
keys := make([]key, 0, len(stocks))
|
||||
for _, s := range stocks {
|
||||
k := key{s.ProductID, s.WarehouseID}
|
||||
if _, hit := endQty[k]; !hit {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
endQty[k] += s.Quantity
|
||||
endValue[k] += s.Quantity * s.CostPrice
|
||||
endCost[k] = s.CostPrice
|
||||
}
|
||||
inQty := map[key]float64{}
|
||||
inAmt := map[key]float64{}
|
||||
outQty := map[key]float64{}
|
||||
outAmt := map[key]float64{}
|
||||
for _, l := range ledgers {
|
||||
k := key{l.ProductID, l.WarehouseID}
|
||||
if l.Direction == models.ErpStockIn {
|
||||
inQty[k] += l.Quantity
|
||||
inAmt[k] += l.TotalCost
|
||||
} else {
|
||||
outQty[k] += l.Quantity
|
||||
outAmt[k] += l.TotalCost
|
||||
}
|
||||
// 区间内有流水但当前无结存的组合也要出现在表里(已全部出库)
|
||||
found := false
|
||||
for _, e := range keys {
|
||||
if e == k {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
keys = append(keys, k)
|
||||
endQty[k] = 0
|
||||
}
|
||||
}
|
||||
|
||||
ids := make([]uint64, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
ids = append(ids, k.productID)
|
||||
}
|
||||
pmap := erpProductMap(tid, ids)
|
||||
wmap := erpWarehouseNameMap(tid)
|
||||
cats := erpCategoryMap(tid)
|
||||
|
||||
list := make([]erpStockMoveRow, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
p := pmap[k.productID]
|
||||
openQty := endQty[k] - (inQty[k] - outQty[k])
|
||||
openAmt := endValue[k] - (inAmt[k] - outAmt[k])
|
||||
cost := endCost[k]
|
||||
if endQty[k] > 0.000001 {
|
||||
cost = endValue[k] / endQty[k]
|
||||
}
|
||||
list = append(list, erpStockMoveRow{
|
||||
ProductID: k.productID, ProductCode: p.ProductCode, ProductName: p.ProductName,
|
||||
Spec: p.Spec, Unit: p.BaseUnit, CategoryName: cats[p.CategoryID],
|
||||
WarehouseID: k.warehouseID, WarehouseName: wmap[k.warehouseID],
|
||||
OpenQty: round4f(openQty), OpenAmount: round2f(openAmt),
|
||||
InQty: round4f(inQty[k]), InAmount: round2f(inAmt[k]),
|
||||
OutQty: round4f(outQty[k]), OutAmount: round2f(outAmt[k]),
|
||||
EndQty: round4f(endQty[k]), EndCost: round2f(cost), EndAmount: round2f(endValue[k]),
|
||||
})
|
||||
}
|
||||
if kw := strings.TrimSpace(keyword); kw != "" {
|
||||
list = erpFilterRows(list, func(r erpStockMoveRow) bool {
|
||||
return strings.Contains(r.ProductName, kw) || strings.Contains(r.ProductCode, kw)
|
||||
})
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].ProductName != list[j].ProductName {
|
||||
return list[i].ProductName < list[j].ProductName
|
||||
}
|
||||
if list[i].WarehouseName != list[j].WarehouseName {
|
||||
return list[i].WarehouseName < list[j].WarehouseName
|
||||
}
|
||||
if list[i].ProductID != list[j].ProductID {
|
||||
return list[i].ProductID < list[j].ProductID
|
||||
}
|
||||
return list[i].WarehouseID < list[j].WarehouseID
|
||||
})
|
||||
|
||||
sum := erpStockMoveRow{}
|
||||
for _, r := range list {
|
||||
sum.InQty += r.InQty
|
||||
sum.InAmount += r.InAmount
|
||||
sum.OutQty += r.OutQty
|
||||
sum.OutAmount += r.OutAmount
|
||||
sum.EndAmount += r.EndAmount
|
||||
}
|
||||
summary := map[string]float64{
|
||||
"in_qty": round4f(sum.InQty), "in_amount": round2f(sum.InAmount),
|
||||
"out_qty": round4f(sum.OutQty), "out_amount": round2f(sum.OutAmount),
|
||||
"end_amount": round2f(sum.EndAmount),
|
||||
}
|
||||
start, end := erpPageWindow(len(list), page, pageSize)
|
||||
return list[start:end], int64(len(list)), summary, 200, ""
|
||||
}
|
||||
|
||||
// erpProfitRow 毛利分析行:收入=销售出库金额-销售退货金额,
|
||||
// 成本取库存流水的实际出库成本(移动加权),不受手工录入影响。
|
||||
type erpProfitRow struct {
|
||||
ProductID uint64 `json:"product_id"`
|
||||
ProductCode string `json:"product_code"`
|
||||
ProductName string `json:"product_name"`
|
||||
Spec string `json:"spec"`
|
||||
Unit string `json:"unit"`
|
||||
Qty float64 `json:"qty"`
|
||||
Revenue float64 `json:"revenue"`
|
||||
Cost float64 `json:"cost"`
|
||||
Profit float64 `json:"profit"`
|
||||
ProfitRate float64 `json:"profit_rate"`
|
||||
}
|
||||
|
||||
// erpProfitCore 毛利分析(按商品)
|
||||
func erpProfitCore(a erpActor, from, to time.Time) (map[string]interface{}, int, string) {
|
||||
tid := a.Tid
|
||||
outIDs := erpApprovedDocIDs(tid, []string{models.ErpDocSalesOut}, from, to, 0)
|
||||
backIDs := erpApprovedDocIDs(tid, []string{models.ErpDocSalesReturn}, from, to, 0)
|
||||
revenue := map[uint64]float64{}
|
||||
qty := map[uint64]float64{}
|
||||
names := map[uint64]models.ErpDocumentItem{}
|
||||
for _, it := range erpItemsByDocs(tid, outIDs) {
|
||||
revenue[it.ProductID] += it.Amount
|
||||
qty[it.ProductID] += it.Quantity
|
||||
names[it.ProductID] = it
|
||||
}
|
||||
for _, it := range erpItemsByDocs(tid, backIDs) {
|
||||
revenue[it.ProductID] -= it.Amount
|
||||
qty[it.ProductID] -= it.Quantity
|
||||
if _, hit := names[it.ProductID]; !hit {
|
||||
names[it.ProductID] = it
|
||||
}
|
||||
}
|
||||
|
||||
var ledgers []models.ErpStockLedger
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpStockLedger)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("doc_type__in", []string{models.ErpDocSalesOut, models.ErpDocSalesReturn}).
|
||||
Filter("create_time__gte", from).Filter("create_time__lt", to).
|
||||
All(&ledgers)
|
||||
cost := map[uint64]float64{}
|
||||
for _, l := range ledgers {
|
||||
if l.Direction == models.ErpStockOut {
|
||||
cost[l.ProductID] += l.TotalCost
|
||||
} else {
|
||||
cost[l.ProductID] -= l.TotalCost
|
||||
}
|
||||
}
|
||||
|
||||
pmap := erpProductMap(tid, keysOfU64(revenue))
|
||||
list := make([]erpProfitRow, 0, len(revenue))
|
||||
var tRev, tCost, tQty float64
|
||||
for pid, rv := range revenue {
|
||||
p := pmap[pid]
|
||||
cst := cost[pid]
|
||||
prof := rv - cst
|
||||
rate := 0.0
|
||||
if rv > 0.005 {
|
||||
rate = round2f(prof / rv * 100)
|
||||
}
|
||||
list = append(list, erpProfitRow{
|
||||
ProductID: pid, ProductCode: p.ProductCode, ProductName: p.ProductName,
|
||||
Spec: p.Spec, Unit: p.BaseUnit, Qty: round4f(qty[pid]),
|
||||
Revenue: round2f(rv), Cost: round2f(cst), Profit: round2f(prof), ProfitRate: rate,
|
||||
})
|
||||
tRev += rv
|
||||
tCost += cst
|
||||
tQty += qty[pid]
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].Profit != list[j].Profit {
|
||||
return list[i].Profit > list[j].Profit
|
||||
}
|
||||
return list[i].ProductID < list[j].ProductID
|
||||
})
|
||||
totalProfit := tRev - tCost
|
||||
rate := 0.0
|
||||
if tRev > 0.005 {
|
||||
rate = round2f(totalProfit / tRev * 100)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"list": list,
|
||||
"summary": map[string]interface{}{
|
||||
"revenue": round2f(tRev), "cost": round2f(tCost),
|
||||
"profit": round2f(totalProfit), "profit_rate": rate, "qty": round4f(tQty),
|
||||
},
|
||||
}, 200, ""
|
||||
}
|
||||
Reference in new issue
Block a user