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) }