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 看板趋势点。Month 保留旧字段(兼容旧页面),Label 为图表 x 轴文案 type erpDashboardTrend struct { Month string `json:"month"` Label string `json:"label"` Buy float64 `json:"buy"` Sale float64 `json:"sale"` } // 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" } } // 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", from).Filter("create_time__lt", to). 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 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). 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) month := f.Format("2006-01") trendList = append(trendList, erpDashboardTrend{Month: month, Label: month, Buy: b, Sale: s}) } return map[string]interface{}{ "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, "period_trend": erpDashboardPeriodTrend(tid, periodFrom, periodTo, period, spans), "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, "" }