批量优化产品和产品分类

This commit is contained in:
2026-09-12 00:44:13 +08:00
parent 184c604bee
commit 2b1874943f
33 changed files with 2595 additions and 59 deletions
+25 -6
View File
@@ -81,6 +81,7 @@ func (c *BackendCrmContractController) List() {
ourRole := strings.TrimSpace(c.GetString("our_role"))
category := strings.TrimSpace(c.GetString("contract_category"))
projectType := strings.TrimSpace(c.GetString("project_type"))
projectID := strings.TrimSpace(c.GetString("project_id"))
status := strings.TrimSpace(c.GetString("status"))
tenantID := pipelineTenantID(claims)
@@ -110,6 +111,12 @@ func (c *BackendCrmContractController) List() {
case "headless": // 无头合同
cond = cond.And("project_id__isnull", true)
}
// 按项目精确筛选(项目详情「合同管理」Tab:一个项目可关联多份合同)
if projectID != "" {
if pid, err := strconv.ParseUint(projectID, 10, 64); err == nil && pid > 0 {
cond = cond.And("project_id", pid)
}
}
qs := models.Orm.QueryTable(new(models.TenantCrmContract)).SetCond(cond)
total, _ := qs.Count()
@@ -244,6 +251,10 @@ func (c *BackendCrmContractController) Create() {
return
}
row.Products = products
// 合同产品清单同步到产品管理(新增产品自动建档、成本单价取产品管理)
if synced, serr := SyncContractProducts(tenantID, pipelineUID(claims), json.RawMessage(products)); serr == nil {
row.Products = string(synced)
}
applyContractAmounts(&row)
if _, err := models.Orm.Insert(&row); err != nil {
@@ -336,6 +347,10 @@ func (c *BackendCrmContractController) Update() {
return
}
row.Products = products
// 合同产品清单同步到产品管理(新增产品自动建档、成本单价取产品管理)
if synced, serr := SyncContractProducts(tenantID, pipelineUID(claims), json.RawMessage(products)); serr == nil {
row.Products = string(synced)
}
}
applyContractAmounts(&row)
if p.Status != 0 {
@@ -512,8 +527,11 @@ func normalizeContractJSON(raw json.RawMessage) (string, error) {
}
// applyContractAmounts 按产品清单重算各部分金额(与前端 ProductList 汇总口径一致):
// 硬件部分=Σ硬件小计;软件部分=Σ软件小计;其他部分=Σ服务/开发/其他小计;
// 合同总金额=硬件+软件+其他;产品总成本=Σ(数量×成本单价);合同总利润=总金额-总成本。
// 每行含税金额=数量×单价×(1+税率/100);
// 硬件部分=Σ硬件含税小计;软件部分=Σ软件含税小计;其他部分=Σ服务/开发/其他含税小计;
// 合同总金额(含税)=硬件+软件+其他;产品总成本=Σ(数量×成本单价)(成本通常不含税,原值汇总);
// 合同总利润=总金额-总成本。
// 归类口径:分类代码 1/2 或名称含「硬件」/「软件」分别计入硬件/软件,其余计入其他。
func applyContractAmounts(row *models.TenantCrmContract) {
var items []map[string]interface{}
if strings.TrimSpace(row.Products) != "" {
@@ -524,13 +542,14 @@ func applyContractAmounts(row *models.TenantCrmContract) {
qty := toFloat64(item["quantity"])
price := toFloat64(item["price"])
costPrice := toFloat64(item["cost_price"])
amount := round2(qty * price)
taxRate := toFloat64(item["tax_rate"])
amount := round2(qty * price * (1 + taxRate/100)) // 含税金额
cost = round2(cost + round2(qty*costPrice))
cat := fmt.Sprintf("%v", item["category"])
switch cat {
case "1":
switch {
case cat == "1" || strings.Contains(cat, "硬件"):
hardware = round2(hardware + amount)
case "2":
case cat == "2" || strings.Contains(cat, "软件"):
software = round2(software + amount)
default:
other = round2(other + amount)