301 lines
8.9 KiB
Go
301 lines
8.9 KiB
Go
package erp
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"server/models"
|
|
|
|
"github.com/beego/beego/v2/client/orm"
|
|
)
|
|
|
|
// applyToBill 向一张往来账核销 take 金额(正数)。
|
|
// 正数账(应收/应付)核销使 settled 增加;红字账(负额)反向移动,
|
|
// 统一口径:余额 = amount - settled,核销使 |余额| 趋近 0。
|
|
func applyToBill(o orm.QueryExecutor, bill *models.ErpPartnerBill, take float64) (float64, error) {
|
|
if take <= 0 {
|
|
return 0, nil
|
|
}
|
|
delta := take
|
|
if bill.Amount < 0 {
|
|
delta = -take
|
|
}
|
|
newSettled := round2(bill.SettledAmount + delta)
|
|
newBalance := absf(round2(bill.Amount - newSettled))
|
|
status := int8(1)
|
|
if newBalance < 0.005 {
|
|
status = 2
|
|
}
|
|
_, err := o.QueryTable(new(models.ErpPartnerBill)).Filter("id", bill.ID).
|
|
Update(orm.Params{"settled_amount": newSettled, "status": status, "update_time": time.Now()})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
used := take
|
|
if absf(delta) > absf(take) { // 不超过剩余余额
|
|
used = absf(bill.Amount - bill.SettledAmount)
|
|
}
|
|
return used, nil
|
|
}
|
|
|
|
// autoOffsetBill 新账入账时与反向挂账互抵:
|
|
// 红字退货账优先抵扣同单位未结的正数往来账;正数账优先吸收红字余额(预收预付)。
|
|
// 返回该账本行插入时应携带的已核销额(正数账吸收红字的场景)。
|
|
func autoOffsetBill(o orm.QueryExecutor, bill *models.ErpPartnerBill) (float64, error) {
|
|
if absf(bill.Amount) < 0.005 {
|
|
return 0, nil
|
|
}
|
|
// 查找同单位、同向相反、未结清的账
|
|
var opps []models.ErpPartnerBill
|
|
qs := o.QueryTable(new(models.ErpPartnerBill)).
|
|
Filter("tenant_id", bill.TenantID).
|
|
Filter("bill_type", bill.BillType).
|
|
Filter("partner_id", bill.PartnerID).
|
|
Filter("partner_type", bill.PartnerType).
|
|
Filter("status__lt", 2)
|
|
_, _ = qs.OrderBy("bill_date", "id").All(&opps)
|
|
|
|
if bill.Amount > 0 {
|
|
// 正数账:先吸收红字(负额未结账)
|
|
consumed := 0.0
|
|
for i := range opps {
|
|
b := &opps[i]
|
|
if b.ID == 0 || b.Amount >= 0 {
|
|
continue
|
|
}
|
|
credit := absf(b.Amount - b.SettledAmount)
|
|
if credit < 0.005 {
|
|
continue
|
|
}
|
|
take := credit
|
|
if bill.Amount-consumed < take {
|
|
take = round2(bill.Amount - consumed)
|
|
}
|
|
if _, err := applyToBill(o, b, take); err != nil {
|
|
return 0, err
|
|
}
|
|
consumed += take
|
|
if bill.Amount-consumed < 0.005 {
|
|
break
|
|
}
|
|
}
|
|
return round2(consumed), nil
|
|
}
|
|
|
|
// 红字账(amount<0):插入后抵扣正数账
|
|
var pos []models.ErpPartnerBill
|
|
_, _ = o.QueryTable(new(models.ErpPartnerBill)).
|
|
Filter("tenant_id", bill.TenantID).
|
|
Filter("bill_type", bill.BillType).
|
|
Filter("partner_id", bill.PartnerID).
|
|
Filter("partner_type", bill.PartnerType).
|
|
Filter("status__lt", 2).
|
|
Filter("amount__gt", 0).
|
|
OrderBy("bill_date", "id").All(&pos)
|
|
need := absf(bill.Amount)
|
|
consumed := 0.0
|
|
for i := range pos {
|
|
if need < 0.005 {
|
|
break
|
|
}
|
|
b := &pos[i]
|
|
balance := b.Amount - b.SettledAmount
|
|
if balance < 0.005 {
|
|
continue
|
|
}
|
|
take := balance
|
|
if take > need {
|
|
take = need
|
|
}
|
|
used, err := applyToBill(o, b, take)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
consumed += used
|
|
need -= used
|
|
}
|
|
if consumed > 0 {
|
|
// 红字自身推进 settled(负向)
|
|
newSettled := round2(-consumed)
|
|
status := int8(1)
|
|
if absf(bill.Amount-newSettled) < 0.005 {
|
|
status = 2
|
|
}
|
|
if _, err := o.QueryTable(new(models.ErpPartnerBill)).Filter("id", bill.ID).
|
|
Update(orm.Params{"settled_amount": newSettled, "status": status}); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
// OffsetAfterBillInsert 往来账插入后的互抵处理(正数账已在插入时携带 settled 的不再调用)。
|
|
func OffsetAfterBillInsert(o orm.QueryExecutor, billID uint64) {
|
|
var bill models.ErpPartnerBill
|
|
if err := o.QueryTable(new(models.ErpPartnerBill)).Filter("id", billID).One(&bill); err != nil {
|
|
return
|
|
}
|
|
if bill.Amount < 0 {
|
|
_, _ = autoOffsetBill(o, &bill)
|
|
}
|
|
}
|
|
|
|
// SettleItem 核销明细入参
|
|
type SettleItem struct {
|
|
PartnerBillID uint64 `json:"partner_bill_id"`
|
|
Amount float64 `json:"amount"`
|
|
}
|
|
|
|
// ConfirmFundBill 收付款单确认生效:
|
|
// - fund_type=1/2(收款/付款):按 settles 逐笔核销,未指定则自动 FIFO
|
|
// (到期日→账期日升序,仅正数余额账);核销余量作为预收/预付留在单上。
|
|
// - fund_type=3/4(其他收入/费用):仅登记资金流水。
|
|
// - 同时校验:草稿态、金额>0、核销对象为同租户同往来单位且类型匹配。
|
|
func ConfirmFundBill(fundID uint64, tenantID string, settles []SettleItem, operatorName string) error {
|
|
o := models.Orm
|
|
tx, err := o.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rollback := true
|
|
defer func() {
|
|
if rollback {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
var fund models.ErpFundBill
|
|
if err := tx.QueryTable(new(models.ErpFundBill)).
|
|
Filter("id", fundID).Filter("tenant_id", tenantID).
|
|
Filter("delete_time__isnull", true).ForUpdate().One(&fund); err != nil {
|
|
return fmt.Errorf("收付款单不存在")
|
|
}
|
|
if fund.Status != 0 {
|
|
return fmt.Errorf("当前状态不可确认")
|
|
}
|
|
if fund.Amount <= 0 {
|
|
return fmt.Errorf("金额必须大于0")
|
|
}
|
|
|
|
remaining := round2(fund.Amount)
|
|
if fund.FundType == models.ErpFundReceive || fund.FundType == models.ErpFundPay {
|
|
billType := int8(models.ErpBillReceivable)
|
|
if fund.FundType == models.ErpFundPay {
|
|
billType = models.ErpBillPayable
|
|
}
|
|
if len(settles) == 0 {
|
|
settles, err = autoFifoSettle(tx, &fund, billType, remaining)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, s := range settles {
|
|
if s.Amount <= 0.004 {
|
|
continue
|
|
}
|
|
if s.Amount-remaining > 0.005 {
|
|
return fmt.Errorf("核销总额超出收付款金额")
|
|
}
|
|
var bill models.ErpPartnerBill
|
|
if err := tx.QueryTable(new(models.ErpPartnerBill)).
|
|
Filter("id", s.PartnerBillID).Filter("tenant_id", tenantID).ForUpdate().One(&bill); err != nil {
|
|
return fmt.Errorf("往来账不存在(id=%d)", s.PartnerBillID)
|
|
}
|
|
if bill.BillType != billType {
|
|
return fmt.Errorf("往来账「%s」类型与收付款方式不符", bill.SourceDocNo)
|
|
}
|
|
if fund.PartnerID != 0 && bill.PartnerID != fund.PartnerID {
|
|
return fmt.Errorf("往来账「%s」不属于 %s", bill.SourceDocNo, fund.PartnerName)
|
|
}
|
|
balance := bill.Amount - bill.SettledAmount
|
|
if balance < 0.005 {
|
|
return fmt.Errorf("往来账「%s」已结清", bill.SourceDocNo)
|
|
}
|
|
take := round2(s.Amount)
|
|
if take > balance {
|
|
take = round2(balance)
|
|
}
|
|
if _, err := applyToBill(tx, &bill, take); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Insert(&models.ErpFundSettle{
|
|
TenantID: tenantID, FundBillID: fund.ID, PartnerBillID: bill.ID,
|
|
PartnerBillNo: bill.SourceDocNo, Amount: take, CreateTime: time.Now(),
|
|
}); err != nil {
|
|
return fmt.Errorf("写入核销明细失败: %v", err)
|
|
}
|
|
remaining = round2(remaining - take)
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
_, err = tx.QueryTable(new(models.ErpFundBill)).Filter("id", fund.ID).
|
|
Update(orm.Params{
|
|
"status": 1,
|
|
"settled_amount": round2(fund.Amount - remaining),
|
|
"update_time": now,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("确认失败: %v", err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
rollback = false
|
|
return nil
|
|
}
|
|
|
|
// autoFifoSettle 未指定核销明细时,按到期日/账期日升序自动分摊。
|
|
func autoFifoSettle(tx orm.QueryExecutor, fund *models.ErpFundBill, billType int8, amount float64) ([]SettleItem, error) {
|
|
var bills []models.ErpPartnerBill
|
|
qs := tx.QueryTable(new(models.ErpPartnerBill)).
|
|
Filter("tenant_id", fund.TenantID).
|
|
Filter("bill_type", billType).
|
|
Filter("status__lt", 2).
|
|
Filter("amount__gt", 0)
|
|
if fund.PartnerID > 0 {
|
|
qs = qs.Filter("partner_id", fund.PartnerID)
|
|
}
|
|
_, err := qs.OrderBy("due_date", "bill_date", "id").Limit(200).All(&bills)
|
|
if err != nil && err != orm.ErrNoRows {
|
|
return nil, err
|
|
}
|
|
items := make([]SettleItem, 0, len(bills))
|
|
remaining := amount
|
|
for _, b := range bills {
|
|
if remaining < 0.005 {
|
|
break
|
|
}
|
|
balance := round2(b.Amount - b.SettledAmount)
|
|
if balance < 0.005 {
|
|
continue
|
|
}
|
|
take := balance
|
|
if take > remaining {
|
|
take = round2(remaining)
|
|
}
|
|
items = append(items, SettleItem{PartnerBillID: b.ID, Amount: take})
|
|
remaining = round2(remaining - take)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// VoidFundBill 作废收付款单:仅草稿或确认但未核销任何往来账的单据。
|
|
func VoidFundBill(fundID uint64, tenantID string) error {
|
|
var fund models.ErpFundBill
|
|
if err := models.Orm.QueryTable(new(models.ErpFundBill)).
|
|
Filter("id", fundID).Filter("tenant_id", tenantID).
|
|
Filter("delete_time__isnull", true).One(&fund); err != nil {
|
|
return fmt.Errorf("收付款单不存在")
|
|
}
|
|
if fund.Status == 4 {
|
|
return nil
|
|
}
|
|
if fund.Status == 1 && fund.SettledAmount > 0.005 {
|
|
return fmt.Errorf("已发生核销的收付款不可作废,请做反向收付款冲销")
|
|
}
|
|
_, err := models.Orm.QueryTable(new(models.ErpFundBill)).Filter("id", fundID).
|
|
Update(orm.Params{"status": 4, "update_time": time.Now()})
|
|
return err
|
|
}
|