Files
yunzerwebsiteallinone/go/controllers/erp_finance_core.go
T
2026-10-19 01:13:17 +08:00

530 lines
17 KiB
Go

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, ""
}