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

653 lines
22 KiB
Go
Raw 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 controllers
import (
"fmt"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
approvalEngine "server/services/approval"
erp "server/services/erp"
"github.com/beego/beego/v2/client/orm"
)
// ERP 单据核心逻辑(两端共用)。
//
// 按项目的接口隔离约定,/backend/erp/document* 与 /api/erp/document* 必须各有独立的
// handler 与路由,但单据的校验、明细装配、落库事务与审批提交口径只能有一份实现:
// 这里就是那一份。两端 handler 只负责「解析自己的令牌 → 组装 erpActor → 调核心 →
// 按各自的响应信封输出」,业务规则不在两端各写一遍,避免移动端口径慢慢跑偏。
//
// 返回约定沿用 reimburse*Core:最后一个 string 为空即成功,非空时给出处置状态码与文案,
// 由调用方原样透出,保证抽取前后接口行为完全一致。
// erpActor ERP 操作人:由各自端的 JWT 解析而来,核心层只认这个结构,不认 beego 上下文
type erpActor struct {
Tid string // 租户ID字符串口径(ERP 表的 tenant_id 是 VARCHAR)
TidNum uint64
UID uint64
Name string
}
func erpNewActor(claims *jwtutil.Claims) erpActor {
return erpActor{
Tid: fmt.Sprintf("%d", claims.TenantId),
TidNum: uint64(claims.TenantId),
UID: uint64(claims.UserID),
Name: resolveUserName(claims),
}
}
// erpDocFilter 单据列表筛选条件(与 Web 端查询参数一一对应)
type erpDocFilter struct {
DocType string
DocTypes string // 逗号分隔的多类型
Status string
Keyword string
PartnerID uint64
WarehouseID uint64
SourceDocID uint64
DateFrom string
DateTo string
}
// erpDocListCore 单据分页查询
func erpDocListCore(a erpActor, f erpDocFilter, page, pageSize int) ([]models.ErpDocument, int64, int, string) {
if f.DocType != "" && !erpDocTypes[f.DocType] {
return nil, 0, 400, "未知的单据类型"
}
qs := models.Orm.QueryTable(new(models.ErpDocument)).
Filter("tenant_id", a.Tid).Filter("delete_time__isnull", true)
if f.DocType != "" {
qs = qs.Filter("doc_type", f.DocType)
}
if types := strings.TrimSpace(f.DocTypes); types != "" {
list := make([]string, 0, 4)
for _, t := range strings.Split(types, ",") {
if erpDocTypes[strings.TrimSpace(t)] {
list = append(list, strings.TrimSpace(t))
}
}
if len(list) > 0 {
qs = qs.Filter("doc_type__in", list)
}
}
if st := strings.TrimSpace(f.Status); st != "" {
qs = qs.Filter("status", st)
}
if kw := strings.TrimSpace(f.Keyword); kw != "" {
cond := orm.NewCondition().
Or("doc_no__contains", kw).
Or("partner_name__contains", kw).
Or("remark__contains", kw)
qs = qs.SetCond(qs.GetCond().AndCond(cond))
}
if f.PartnerID > 0 {
qs = qs.Filter("partner_id", f.PartnerID)
}
if f.WarehouseID > 0 {
qs = qs.Filter("warehouse_id", f.WarehouseID)
}
if f.SourceDocID > 0 {
qs = qs.Filter("source_doc_id", f.SourceDocID)
}
for _, d := range []struct {
raw string
cond string
}{{f.DateFrom, "biz_date__gte"}, {f.DateTo, "biz_date__lte"}} {
if strings.TrimSpace(d.raw) == "" {
continue
}
if t, e := time.ParseInLocation("2006-01-02", strings.TrimSpace(d.raw), time.Local); e == nil {
qs = qs.Filter(d.cond, t)
}
}
total, _ := qs.Count()
var list []models.ErpDocument
if _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&list); err != nil {
return nil, 0, 500, "查询失败: " + err.Error()
}
erpFillDocNames(list)
for i := range list {
list[i].StatusLabel = models.ErpDocStatusLabel(list[i].Status)
}
return list, total, 200, ""
}
// erpDocDetailCore 单据详情(含明细、库存快照与审批实例摘要)
func erpDocDetailCore(a erpActor, id uint64) (*models.ErpDocument, int, string) {
if id == 0 {
return nil, 400, "无效的ID"
}
doc, err := erpLoadDoc(id, a.Tid)
if err != nil {
return nil, 404, "单据未找到"
}
doc.Items = erpDocItems(models.Orm, id)
erpFillItemStock(a.Tid, doc.Items, doc.WarehouseID)
single := []models.ErpDocument{*doc}
erpFillDocNames(single)
single[0].StatusLabel = models.ErpDocStatusLabel(single[0].Status)
if doc.InstanceID > 0 {
single[0].Instance = approvalEngine.InstanceBrief(doc.InstanceID)
}
return &single[0], 200, ""
}
// erpValidateDocHead 校验表头:仓库归属、往来单位匹配、调拨/盘点的仓库约束
func erpValidateDocHead(tid string, p *erpDocPayload) string {
needWh := models.ErpDocAffectsStock(p.DocType)
if needWh && p.WarehouseID == 0 && p.DocType != models.ErpDocStockTake {
return "请选择仓库"
}
if p.WarehouseID > 0 && !erpWarehouseOwned(tid, p.WarehouseID) {
return "仓库不存在或不属于当前租户"
}
if p.DocType == models.ErpDocTransfer {
if p.ToWarehouseID == 0 {
return "请选择调入仓库"
}
if p.ToWarehouseID == p.WarehouseID {
return "调入仓库不能与调出仓库相同"
}
if !erpWarehouseOwned(tid, p.ToWarehouseID) {
return "调入仓库不存在或不属于当前租户"
}
}
if pt := docTypePartner(p.DocType); pt != "" {
if p.PartnerID == 0 {
return "请选择" + erpPartnerLabel(pt)
}
}
if p.SourceDocID > 0 {
src, e := erpLoadDoc(p.SourceDocID, tid)
if e != nil {
return "来源单据不存在"
}
wants := docTypeSources(p.DocType)
if len(wants) > 0 {
allowed := false
labels := make([]string, 0, len(wants))
for _, w := range wants {
labels = append(labels, docTypeLabelOf(w))
if src.DocType == w {
allowed = true
}
}
if !allowed {
return "来源单据类型应为" + strings.Join(labels, "或")
}
if src.Status != models.ErpDocStatusApproved {
return "来源单据「" + src.DocNo + "」尚未生效,不能引用"
}
}
}
if p.DiscountAmount < 0 {
return "优惠金额不能为负"
}
return ""
}
// refItemIDs 收集明细行引用的来源单据行ID
func refItemIDs(raw []erpDocItemPayload) []uint64 {
out := make([]uint64, 0, len(raw))
for _, r := range raw {
if r.RefItemID > 0 {
out = append(out, r.RefItemID)
}
}
return out
}
// erpBuildItems 明细行规范化:补商品快照/单价(价格体系)、算金额、校验限价与批次
func erpBuildItems(tid string, doc *models.ErpDocument, raw []erpDocItemPayload) ([]models.ErpDocumentItem, string) {
ids := make([]uint64, 0, len(raw))
for _, r := range raw {
if r.ProductID > 0 {
ids = append(ids, r.ProductID)
}
}
pmap := erpProductMap(tid, ids)
// 来源订单明细行归属:草稿覆盖保存会重建明细(行 ID 变化),前端若仍拿着旧行 ID,
// 不在开单时就拦下,脏引用要拖到审核生效才报错,移动端更难定位。
refOwner := map[uint64]uint64{}
if refIDs := refItemIDs(raw); len(refIDs) > 0 {
var refs []models.ErpDocumentItem
_, _ = models.Orm.QueryTable(new(models.ErpDocumentItem)).
Filter("id__in", refIDs).Filter("tenant_id", tid).All(&refs, "id", "doc_id")
for _, r := range refs {
refOwner[r.ID] = r.DocID
}
}
settings := erp.GetSettings(tid)
out := make([]models.ErpDocumentItem, 0, len(raw))
for i, r := range raw {
p, okm := pmap[r.ProductID]
if !okm {
return nil, fmt.Sprintf("第%d行商品不存在", i+1)
}
if p.IsDraft == 1 || p.Status != 1 {
return nil, fmt.Sprintf("商品「%s」未启用,不能开单", p.ProductName)
}
if r.Quantity <= 0 {
return nil, fmt.Sprintf("商品「%s」数量必须大于0", p.ProductName)
}
unitPrice := r.UnitPrice
if unitPrice <= 0 {
unitPrice = erpPickPrice(p, doc.DocType, settings.SalesPriceType)
}
// 限价控制:低于最低售价直接拦截(除管理员放行的场景,这里不做例外)
if strings.HasPrefix(doc.DocType, "sales") && p.MinPrice > 0 && unitPrice < p.MinPrice-0.005 {
return nil, fmt.Sprintf("商品「%s」售价 %.2f 低于最低售价 %.2f", p.ProductName, unitPrice, p.MinPrice)
}
if doc.DocType == models.ErpDocPurchaseOrder || doc.DocType == models.ErpDocPurchaseIn {
if p.EnableBatch == 1 && strings.TrimSpace(r.BatchNo) == "" && doc.DocType == models.ErpDocPurchaseIn {
return nil, fmt.Sprintf("商品「%s」启用批次管理,请填写批次号", p.ProductName)
}
}
unit := strings.TrimSpace(r.Unit)
if unit == "" {
unit = p.BaseUnit
}
amount := r.Amount
if amount <= 0 {
amount = round2f(r.Quantity * unitPrice)
}
wh := r.WarehouseID
if wh > 0 && !erpWarehouseOwned(tid, wh) {
return nil, fmt.Sprintf("第%d行仓库无效", i+1)
}
if r.RefItemID > 0 && refOwner[r.RefItemID] != doc.SourceDocID {
return nil, fmt.Sprintf("第%d行关联的来源单据明细已失效,请重新选择来源单据", i+1)
}
it := models.ErpDocumentItem{
ProductID: p.ID, ProductName: p.ProductName, Spec: p.Spec, Unit: unit,
WarehouseID: wh, BatchNo: strings.TrimSpace(r.BatchNo),
Quantity: round4f(r.Quantity), BookQty: round4f(r.BookQty),
RefItemID: r.RefItemID, UnitPrice: unitPrice, Amount: round2f(amount),
Remark: r.Remark, SortOrder: r.SortOrder,
}
if it.SortOrder == 0 {
it.SortOrder = i + 1
}
if r.ProductionDate != "" {
if t, e := time.ParseInLocation("2006-01-02", r.ProductionDate, time.Local); e == nil {
it.ProductionDate = &t
if p.ShelfLifeDays > 0 {
ed := t.AddDate(0, 0, p.ShelfLifeDays)
it.ExpireDate = &ed
}
}
}
if r.ExpireDate != "" {
if t, e := time.ParseInLocation("2006-01-02", r.ExpireDate, time.Local); e == nil {
it.ExpireDate = &t
}
}
// 盘点行未带账面数时按当前结存补齐
if doc.DocType == models.ErpDocStockTake && it.BookQty == 0 && it.WarehouseID == 0 {
it.BookQty = erpStockQty(tid, p.ID, doc.WarehouseID, it.BatchNo)
if it.UnitPrice == 0 {
it.UnitPrice = erpStockCost(tid, p.ID, doc.WarehouseID, it.BatchNo)
}
}
if msg := erpLinePreCheck(tid, doc, settings, &it); msg != "" {
return nil, msg
}
out = append(out, it)
}
return out, ""
}
// erpDocSaveCore 新建或覆盖保存草稿(payload 带 id 即为编辑)。已生效/审批中的单据不允许改动。
func erpDocSaveCore(a erpActor, p erpDocPayload) (uint64, string, int, string) {
tid := a.Tid
if !erpDocTypes[p.DocType] {
return 0, "", 400, "未知的单据类型"
}
if len(p.Items) == 0 {
return 0, "", 400, "请至少添加一行明细"
}
if errMsg := erpValidateDocHead(tid, &p); errMsg != "" {
return 0, "", 400, errMsg
}
doc := &models.ErpDocument{TenantID: tid}
if p.ID > 0 {
exist, e := erpLoadDoc(p.ID, tid)
if e != nil {
return 0, "", 404, "单据未找到"
}
if exist.Status == models.ErpDocStatusApproved || exist.Status == models.ErpDocStatusPending {
return 0, "", 400, "审批中或已生效的单据不可编辑,请先撤回或作废"
}
if exist.Status == models.ErpDocStatusCancelled {
return 0, "", 400, "已作废单据不可编辑"
}
doc = exist
}
if doc.DocNo == "" {
no, e := erp.GenerateDocNo(nil, tid, p.DocType)
if e != nil {
return 0, "", 500, e.Error()
}
doc.DocNo = no
}
bizDate := time.Now()
if p.BizDate != "" {
if t, e := time.ParseInLocation("2006-01-02", p.BizDate, time.Local); e == nil {
bizDate = t
}
}
doc.DocType = p.DocType
doc.BizDate = bizDate
doc.WarehouseID = p.WarehouseID
doc.ToWarehouseID = p.ToWarehouseID
doc.SourceDocID = p.SourceDocID
doc.DiscountAmount = p.DiscountAmount
doc.Remark = p.Remark
doc.Status = models.ErpDocStatusDraft
if pt := docTypePartner(p.DocType); pt != "" {
doc.PartnerType = pt
doc.PartnerID = p.PartnerID
doc.PartnerName = erpPartnerName(pt, p.PartnerID)
if doc.PartnerName == "" {
return 0, "", 400, erpPartnerLabel(pt) + "未选择或不存在"
}
} else {
doc.PartnerType, doc.PartnerID, doc.PartnerName = "", 0, ""
}
if doc.CreateUserID == "" {
doc.CreateUserID = fmt.Sprintf("%d", a.UID)
doc.CreateUserName = a.Name
}
items, errMsg := erpBuildItems(tid, doc, p.Items)
if errMsg != "" {
return 0, "", 400, errMsg
}
doc.TotalQty = 0
doc.TotalAmount = 0
for _, it := range items {
doc.TotalQty += it.Quantity
doc.TotalAmount += it.Amount
}
doc.TotalQty = round4f(doc.TotalQty)
doc.TotalAmount = round2f(doc.TotalAmount)
doc.ActualAmount = round2f(doc.TotalAmount - doc.DiscountAmount)
if doc.ActualAmount < 0 {
return 0, "", 400, "优惠金额不能大于单据金额"
}
o := models.Orm
tx, err := o.Begin()
if err != nil {
return 0, "", 500, "保存失败"
}
rollback := true
defer func() { if rollback { _ = tx.Rollback() } }()
now := time.Now()
doc.UpdateTime = now
if doc.ID == 0 {
doc.CreateTime = now
id, e := tx.Insert(doc)
if e != nil {
return 0, "", 500, "保存失败: " + e.Error()
}
doc.ID = uint64(id)
} else {
if _, e := tx.QueryTable(new(models.ErpDocument)).Filter("id", doc.ID).
Update(orm.Params{
"doc_type": doc.DocType, "biz_date": doc.BizDate, "warehouse_id": doc.WarehouseID,
"to_warehouse_id": doc.ToWarehouseID, "partner_type": doc.PartnerType,
"partner_id": doc.PartnerID, "partner_name": doc.PartnerName,
"source_doc_id": doc.SourceDocID, "total_qty": doc.TotalQty,
"total_amount": doc.TotalAmount, "discount_amount": doc.DiscountAmount,
"actual_amount": doc.ActualAmount, "status": models.ErpDocStatusDraft,
"instance_id": 0, "remark": doc.Remark, "update_time": now,
}); e != nil {
return 0, "", 500, "保存失败: " + e.Error()
}
if _, e := tx.QueryTable(new(models.ErpDocumentItem)).Filter("doc_id", doc.ID).Delete(); e != nil {
return 0, "", 500, "保存失败: " + e.Error()
}
}
for i := range items {
items[i].DocID = doc.ID
items[i].TenantID = tid
items[i].ID = 0
if _, e := tx.Insert(&items[i]); e != nil {
return 0, "", 500, "保存明细失败: " + e.Error()
}
}
if err := tx.Commit(); err != nil {
return 0, "", 500, "保存失败: " + err.Error()
}
rollback = false
return doc.ID, doc.DocNo, 200, ""
}
// erpDocDeleteCore 删除单据(软删主表、硬删明细),仅草稿/驳回可删
func erpDocDeleteCore(a erpActor, id uint64) (int, string) {
doc, errMsg := erpLoadEditable(id, a.Tid, []int8{models.ErpDocStatusDraft, models.ErpDocStatusRejected})
if errMsg != "" {
return 400, errMsg
}
now := time.Now()
tx, err := models.Orm.Begin()
if err != nil {
return 500, "删除失败"
}
rollback := true
defer func() { if rollback { _ = tx.Rollback() } }()
if _, err = tx.QueryTable(new(models.ErpDocument)).Filter("id", doc.ID).
Update(orm.Params{"delete_time": now, "update_time": now}); err != nil {
return 500, "删除失败: " + err.Error()
}
// 明细随主表硬删:主表只软删,历史仍可追溯;明细无独立查询价值
if _, err = tx.QueryTable(new(models.ErpDocumentItem)).Filter("doc_id", doc.ID).Delete(); err != nil {
return 500, "删除失败: " + err.Error()
}
if err = tx.Commit(); err != nil {
return 500, "删除失败"
}
rollback = false
return 200, ""
}
// erpDocCancelCore 作废:草稿/驳回/审批中均可作废;已生效单据不做反向单据,直接拒绝,
// 保证「已生效=流水已入账」这一不可篡改前提。
func erpDocCancelCore(a erpActor, id uint64) (int, string) {
doc, errMsg := erpLoadEditable(id, a.Tid, []int8{
models.ErpDocStatusDraft, models.ErpDocStatusRejected, models.ErpDocStatusPending,
})
if errMsg != "" {
return 400, errMsg
}
now := time.Now()
params := orm.Params{"status": models.ErpDocStatusCancelled, "instance_id": 0, "update_time": now}
if _, err := models.Orm.QueryTable(new(models.ErpDocument)).Filter("id", doc.ID).Update(params); err != nil {
return 500, "作废失败: " + err.Error()
}
if doc.InstanceID > 0 {
approvalEngine.CancelInstance(doc.InstanceID, "单据已作废")
}
return 200, ""
}
// erpDocApproveCore 直接审核生效(未走审批流时使用)。库存/往来账效果在同一事务内落地。
func erpDocApproveCore(a erpActor, id uint64) (int, string) {
if id == 0 {
return 400, "无效的ID"
}
if err := erp.ApproveDocument(id, a.Tid, fmt.Sprintf("%d", a.UID), a.Name); err != nil {
return 400, err.Error()
}
return 200, ""
}
// erpDocSubmitCore 提交审批:flowID 为 0 时按单据类型自动选流。
// 流程未配置人工节点时引擎会直接结束实例,此时同步单据终态。
func erpDocSubmitCore(a erpActor, id, flowID uint64) (uint64, int, string) {
if id == 0 {
return 0, 400, "无效的ID"
}
doc, errMsg := erpLoadEditable(id, a.Tid, []int8{models.ErpDocStatusDraft, models.ErpDocStatusRejected})
if errMsg != "" {
return 0, 400, errMsg
}
if len(erpDocItems(models.Orm, doc.ID)) == 0 {
return 0, 400, "单据没有明细行,无法提交"
}
bizType := models.ErpDocApprovalBizType(doc.DocType)
deptID := erpUserOrgID(a.TidNum, a.UID)
if flowID == 0 {
flowID = erpPickFlowID(a.TidNum, deptID, bizType)
}
if flowID == 0 {
return 0, 400, "该单据类型尚未配置审批流程,可直接审核或到「审批流配置」中新增"
}
instID, err := approvalEngine.Submit(approvalEngine.SubmitInput{
Scope: models.ApprovalScopeTenant,
Tid: a.TidNum,
FlowID: flowID,
BizType: bizType,
BizID: doc.ID,
BizTitle: fmt.Sprintf("[%s] %s %s", docTypeLabelOf(doc.DocType), doc.DocNo, doc.PartnerName),
SubmitUID: a.UID,
SubmitName: a.Name,
DeptID: deptID,
FormData: map[string]interface{}{
"doc_type": doc.DocType,
"doc_no": doc.DocNo,
"total_amount": doc.TotalAmount,
"actual_amount": doc.ActualAmount,
"total_qty": doc.TotalQty,
"warehouse_id": doc.WarehouseID,
"partner_id": doc.PartnerID,
"partner_type": doc.PartnerType,
},
})
if err != nil {
return 0, 400, err.Error()
}
now := time.Now()
if _, err := models.Orm.QueryTable(new(models.ErpDocument)).Filter("id", doc.ID).
Update(orm.Params{"status": models.ErpDocStatusPending, "instance_id": instID, "update_time": now}); err != nil {
return 0, 500, "提交失败: " + err.Error()
}
approvalEngine.SyncBizResult(instID, approvalEngine.InstanceStatusOf(instID))
return instID, 200, ""
}
type erpFlowRow struct {
FlowID uint64 `json:"flow_id"`
FlowCode string `json:"flow_code"`
FlowName string `json:"flow_name"`
BizType string `json:"biz_type"`
}
// erpDocFlowsCore 该单据类型可用的审批流(先精确匹配 erp_xxx,再回落到模块级 erp)
func erpDocFlowsCore(a erpActor, docType string) (string, []erpFlowRow, int, string) {
if !erpDocTypes[docType] {
return "", nil, 400, "未知的单据类型"
}
bizType := models.ErpDocApprovalBizType(docType)
deptID := erpUserOrgID(a.TidNum, a.UID)
flows, err := approvalEngine.ListAvailableFlows(models.ApprovalScopeTenant, a.TidNum, bizType, deptID)
if err != nil {
return bizType, nil, 500, err.Error()
}
if len(flows) == 0 {
flows, _ = approvalEngine.ListAvailableFlows(models.ApprovalScopeTenant, a.TidNum, "erp", deptID)
}
out := make([]erpFlowRow, 0, len(flows))
for _, f := range flows {
out = append(out, erpFlowRow{f.FlowID, f.FlowCode, f.FlowName, f.BizType})
}
return bizType, out, 200, ""
}
// erpDocExecutableCore 取来源订单中尚可执行的明细行(入库/出库/退货按行带出,避免超收超发)
func erpDocExecutableCore(a erpActor, id uint64) (map[string]interface{}, int, string) {
doc, e := erpLoadDoc(id, a.Tid)
if e != nil {
return nil, 404, "单据未找到"
}
isReturn := doc.DocType == models.ErpDocPurchaseReturn || doc.DocType == models.ErpDocSalesReturn
items := erpDocItems(models.Orm, doc.ID)
out := make([]map[string]interface{}, 0, len(items))
for _, it := range items {
basis := it.Quantity
used := it.ExecutedQty
if isReturn {
used = erpReturnedQty(a.Tid, it.ID, 0)
}
remain := basis - used
if remain < -0.000001 {
remain = 0
}
out = append(out, map[string]interface{}{
"item_id": it.ID, "product_id": it.ProductID, "product_name": it.ProductName,
"spec": it.Spec, "unit": it.Unit, "batch_no": it.BatchNo,
"quantity": it.Quantity, "unit_price": it.UnitPrice, "amount": it.Amount,
"executed_qty": round4f(used), "remain_qty": round4f(remain),
})
}
return map[string]interface{}{"doc": doc, "items": out}, 200, ""
}
// erpTakeSheetCore 盘点表初始化:带出当前账面数量与加权成本,盘点员只改实盘数
func erpTakeSheetCore(a erpActor, warehouseID uint64) ([]map[string]interface{}, int, string) {
var stocks []models.ErpStock
_, _ = models.Orm.QueryTable(new(models.ErpStock)).
Filter("tenant_id", a.Tid).Filter("warehouse_id", warehouseID).
OrderBy("product_id", "batch_no").All(&stocks)
ids := make([]uint64, 0, len(stocks))
for _, s := range stocks {
ids = append(ids, s.ProductID)
}
pmap := erpProductMap(a.Tid, ids)
out := make([]map[string]interface{}, 0, len(stocks))
for _, s := range stocks {
p := pmap[s.ProductID]
out = append(out, map[string]interface{}{
"product_id": s.ProductID, "product_name": p.ProductName, "product_code": p.ProductCode,
"spec": p.Spec, "unit": p.BaseUnit, "batch_no": s.BatchNo,
"book_qty": s.Quantity, "cost_price": s.CostPrice,
})
}
return out, 200, ""
}
// erpDocMetaDict 开单页所需字典与默认值(两端共用同一份口径)
func erpDocMetaDict(a erpActor) map[string]interface{} {
s := erp.GetSettings(a.Tid)
types := make([]map[string]interface{}, 0, len(models.ErpDocTypeLabel))
for _, k := range []string{
models.ErpDocPurchaseOrder, models.ErpDocPurchaseIn, models.ErpDocPurchaseReturn,
models.ErpDocSalesOrder, models.ErpDocSalesOut, models.ErpDocSalesReturn,
models.ErpDocOtherIn, models.ErpDocOtherOut, models.ErpDocTransfer, models.ErpDocStockTake,
} {
types = append(types, map[string]interface{}{
"value": k, "label": models.ErpDocTypeLabel[k],
"affects_stock": models.ErpDocAffectsStock(k),
"partner_type": docTypePartner(k),
})
}
return map[string]interface{}{
"doc_types": types,
"statuses": []map[string]interface{}{
{"value": 0, "label": "草稿"}, {"value": 1, "label": "审批中"},
{"value": 2, "label": "已生效"}, {"value": 3, "label": "已驳回"}, {"value": 4, "label": "已作废"},
},
"default_warehouse_id": s.DefaultWarehouseID,
"sales_price_type": s.SalesPriceType,
"negative_stock": s.NegativeAllowed,
}
}