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

301 lines
9.7 KiB
Go

package controllers
import (
"strings"
"server/models"
"github.com/beego/beego/v2/client/orm"
)
// ERP 档案读取核心(两端共用)。
//
// 商品/分类/仓库/客商在 /backend/erp/** 与 /api/erp/** 各有独立 handler 与路由(接口隔离规范),
// 但「按租户过滤 + 关键字命中字段 + 列表补库存与状态」的口径只能有一份实现,放在这里。
// 返回值约定与 erp_*_core.go 一致:最后一个 string 为空表示成功,非空时配套 http 状态码。
// ---- 商品 ----
type erpProductFilter struct {
Keyword string
CategoryID uint64
Brand string
Status string
IsDraft string
OnlyEnabled bool
}
// erpProductListCore 商品列表:出参已补分类名、全仓库存合计与预警状态。
func erpProductListCore(a erpActor, f erpProductFilter, page, pageSize int) ([]models.ErpProduct, int64, int, string) {
qs := models.Orm.QueryTable(new(models.ErpProduct)).
Filter("tenant_id", a.Tid).Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(f.Keyword); kw != "" {
cond := orm.NewCondition().
Or("product_name__contains", kw).
Or("product_code__contains", kw).
Or("barcode__contains", kw).
Or("spec__contains", kw).
Or("brand__contains", kw)
qs = qs.SetCond(qs.GetCond().AndCond(cond))
}
if f.CategoryID > 0 {
qs = qs.Filter("category_id", f.CategoryID)
}
if b := strings.TrimSpace(f.Brand); b != "" {
qs = qs.Filter("brand", b)
}
if st := strings.TrimSpace(f.Status); st != "" {
qs = qs.Filter("status", st)
}
if d := strings.TrimSpace(f.IsDraft); d != "" {
qs = qs.Filter("is_draft", d)
}
if f.OnlyEnabled {
qs = qs.Filter("status", 1).Filter("is_draft", 0)
}
total, _ := qs.Count()
var list []models.ErpProduct
if _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&list); err != nil {
return nil, 0, 500, "查询失败: " + err.Error()
}
cats := erpCategoryMap(a.Tid)
ids := make([]uint64, 0, len(list))
for _, p := range list {
ids = append(ids, p.ID)
}
agg := erpStockAggregate(a.Tid, ids)
for i := range list {
list[i].CategoryName = cats[list[i].CategoryID]
list[i].TotalStock = agg[list[i].ID].qty
list[i].SafetyState = erpSafetyState(list[i].SafetyQty, list[i].TotalStock, list[i].MaxQty)
}
return list, total, 200, ""
}
// erpProductDetailCore 商品详情:含分类名与分仓/分批次结存。
func erpProductDetailCore(a erpActor, id uint64) (*models.ErpProduct, int, string) {
if id == 0 {
return nil, 400, "无效的ID"
}
var p models.ErpProduct
if err := models.Orm.QueryTable(new(models.ErpProduct)).
Filter("id", id).Filter("tenant_id", a.Tid).
Filter("delete_time__isnull", true).One(&p); err != nil {
return nil, 404, "商品未找到"
}
p.CategoryName = erpCategoryMap(a.Tid)[p.CategoryID]
p.Stocks = erpProductStocks(a.Tid, id)
return &p, 200, ""
}
// ---- 商品分类 ----
// erpCategoryListCore 分类列表 + 树,并统计每个分类下的商品数。
func erpCategoryListCore(a erpActor) ([]map[string]interface{}, []map[string]interface{}, int, string) {
var list []models.ErpProductCategory
_, _ = models.Orm.QueryTable(new(models.ErpProductCategory)).
Filter("tenant_id", a.Tid).Filter("delete_time__isnull", true).
OrderBy("sort_order", "id").All(&list)
counts := map[uint64]int64{}
var ps []models.ErpProduct
_, _ = models.Orm.QueryTable(new(models.ErpProduct)).
Filter("tenant_id", a.Tid).Filter("delete_time__isnull", true).
All(&ps, "id", "category_id")
for _, p := range ps {
counts[p.CategoryID]++
}
rows := make([]map[string]interface{}, 0, len(list))
for _, r := range list {
rows = append(rows, map[string]interface{}{
"id": r.ID, "category_name": r.CategoryName, "parent_id": r.ParentID,
"sort_order": r.SortOrder, "status": r.Status, "remark": r.Remark,
"product_count": counts[r.ID],
})
}
return rows, erpCategoryTree(rows, 0), 200, ""
}
// ---- 仓库 ----
// erpWarehouseListCore 仓库列表:每仓在库品种数与货值,列表直接反映仓库占用情况。
func erpWarehouseListCore(a erpActor, keyword, status string, onlyEnabled bool) ([]map[string]interface{}, int, string) {
qs := models.Orm.QueryTable(new(models.ErpWarehouse)).
Filter("tenant_id", a.Tid).Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(keyword); kw != "" {
qs = qs.Filter("warehouse_name__contains", kw)
}
if st := strings.TrimSpace(status); st != "" {
qs = qs.Filter("status", st)
}
if onlyEnabled {
qs = qs.Filter("status", 1)
}
var list []models.ErpWarehouse
if _, err := qs.OrderBy("-is_default", "id").All(&list); err != nil {
return nil, 500, "查询失败: " + err.Error()
}
type agg struct {
kinds int
value float64
}
amap := map[uint64]*agg{}
var stocks []models.ErpStock
_, _ = models.Orm.QueryTable(new(models.ErpStock)).
Filter("tenant_id", a.Tid).All(&stocks, "warehouse_id", "quantity", "cost_price")
for _, s := range stocks {
w := amap[s.WarehouseID]
if w == nil {
w = &agg{}
amap[s.WarehouseID] = w
}
if s.Quantity > 0.000001 {
w.kinds++
}
w.value += s.Quantity * s.CostPrice
}
rows := make([]map[string]interface{}, 0, len(list))
for _, wh := range list {
v := amap[wh.ID]
if v == nil {
v = &agg{}
}
rows = append(rows, map[string]interface{}{
"warehouse": wh,
"stock_kinds": v.kinds,
"stock_value": round2f(v.value),
})
}
return rows, 200, ""
}
// ---- 客商(供应商/客户统一出参)----
// erpPartnerRow 移动端开单/对账用的客商精简行:供应商与客户共用一套字段,
// 前端按 partner_type 决定「应收/应付」口径,不必分别处理两张表。
type erpPartnerRow struct {
PartnerID uint64 `json:"partner_id"`
PartnerType string `json:"partner_type"`
PartnerName string `json:"partner_name"`
ContactPerson string `json:"contact_person"`
ContactPhone string `json:"contact_phone"`
Level string `json:"level"`
PaymentDays int `json:"payment_days"`
CreditLimit float64 `json:"credit_limit"`
OpenAmount float64 `json:"open_amount"`
}
// erpPartnerListCore 客商列表(partner_type=supplier|customer),附带未结往来余额。
func erpPartnerListCore(a erpActor, partnerType, keyword string, onlyEnabled bool, page, pageSize int) ([]erpPartnerRow, int64, int, string) {
nameField := "customer_name"
var qTable interface{} = new(models.TenantCrmCustomer)
if partnerType == "supplier" {
nameField = "supplier_name"
qTable = new(models.TenantCrmSupplier)
} else {
partnerType = "customer"
}
qs := models.Orm.QueryTable(qTable).
Filter("tenant_id", a.Tid).Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(keyword); kw != "" {
qs = qs.Filter(nameField+"__contains", kw)
}
if onlyEnabled {
qs = qs.Filter("status", "1").Filter("is_draft", 0)
}
if partnerType == "customer" {
// 公海客户没有归属,不参与开单
qs = qs.Filter("in_pool", 0)
}
total, _ := qs.Count()
billType := int8(models.ErpBillReceivable)
if partnerType == "supplier" {
billType = models.ErpBillPayable
}
type brief struct {
id uint64
name string
contact string
phone string
level string
payDays int
credit float64
}
var briefs []brief
if partnerType == "supplier" {
var list []models.TenantCrmSupplier
if _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).
All(&list, "id", "supplier_name", "contact_person", "contact_phone", "supplier_level", "payment_days"); err != nil {
return nil, 0, 500, "查询失败: " + err.Error()
}
for _, s := range list {
briefs = append(briefs, brief{s.ID, s.SupplierName, s.ContactPerson, s.ContactPhone, s.SupplierLevel, s.PaymentDays, 0})
}
} else {
var list []models.TenantCrmCustomer
if _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).
All(&list, "id", "customer_name", "contact_person", "contact_phone", "customer_level", "payment_days", "credit_limit"); err != nil {
return nil, 0, 500, "查询失败: " + err.Error()
}
for _, c := range list {
briefs = append(briefs, brief{c.ID, c.CustomerName, c.ContactPerson, c.ContactPhone, c.CustomerLevel, c.PaymentDays, c.CreditLimit})
}
}
ids := make([]uint64, 0, len(briefs))
for _, b := range briefs {
ids = append(ids, b.id)
}
open := map[uint64]float64{}
if len(ids) > 0 {
var bills []models.ErpPartnerBill
_, _ = models.Orm.QueryTable(new(models.ErpPartnerBill)).
Filter("tenant_id", a.Tid).Filter("bill_type", billType).
Filter("status__lt", 2).Filter("partner_id__in", ids).
All(&bills, "partner_id", "amount", "settled_amount")
for _, b := range bills {
open[b.PartnerID] += b.Amount - b.SettledAmount
}
}
out := make([]erpPartnerRow, 0, len(briefs))
for _, b := range briefs {
out = append(out, erpPartnerRow{
PartnerID: b.id, PartnerType: partnerType, PartnerName: strings.TrimSpace(b.name),
ContactPerson: b.contact, ContactPhone: b.phone, Level: b.level,
PaymentDays: b.payDays, CreditLimit: b.credit,
OpenAmount: round2f(open[b.id]),
})
}
return out, total, 200, ""
}
// erpPartnerDetailCore 客商详情(按 partner_type 回原始档案,字段与 Web 端一致)。
func erpPartnerDetailCore(a erpActor, partnerType string, id uint64) (interface{}, int, string) {
if id == 0 {
return nil, 400, "无效的ID"
}
if partnerType == "supplier" {
var s models.TenantCrmSupplier
if err := models.Orm.QueryTable(new(models.TenantCrmSupplier)).
Filter("id", id).Filter("tenant_id", a.Tid).
Filter("delete_time__isnull", true).One(&s); err != nil {
return nil, 404, "供应商未找到"
}
return s, 200, ""
}
var c models.TenantCrmCustomer
if err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
Filter("id", id).Filter("tenant_id", a.Tid).
Filter("delete_time__isnull", true).One(&c); err != nil {
return nil, 404, "客户未找到"
}
return c, 200, ""
}