Files
2026-10-19 01:13:17 +08:00

226 lines
7.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package erp
import (
"fmt"
"strconv"
"strings"
"time"
"server/models"
"github.com/beego/beego/v2/client/orm"
)
// Settings 从租户「ERP基础设置」读取的运行时参数。
type Settings struct {
NegativeAllowed bool // negative_stock_allowed
AutoDocNo bool // auto_generate_doc_no
DefaultWarehouseID uint64 // default_warehouse(存的是名称,按名称匹配)
SalesPriceType string // sales_price_type:sale/wholesale/member
}
func GetSettings(tenantID string) *Settings {
s := &Settings{AutoDocNo: true, SalesPriceType: "sale"}
if v := models.GetErpNormalSettingByKey(tenantID, "negative_stock_allowed"); v != nil {
s.NegativeAllowed = strings.TrimSpace(v.ConfigValue) == "1"
}
if v := models.GetErpNormalSettingByKey(tenantID, "auto_generate_doc_no"); v != nil {
s.AutoDocNo = strings.TrimSpace(v.ConfigValue) != "0"
}
if v := models.GetErpNormalSettingByKey(tenantID, "sales_price_type"); v != nil {
if t := strings.TrimSpace(v.ConfigValue); t != "" {
s.SalesPriceType = t
}
}
if v := models.GetErpNormalSettingByKey(tenantID, "default_warehouse"); v != nil {
name := strings.TrimSpace(v.ConfigValue)
if name != "" {
var wh models.ErpWarehouse
if err := models.Orm.QueryTable(new(models.ErpWarehouse)).
Filter("tenant_id", tenantID).
Filter("warehouse_name", name).
Filter("delete_time__isnull", true).One(&wh); err == nil {
s.DefaultWarehouseID = wh.ID
}
}
}
if s.DefaultWarehouseID == 0 {
var wh models.ErpWarehouse
if err := models.Orm.QueryTable(new(models.ErpWarehouse)).
Filter("tenant_id", tenantID).
Filter("is_default", 1).
Filter("status", 1).
Filter("delete_time__isnull", true).One(&wh); err == nil {
s.DefaultWarehouseID = wh.ID
}
}
return s
}
// erpDocPrefixDefaults 各单据类型的取号前缀默认值(也是基础设置里可改的初值)。
// 必须一型一前缀:共用前缀会让采购入库/其他入库、采购退货/销售退货同号段,
// 单据号无法反推类型,日次序号也会互相穿插。
var erpDocPrefixDefaults = map[string]string{
models.ErpDocPurchaseOrder: "CGDD",
models.ErpDocPurchaseIn: "CGRK",
models.ErpDocPurchaseReturn: "CGTH",
models.ErpDocSalesOrder: "XSDD",
models.ErpDocSalesOut: "XSCK",
models.ErpDocSalesReturn: "XSTH",
models.ErpDocOtherIn: "QTRK",
models.ErpDocOtherOut: "QTCK",
models.ErpDocTransfer: "DB",
models.ErpDocStockTake: "PD",
}
// ErpDocPrefixKey 单据类型 → 基础设置键(document 分组,每型独立)
func ErpDocPrefixKey(docType string) string {
if _, ok := erpDocPrefixDefaults[docType]; !ok {
return ""
}
return "doc_prefix_" + docType
}
// ErpDocPrefixDefaults 供基础设置接口登记默认前缀
func ErpDocPrefixDefaults() map[string]string {
return erpDocPrefixDefaults
}
// ErpDocPrefixField 基础设置「单据设置」里一条可编辑的单据前缀
type ErpDocPrefixField struct {
Key string
Name string
Default string
}
// 表单展示顺序:按采购→销售→其他→调拨/盘点
var erpDocPrefixLabels = []struct {
docType string
name string
}{
{models.ErpDocPurchaseOrder, "采购订单前缀"},
{models.ErpDocPurchaseIn, "采购入库前缀"},
{models.ErpDocPurchaseReturn, "采购退货前缀"},
{models.ErpDocSalesOrder, "销售订单前缀"},
{models.ErpDocSalesOut, "销售出库前缀"},
{models.ErpDocSalesReturn, "销售退货前缀"},
{models.ErpDocOtherIn, "其他入库前缀"},
{models.ErpDocOtherOut, "其他出库前缀"},
{models.ErpDocTransfer, "调拨单前缀"},
{models.ErpDocStockTake, "盘点单前缀"},
}
// ErpDocPrefixFields 返回全部单据前缀配置项,供基础设置接口注册默认值
func ErpDocPrefixFields() []ErpDocPrefixField {
out := make([]ErpDocPrefixField, 0, len(erpDocPrefixLabels))
for _, l := range erpDocPrefixLabels {
out = append(out, ErpDocPrefixField{
Key: ErpDocPrefixKey(l.docType),
Name: l.name,
Default: erpDocPrefixDefaults[l.docType],
})
}
return out
}
// docNoPrefix 按单据类型取号前缀(基础设置 document 分组),缺省用类型缩写。
func docNoPrefix(tenantID, docType string) string {
if key := ErpDocPrefixKey(docType); key != "" {
if v := models.GetErpNormalSettingByKey(tenantID, key); v != nil {
if p := strings.ToUpper(strings.TrimSpace(v.ConfigValue)); p != "" {
return p
}
}
}
if p, ok := erpDocPrefixDefaults[docType]; ok {
return p
}
return "ERP"
}
// GenerateDocNo 生成单据号:前缀+YYYYMMDD+当日3位序号(按租户+类型+日期计数)。
// 依赖 yz_backend_erp_document 的 uk_tenant_docno 唯一索引兜底,调用方需处理重号重试。
func GenerateDocNo(o orm.QueryExecutor, tenantID, docType string) (string, error) {
prefix := docNoPrefix(tenantID, docType)
day := time.Now().Format("20060102")
var count int64
var err error
if o != nil {
count, err = o.QueryTable(new(models.ErpDocument)).
Filter("tenant_id", tenantID).Filter("doc_no__startswith", prefix+day).Count()
} else {
count, err = models.Orm.QueryTable(new(models.ErpDocument)).
Filter("tenant_id", tenantID).Filter("doc_no__startswith", prefix+day).Count()
}
if err != nil && err != orm.ErrNoRows {
return "", err
}
seq := count + 1
if seq > 999 {
return "", fmt.Errorf("当日单据编号已超过999,请先归档")
}
return fmt.Sprintf("%s%s%03d", prefix, day, seq), nil
}
// GenerateFundNo 收付款单号:前缀(SK/FK/QR/ZC)+日期+序号。
func GenerateFundNo(tenantID string, fundType int8) (string, error) {
prefixMap := map[int8]string{
models.ErpFundReceive: "SK", models.ErpFundPay: "FK",
models.ErpFundOtherIn: "QR", models.ErpFundExpense: "FY",
}
p, ok := prefixMap[fundType]
if !ok {
p = "ZJ"
}
day := time.Now().Format("20060102")
count, err := models.Orm.QueryTable(new(models.ErpFundBill)).
Filter("tenant_id", tenantID).Filter("fund_no__startswith", p+day).Count()
if err != nil && err != orm.ErrNoRows {
return "", err
}
return fmt.Sprintf("%s%s%03d", p, day, count+1), nil
}
// GenerateProductCode 商品编号:SP+日期+当日序号(租户内唯一由 uk_tenant_code 兜底)。
func GenerateProductCode(tenantID string) string {
day := time.Now().Format("20060102")
count, _ := models.Orm.QueryTable(new(models.ErpProduct)).
Filter("tenant_id", tenantID).Filter("product_code__startswith", "SP"+day).Count()
return fmt.Sprintf("SP%s%03d", day, count+1)
}
// GetPartnerPaymentDays 取往来单位账期天数(应收到期日计算)。
func GetPartnerPaymentDays(partnerType string, partnerID uint64) int {
if partnerID == 0 {
return 0
}
switch partnerType {
case "customer":
var row models.TenantCrmCustomer
if err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
Filter("id", partnerID).One(&row); err == nil {
return row.PaymentDays
}
case "supplier":
var row models.TenantCrmSupplier
if err := models.Orm.QueryTable(new(models.TenantCrmSupplier)).
Filter("id", partnerID).One(&row); err == nil {
return row.PaymentDays
}
}
return 0
}
// GetCustomerCreditLimit 客户信用额度,0=不限。
func GetCustomerCreditLimit(customerID uint64) float64 {
var row models.TenantCrmCustomer
if err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
Filter("id", customerID).One(&row); err != nil {
return 0
}
return row.CreditLimit
}
// 内部工具
func itoa(v uint64) string { return strconv.FormatUint(v, 10) }