301 lines
9.7 KiB
Go
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, ""
|
|
}
|