更新日报相关功能
This commit is contained in:
1 parent
51fd90fc51
commit
68cdaf38fa
23 files changed
+2313
-1056
No files matched your search
@@ -68,33 +68,84 @@ func erpDocAmount(tid string, docTypes []string, from, to time.Time) (float64, i
|
||||
return round2f(sum), int64(len(docs))
|
||||
}
|
||||
|
||||
// erpDashboardTrend 近 6 个月采购/销售趋势
|
||||
// erpDashboardTrend 看板趋势点。Month 保留旧字段(兼容旧页面),Label 为图表 x 轴文案
|
||||
type erpDashboardTrend struct {
|
||||
Month string `json:"month"`
|
||||
Label string `json:"label"`
|
||||
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)
|
||||
// erpDashboardPeriod 解析看板统计周期:week|month|quarter|year,缺省或非法值回落 month
|
||||
func erpDashboardPeriod(raw string) string {
|
||||
switch p := strings.ToLower(strings.TrimSpace(raw)); p {
|
||||
case "week", "quarter", "year":
|
||||
return p
|
||||
default:
|
||||
return "month"
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
// erpPeriodSpan 趋势分桶:Key 用于把单据日期映射到桶,Label 是图表 x 轴文案
|
||||
type erpPeriodSpan struct {
|
||||
Key string
|
||||
Label string
|
||||
}
|
||||
|
||||
// 本月毛利(出库流水成本口径)
|
||||
// erpPeriodRange 返回周期区间 [from, to)、中文标签与趋势分桶。
|
||||
// 周/月按天分桶,季度按 3 个月分桶,年度按 12 个月分桶。
|
||||
func erpPeriodRange(period string, now time.Time) (time.Time, time.Time, string, []erpPeriodSpan) {
|
||||
switch period {
|
||||
case "week":
|
||||
// 周一为一周起点;Go 的 Sunday=0,需要换算成「距本周一的天数」
|
||||
offset := (int(now.Weekday()) + 6) % 7
|
||||
from := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, -offset)
|
||||
names := []string{"周一", "周二", "周三", "周四", "周五", "周六", "周日"}
|
||||
spans := make([]erpPeriodSpan, 0, 7)
|
||||
for i := 0; i < 7; i++ {
|
||||
d := from.AddDate(0, 0, i)
|
||||
spans = append(spans, erpPeriodSpan{Key: d.Format("2006-01-02"), Label: names[i]})
|
||||
}
|
||||
return from, from.AddDate(0, 0, 7), "本周", spans
|
||||
case "quarter":
|
||||
q := (int(now.Month()) - 1) / 3
|
||||
from := time.Date(now.Year(), time.Month(q*3+1), 1, 0, 0, 0, 0, time.Local)
|
||||
spans := make([]erpPeriodSpan, 0, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
m := from.AddDate(0, i, 0)
|
||||
spans = append(spans, erpPeriodSpan{Key: m.Format("2006-01"), Label: fmt.Sprintf("%d月", int(m.Month()))})
|
||||
}
|
||||
return from, from.AddDate(0, 3, 0), "本季度", spans
|
||||
case "year":
|
||||
from := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.Local)
|
||||
spans := make([]erpPeriodSpan, 0, 12)
|
||||
for i := 1; i <= 12; i++ {
|
||||
spans = append(spans, erpPeriodSpan{Key: fmt.Sprintf("%04d-%02d", now.Year(), i), Label: fmt.Sprintf("%d月", i)})
|
||||
}
|
||||
return from, from.AddDate(1, 0, 0), "本年", spans
|
||||
default: // month
|
||||
from := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local)
|
||||
to := from.AddDate(0, 1, 0)
|
||||
spans := make([]erpPeriodSpan, 0, 31)
|
||||
for d := from; d.Before(to); d = d.AddDate(0, 0, 1) {
|
||||
spans = append(spans, erpPeriodSpan{Key: d.Format("2006-01-02"), Label: fmt.Sprintf("%d日", d.Day())})
|
||||
}
|
||||
return from, to, "本月", spans
|
||||
}
|
||||
}
|
||||
|
||||
// erpDashboardStats 区间内购销/毛利统计(只认已生效单据与库存流水)
|
||||
func erpDashboardStats(tid string, from, to time.Time) map[string]interface{} {
|
||||
buyAmt, buyCnt := erpDocAmount(tid, []string{models.ErpDocPurchaseIn}, from, to)
|
||||
saleAmt, saleCnt := erpDocAmount(tid, []string{models.ErpDocSalesOut}, from, to)
|
||||
backAmt, _ := erpDocAmount(tid, []string{models.ErpDocSalesReturn}, from, to)
|
||||
|
||||
// 毛利按出库流水成本口径
|
||||
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).
|
||||
Filter("create_time__gte", from).Filter("create_time__lt", to).
|
||||
All(&ledgers, "direction", "total_cost")
|
||||
var cost float64
|
||||
for _, l := range ledgers {
|
||||
@@ -105,6 +156,82 @@ func erpDashboardCore(a erpActor) map[string]interface{} {
|
||||
}
|
||||
}
|
||||
profit := saleAmt - backAmt - cost
|
||||
rate := 0.0
|
||||
if saleAmt-backAmt > 0.005 {
|
||||
rate = round2f(profit / (saleAmt - backAmt) * 100)
|
||||
}
|
||||
return 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": rate,
|
||||
}
|
||||
}
|
||||
|
||||
// erpDashboardPeriodTrend 当前周期内的购销分布:一次查出单据后按分桶汇总,
|
||||
// 避免「每个点都打一次库」(年度 12 点、月度 31 点)。
|
||||
func erpDashboardPeriodTrend(tid string, from, to time.Time, period string, spans []erpPeriodSpan) []erpDashboardTrend {
|
||||
var docs []models.ErpDocument
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpDocument)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("doc_type__in", []string{models.ErpDocPurchaseIn, models.ErpDocSalesOut}).
|
||||
Filter("status", models.ErpDocStatusApproved).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("biz_date__gte", from).Filter("biz_date__lt", to).
|
||||
All(&docs, "doc_type", "biz_date", "actual_amount")
|
||||
|
||||
index := make(map[string]int, len(spans))
|
||||
for i, s := range spans {
|
||||
index[s.Key] = i
|
||||
}
|
||||
buy := make([]float64, len(spans))
|
||||
sale := make([]float64, len(spans))
|
||||
for _, d := range docs {
|
||||
key := d.BizDate.Format("2006-01-02")
|
||||
if period == "quarter" || period == "year" {
|
||||
key = d.BizDate.Format("2006-01")
|
||||
}
|
||||
i, ok := index[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if d.DocType == models.ErpDocPurchaseIn {
|
||||
buy[i] += d.ActualAmount
|
||||
} else {
|
||||
sale[i] += d.ActualAmount
|
||||
}
|
||||
}
|
||||
list := make([]erpDashboardTrend, 0, len(spans))
|
||||
for i, s := range spans {
|
||||
list = append(list, erpDashboardTrend{
|
||||
Month: s.Key, Label: s.Label,
|
||||
Buy: round2f(buy[i]), Sale: round2f(sale[i]),
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// erpDashboardCore 经营看板:所选周期的购销/毛利、今日销售、库存价值、往来余额、
|
||||
// 待办、近 6 个月趋势、当前周期趋势与预警。period 取 week|month|quarter|year,缺省 month。
|
||||
// month 字段保持「本月」口径不变,供旧页面与小程序继续使用;current 为所选周期。
|
||||
func erpDashboardCore(a erpActor, periodRaw string) map[string]interface{} {
|
||||
tid := a.Tid
|
||||
period := erpDashboardPeriod(periodRaw)
|
||||
now := time.Now()
|
||||
|
||||
monthFrom, monthTo := erpMonthRange(0)
|
||||
monthStats := erpDashboardStats(tid, monthFrom, monthTo)
|
||||
|
||||
periodFrom, periodTo, periodLabel, spans := erpPeriodRange(period, now)
|
||||
current := monthStats
|
||||
if period != "month" {
|
||||
current = erpDashboardStats(tid, periodFrom, periodTo)
|
||||
}
|
||||
|
||||
// 不能用 time.Now().Truncate(24*time.Hour):它按「自零时起算的绝对时长」取整,
|
||||
// 在东八区得到的是 UTC 零点(本地 08:00),会把当天 00:00–08:00 的单据漏掉。
|
||||
todayFrom := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
todaySale, todayCnt := erpDocAmount(tid, []string{models.ErpDocSalesOut}, todayFrom, todayFrom.AddDate(0, 0, 1))
|
||||
|
||||
var stocks []models.ErpStock
|
||||
_, _ = models.Orm.QueryTable(new(models.ErpStock)).Filter("tenant_id", tid).
|
||||
@@ -142,30 +269,29 @@ func erpDashboardCore(a erpActor) map[string]interface{} {
|
||||
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})
|
||||
month := f.Format("2006-01")
|
||||
trendList = append(trendList, erpDashboardTrend{Month: month, Label: month, 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
|
||||
}(),
|
||||
"period": period,
|
||||
"range": map[string]interface{}{
|
||||
"from": periodFrom.Format("2006-01-02"),
|
||||
"to": periodTo.AddDate(0, 0, -1).Format("2006-01-02"),
|
||||
"label": periodLabel,
|
||||
},
|
||||
"current": current,
|
||||
// 兼容旧调用方:month 始终是本月口径
|
||||
"month": monthStats,
|
||||
"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),
|
||||
"todo": map[string]interface{}{"approval_pending": pendingTasks, "draft_docs": draftCnt},
|
||||
"trend": trendList,
|
||||
"period_trend": erpDashboardPeriodTrend(tid, periodFrom, periodTo, period, spans),
|
||||
"warning": erpWarningCounts(tid),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user