package controllers import ( "fmt" "sort" "strings" "time" "server/models" erp "server/services/erp" "github.com/beego/beego/v2/client/orm" ) // ERP 往来账/收付款核心(两端共用):/backend/erp/finance* 与 /api/erp/finance* 都调这里, // 未收未付、账龄、核销余额等口径只算一次,手机端不会看到与 Web 端不一致的应收应付。 // erpBillFilter 往来账查询条件 type erpBillFilter struct { BillType string PartnerID uint64 PartnerType string SourceDocID uint64 SettleState string // open|part|done Keyword string DateFrom string DateTo string } // erpBillListCore 往来账列表 + 未结余额合计 func erpBillListCore(a erpActor, f erpBillFilter, page, pageSize int) ([]map[string]interface{}, int64, map[string]float64, int, string) { qs := models.Orm.QueryTable(new(models.ErpPartnerBill)).Filter("tenant_id", a.Tid) if f.BillType != "" { qs = qs.Filter("bill_type", f.BillType) } if f.PartnerID > 0 { qs = qs.Filter("partner_id", f.PartnerID) } if f.PartnerType != "" { qs = qs.Filter("partner_type", f.PartnerType) } if f.SourceDocID > 0 { qs = qs.Filter("source_doc_id", f.SourceDocID) } switch f.SettleState { case "open": qs = qs.Filter("status__lt", 2) case "part": qs = qs.Filter("status", 1) case "done": qs = qs.Filter("status", 2) } if f.Keyword != "" { cond := orm.NewCondition(). Or("partner_name__contains", f.Keyword). Or("source_doc_no__contains", f.Keyword) qs = qs.SetCond(qs.GetCond().AndCond(cond)) } for _, d := range []struct { raw string cond string }{{f.DateFrom, "bill_date__gte"}, {f.DateTo, "bill_date__lte"}} { if strings.TrimSpace(d.raw) == "" { continue } if t, e := time.ParseInLocation("2006-01-02", strings.TrimSpace(d.raw), time.Local); e == nil { qs = qs.Filter(d.cond, t) } } total, _ := qs.Count() var rows []models.ErpPartnerBill if _, err := qs.OrderBy("-bill_date", "-id").Limit(pageSize, (page-1)*pageSize).All(&rows); err != nil { return nil, 0, nil, 500, "查询失败: " + err.Error() } // 摘要区合计:未收/未付总额与本页余额 var openRecv, openPay float64 var allRows []models.ErpPartnerBill _, _ = models.Orm.QueryTable(new(models.ErpPartnerBill)). Filter("tenant_id", a.Tid).Filter("status__lt", 2). All(&allRows, "bill_type", "amount", "settled_amount") for _, b := range allRows { bal := b.Amount - b.SettledAmount if b.BillType == models.ErpBillReceivable { openRecv += bal } else { openPay += bal } } out := make([]map[string]interface{}, 0, len(rows)) now := time.Now() for _, b := range rows { bal := round2f(b.Amount - b.SettledAmount) overdue := 0 if b.DueDate != nil && b.Status < 2 { d := int(b.DueDate.Sub(now).Hours() / 24) if d < 0 { overdue = -d } } out = append(out, map[string]interface{}{ "bill": b, "balance": bal, "overdue_days": overdue, "status_label": erpBillStatusLabel(b.Status), }) } return out, total, map[string]float64{ "open_receivable": round2f(openRecv), "open_payable": round2f(openPay), }, 200, "" } // erpBillDetailCore 往来账详情(含核销记录与原单据) func erpBillDetailCore(a erpActor, id uint64) (map[string]interface{}, int, string) { if id == 0 { return nil, 400, "无效的ID" } var bill models.ErpPartnerBill if err := models.Orm.QueryTable(new(models.ErpPartnerBill)). Filter("id", id).Filter("tenant_id", a.Tid).One(&bill); err != nil { return nil, 404, "往来账未找到" } var settles []models.ErpFundSettle _, _ = models.Orm.QueryTable(new(models.ErpFundSettle)). Filter("tenant_id", a.Tid).Filter("partner_bill_id", id).OrderBy("-id").All(&settles) fundIDs := make([]uint64, 0, len(settles)) for _, s := range settles { fundIDs = append(fundIDs, s.FundBillID) } fundMap := map[uint64]models.ErpFundBill{} if len(fundIDs) > 0 { var funds []models.ErpFundBill _, _ = models.Orm.QueryTable(new(models.ErpFundBill)). Filter("id__in", fundIDs).All(&funds, "id", "fund_no", "fund_type", "biz_date", "pay_method") for _, fn := range funds { fundMap[fn.ID] = fn } } items := erpDocItems(models.Orm, bill.SourceDocID) return map[string]interface{}{ "bill": bill, "balance": round2f(bill.Amount - bill.SettledAmount), "settles": settles, "funds": fundMap, "doc_items": items, }, 200, "" } // erpPartnerAging 单个往来单位的账龄分布 type erpPartnerAging struct { PartnerID uint64 `json:"partner_id"` PartnerName string `json:"partner_name"` BillType int8 `json:"bill_type"` Total float64 `json:"total"` Buckets map[string]float64 `json:"buckets"` } // erpAgingCore 账龄分析(按未结余额分桶) func erpAgingCore(a erpActor) (map[string]interface{}, int, string) { buckets := []struct { Label string `json:"label"` Min int `json:"min"` Max int `json:"max"` }{ {"未到期", -99999, 0}, {"1-30天", 1, 30}, {"31-60天", 31, 60}, {"61-90天", 61, 90}, {"90天以上", 91, 99999}, } agg := map[string]*erpPartnerAging{} var bills []models.ErpPartnerBill _, _ = models.Orm.QueryTable(new(models.ErpPartnerBill)). Filter("tenant_id", a.Tid).Filter("status__lt", 2). All(&bills, "partner_id", "partner_name", "bill_type", "amount", "settled_amount", "due_date", "bill_date") now := time.Now() sum := map[string]map[string]float64{} for _, b := range bills { // 分组必须带上 bill_type:同一客商既挂应收又挂应付时,只按 partner 归并 // 会把两种账期混进行里,导致 list 合计与 summary(按单据类型分)对不上。 key := b.PartnerType + ":" + fmt.Sprintf("%d:%d", b.PartnerID, b.BillType) pa := agg[key] if pa == nil { pa = &erpPartnerAging{ PartnerID: b.PartnerID, PartnerName: b.PartnerName, BillType: b.BillType, Buckets: map[string]float64{}, } agg[key] = pa } ref := b.DueDate if ref == nil { ref = &b.BillDate } days := int(now.Sub(*ref).Hours() / 24) label := buckets[0].Label for _, bk := range buckets { if days >= bk.Min && days <= bk.Max { label = bk.Label break } } bal := round2f(b.Amount - b.SettledAmount) pa.Total = round2f(pa.Total + bal) pa.Buckets[label] = round2f(pa.Buckets[label] + bal) bt := "receivable" if b.BillType == models.ErpBillPayable { bt = "payable" } if sum[bt] == nil { sum[bt] = map[string]float64{} } sum[bt][label] = round2f(sum[bt][label] + bal) } list := make([]*erpPartnerAging, 0, len(agg)) for _, pa := range agg { list = append(list, pa) } return map[string]interface{}{"list": list, "summary": sum, "buckets": buckets}, 200, "" } // erpFundFilter 收付款单查询条件 type erpFundFilter struct { FundType string Status string PartnerID uint64 Keyword string DateFrom string DateTo string } // erpFundListCore 收付款单列表 func erpFundListCore(a erpActor, f erpFundFilter, page, pageSize int) ([]models.ErpFundBill, int64, int, string) { qs := models.Orm.QueryTable(new(models.ErpFundBill)). Filter("tenant_id", a.Tid).Filter("delete_time__isnull", true) if f.FundType != "" { qs = qs.Filter("fund_type", f.FundType) } if f.Status != "" { qs = qs.Filter("status", f.Status) } if f.PartnerID > 0 { qs = qs.Filter("partner_id", f.PartnerID) } if f.Keyword != "" { cond := orm.NewCondition(). Or("fund_no__contains", f.Keyword). Or("partner_name__contains", f.Keyword) qs = qs.SetCond(qs.GetCond().AndCond(cond)) } for _, d := range []struct { raw string cond string }{{f.DateFrom, "biz_date__gte"}, {f.DateTo, "biz_date__lte"}} { if strings.TrimSpace(d.raw) == "" { continue } if t, e := time.ParseInLocation("2006-01-02", strings.TrimSpace(d.raw), time.Local); e == nil { qs = qs.Filter(d.cond, t) } } total, _ := qs.Count() var rows []models.ErpFundBill if _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows); err != nil { return nil, 0, 500, "查询失败: " + err.Error() } return rows, total, 200, "" } // erpFundDetailCore 收付款单详情(含核销明细与对应往来账) func erpFundDetailCore(a erpActor, id uint64) (map[string]interface{}, int, string) { if id == 0 { return nil, 400, "无效的ID" } var fund models.ErpFundBill if err := models.Orm.QueryTable(new(models.ErpFundBill)). Filter("id", id).Filter("tenant_id", a.Tid). Filter("delete_time__isnull", true).One(&fund); err != nil { return nil, 404, "收付款单未找到" } var settles []models.ErpFundSettle _, _ = models.Orm.QueryTable(new(models.ErpFundSettle)). Filter("fund_bill_id", id).OrderBy("id").All(&settles) billIDs := make([]uint64, 0, len(settles)) for _, s := range settles { billIDs = append(billIDs, s.PartnerBillID) } bills := []models.ErpPartnerBill{} if len(billIDs) > 0 { _, _ = models.Orm.QueryTable(new(models.ErpPartnerBill)). Filter("id__in", billIDs).All(&bills) } fund.Settles = settles return map[string]interface{}{"fund": fund, "bills": bills}, 200, "" } // erpFundSaveCore 新建/修改收付款草稿(草稿态才可改,确认后只能作废重开) func erpFundSaveCore(a erpActor, p erpFundPayload) (uint64, string, int, string) { tid := a.Tid if p.FundType < models.ErpFundReceive || p.FundType > models.ErpFundExpense { return 0, "", 400, "收付款方式无效" } if p.Amount <= 0 { return 0, "", 400, "金额必须大于0" } if (p.FundType == models.ErpFundReceive || p.FundType == models.ErpFundPay) && p.PartnerID == 0 { return 0, "", 400, "收款/付款必须选择往来单位" } partnerType := "" if p.FundType == models.ErpFundReceive { partnerType = "customer" } else if p.FundType == models.ErpFundPay { partnerType = "supplier" } name := erpPartnerName(partnerType, p.PartnerID) if p.PartnerID > 0 && name == "" { return 0, "", 400, erpPartnerLabel(partnerType) + "不存在" } bizDate := time.Now() if p.BizDate != "" { if t, e := time.ParseInLocation("2006-01-02", p.BizDate, time.Local); e == nil { bizDate = t } } method := strings.TrimSpace(p.PayMethod) if method == "" { method = "cash" } now := time.Now() if p.ID > 0 { var fund models.ErpFundBill if err := models.Orm.QueryTable(new(models.ErpFundBill)). Filter("id", p.ID).Filter("tenant_id", tid). Filter("delete_time__isnull", true).One(&fund); err != nil { return 0, "", 404, "收付款单未找到" } if fund.Status != 0 { return 0, "", 400, "已确认的收付款单不可修改" } if _, err := models.Orm.QueryTable(new(models.ErpFundBill)).Filter("id", p.ID). Update(orm.Params{ "fund_type": p.FundType, "partner_type": partnerType, "partner_id": p.PartnerID, "partner_name": name, "amount": p.Amount, "pay_method": method, "pay_account": p.PayAccount, "biz_date": bizDate, "remark": p.Remark, "update_time": now, }); err != nil { return 0, "", 500, "保存失败: " + err.Error() } return p.ID, fund.FundNo, 200, "" } no, e := erp.GenerateFundNo(tid, p.FundType) if e != nil { return 0, "", 500, e.Error() } fund := models.ErpFundBill{ TenantID: tid, FundNo: no, FundType: p.FundType, PartnerType: partnerType, PartnerID: p.PartnerID, PartnerName: name, Amount: p.Amount, PayMethod: method, PayAccount: p.PayAccount, BizDate: bizDate, Status: 0, Remark: p.Remark, CreateUserID: fmt.Sprintf("%d", a.UID), CreateUserName: a.Name, CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&fund) if err != nil { return 0, "", 500, "创建失败: " + err.Error() } return uint64(id), no, 200, "" } // erpFundConfirmCore 确认收付款并核销;settles 为空时按到期日自动 FIFO func erpFundConfirmCore(a erpActor, id uint64, settles []erp.SettleItem) (int, string) { if id == 0 { return 400, "无效的ID" } if err := erp.ConfirmFundBill(id, a.Tid, settles, a.Name); err != nil { return 400, err.Error() } return 200, "" } // erpFundVoidCore 作废收付款单(已核销的往来账余额同步回滚) func erpFundVoidCore(a erpActor, id uint64) (int, string) { if id == 0 { return 400, "无效的ID" } if err := erp.VoidFundBill(id, a.Tid); err != nil { return 400, err.Error() } return 200, "" } // erpFundDeleteCore 删除收付款单,仅草稿可删 func erpFundDeleteCore(a erpActor, id uint64) (int, string) { if id == 0 { return 400, "无效的ID" } var fund models.ErpFundBill if err := models.Orm.QueryTable(new(models.ErpFundBill)). Filter("id", id).Filter("tenant_id", a.Tid). Filter("delete_time__isnull", true).One(&fund); err != nil { return 404, "收付款单未找到" } if fund.Status != 0 { return 400, "只有草稿状态的收付款单可以删除" } now := time.Now() if _, err := models.Orm.QueryTable(new(models.ErpFundBill)).Filter("id", id). Update(orm.Params{"delete_time": now, "update_time": now}); err != nil { return 500, "删除失败: " + err.Error() } return 200, "" } // erpFundOpenBillsCore 收款/付款时可核销的往来账(前端据此勾选) func erpFundOpenBillsCore(a erpActor, fundType int8, partnerID uint64) ([]map[string]interface{}, int, string) { billType := int8(models.ErpBillReceivable) if fundType == 2 { billType = models.ErpBillPayable } qs := models.Orm.QueryTable(new(models.ErpPartnerBill)). Filter("tenant_id", a.Tid).Filter("bill_type", billType). Filter("status__lt", 2).Filter("amount__gt", 0) if partnerID > 0 { qs = qs.Filter("partner_id", partnerID) } var rows []models.ErpPartnerBill _, _ = qs.OrderBy("due_date", "bill_date", "id").Limit(200).All(&rows) out := make([]map[string]interface{}, 0, len(rows)) for _, b := range rows { out = append(out, map[string]interface{}{ "id": b.ID, "source_doc_no": b.SourceDocNo, "bill_date": b.BillDate, "due_date": b.DueDate, "amount": b.Amount, "settled_amount": b.SettledAmount, "balance": round2f(b.Amount - b.SettledAmount), "partner_name": b.PartnerName, }) } return out, 200, "" } // erpCashDayRow 按日汇总的资金流水 type erpCashDayRow struct { Date string `json:"date"` In float64 `json:"in"` Out float64 `json:"out"` Net float64 `json:"net"` } // erpCashFlowCore 资金流水(只统计实际收付,不含单据生成的往来账) func erpCashFlowCore(a erpActor, from, to time.Time) (map[string]interface{}, int, string) { var rows []models.ErpFundBill _, _ = models.Orm.QueryTable(new(models.ErpFundBill)). Filter("tenant_id", a.Tid).Filter("status", 1). Filter("biz_date__gte", from).Filter("biz_date__lte", to). OrderBy("biz_date", "id").All(&rows) days := map[string]*erpCashDayRow{} var totalIn, totalOut float64 for _, f := range rows { d := f.BizDate.Format("2006-01-02") if days[d] == nil { days[d] = &erpCashDayRow{Date: d} } if f.FundType == models.ErpFundReceive || f.FundType == models.ErpFundOtherIn { days[d].In = round2f(days[d].In + f.Amount) totalIn += f.Amount } else { days[d].Out = round2f(days[d].Out + f.Amount) totalOut += f.Amount } } list := make([]*erpCashDayRow, 0, len(days)) keys := make([]string, 0, len(days)) for k := range days { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { r := days[k] r.Net = round2f(r.In - r.Out) list = append(list, r) } return map[string]interface{}{ "list": list, "total_in": round2f(totalIn), "total_out": round2f(totalOut), "net": round2f(totalIn - totalOut), }, 200, "" } // erpPartnerSumRow 往来单位对账汇总行 type erpPartnerSumRow struct { PartnerID uint64 `json:"partner_id"` PartnerType string `json:"partner_type"` PartnerName string `json:"partner_name"` RecvTotal float64 `json:"recv_total"` RecvOpen float64 `json:"recv_open"` PayTotal float64 `json:"pay_total"` PayOpen float64 `json:"pay_open"` LastBillDate string `json:"last_bill_date"` } // erpPartnerSummaryCore 往来单位对账汇总(partnerType 为空=不限) func erpPartnerSummaryCore(a erpActor, partnerType string) (map[string]interface{}, int, string) { var bills []models.ErpPartnerBill _, _ = models.Orm.QueryTable(new(models.ErpPartnerBill)). Filter("tenant_id", a.Tid).All(&bills) m := map[string]*erpPartnerSumRow{} for _, b := range bills { k := b.PartnerType + ":" + fmt.Sprintf("%d", b.PartnerID) r := m[k] if r == nil { r = &erpPartnerSumRow{PartnerID: b.PartnerID, PartnerType: b.PartnerType, PartnerName: b.PartnerName} m[k] = r } if b.BillDate.Format("2006-01-02") > r.LastBillDate { r.LastBillDate = b.BillDate.Format("2006-01-02") } if b.BillType == models.ErpBillReceivable { r.RecvTotal = round2f(r.RecvTotal + b.Amount) if b.Status < 2 { r.RecvOpen = round2f(r.RecvOpen + b.Amount - b.SettledAmount) } } else { r.PayTotal = round2f(r.PayTotal + b.Amount) if b.Status < 2 { r.PayOpen = round2f(r.PayOpen + b.Amount - b.SettledAmount) } } } list := make([]*erpPartnerSumRow, 0, len(m)) for _, r := range m { if partnerType != "" && r.PartnerType != partnerType { continue } list = append(list, r) } return map[string]interface{}{"list": list}, 200, "" }