更新日报相关功能

This commit is contained in:
hero920103 committed 2026-10-22 17:55:50 +08:00
1 parent 51fd90fc51
commit 68cdaf38fa
23 files changed
+2313 -1056

No files matched your search

+156 -30
View File
@@ -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),
}
}