批量更新功能
This commit is contained in:
@@ -35,23 +35,25 @@ type contractSummary struct {
|
||||
|
||||
// contractPayload 创建 / 更新请求体。
|
||||
type contractPayload struct {
|
||||
ContractNo string `json:"contract_no"`
|
||||
ContractName string `json:"contract_name"`
|
||||
ContractCategory string `json:"contract_category"`
|
||||
OurRole int8 `json:"our_role"` // 我方角色:1甲方/2乙方/3丙方/4丁方
|
||||
PartyCount int8 `json:"party_count"`
|
||||
ProjectID uint64 `json:"project_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
OwnerUserName string `json:"owner_user_name"`
|
||||
SignDate string `json:"sign_date"`
|
||||
EffectiveDate string `json:"effective_date"`
|
||||
ExpireDate string `json:"expire_date"`
|
||||
Parties json.RawMessage `json:"parties"`
|
||||
Products json.RawMessage `json:"products"`
|
||||
Step int8 `json:"step"`
|
||||
Status int8 `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
ContractNo string `json:"contract_no"`
|
||||
ContractName string `json:"contract_name"`
|
||||
ContractCategory string `json:"contract_category"`
|
||||
// 我方角色:0=我方不参与(如上中游项目中上游与中游主体之间的合同,我方不是签约方)
|
||||
// 1甲方/2乙方/3丙方/4丁方。用指针区分「未传」与「传 0」。
|
||||
OurRole *int8 `json:"our_role"`
|
||||
PartyCount int8 `json:"party_count"`
|
||||
ProjectID uint64 `json:"project_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
OwnerUserName string `json:"owner_user_name"`
|
||||
SignDate string `json:"sign_date"`
|
||||
EffectiveDate string `json:"effective_date"`
|
||||
ExpireDate string `json:"expire_date"`
|
||||
Parties json.RawMessage `json:"parties"`
|
||||
Products json.RawMessage `json:"products"`
|
||||
Step int8 `json:"step"`
|
||||
Status int8 `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// contractResp 列表 / 详情响应:parties / products 解析为 JSON 数组透出,summary 由金额字段组装。
|
||||
@@ -138,7 +140,58 @@ func (c *BackendCrmContractController) List() {
|
||||
total, _ := qs.Count()
|
||||
var list []models.TenantCrmContract
|
||||
if total > 0 {
|
||||
_, _ = qs.OrderBy("-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
|
||||
offset := (page - 1) * pageSize
|
||||
if status != "" {
|
||||
// 已按状态筛选:结果只含单一状态,直接按新→旧分页
|
||||
if _, err := qs.OrderBy("-id").Offset(offset).Limit(pageSize).All(&list); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 列表排列:按状态分组展示 —— 草稿(1) → 异常(5) → 履约中(4) → 已完成(2) → 已作废(3),
|
||||
// 其余状态(NULL / 历史异常值)沉底;组内仍按新→旧。
|
||||
// 先统计各组数量,把当前页窗口换算成各组区间,再分别取数按序拼接,保证翻页后整体顺序一致。
|
||||
groupConds := []*orm.Condition{
|
||||
orm.NewCondition().And("status", 1),
|
||||
orm.NewCondition().And("status", 5),
|
||||
orm.NewCondition().And("status", 4),
|
||||
orm.NewCondition().And("status", 2),
|
||||
orm.NewCondition().And("status", 3),
|
||||
orm.NewCondition().AndNot("status__in", []int8{1, 5, 4, 2, 3}).Or("status__isnull", true),
|
||||
}
|
||||
end := offset + pageSize
|
||||
if end > int(total) {
|
||||
end = int(total)
|
||||
}
|
||||
cursor := 0
|
||||
for _, gc := range groupConds {
|
||||
if cursor >= end {
|
||||
break
|
||||
}
|
||||
// 注意:beego 的 AndCond 会修改接收者,这里用新条件对象包装,避免循环内条件累积
|
||||
groupCond := orm.NewCondition().AndCond(cond).AndCond(gc)
|
||||
cnt, _ := models.Orm.QueryTable(new(models.TenantCrmContract)).SetCond(groupCond).Count()
|
||||
groupTotal := int(cnt)
|
||||
if groupTotal == 0 {
|
||||
continue
|
||||
}
|
||||
groupStart := cursor
|
||||
groupEnd := cursor + groupTotal
|
||||
cursor = groupEnd
|
||||
// 当前页与本组区间的交集
|
||||
from, to := max(offset, groupStart), min(end, groupEnd)
|
||||
if to <= from {
|
||||
continue
|
||||
}
|
||||
var part []models.TenantCrmContract
|
||||
if _, err := models.Orm.QueryTable(new(models.TenantCrmContract)).SetCond(groupCond).
|
||||
OrderBy("-id").Offset(from - groupStart).Limit(to - from).All(&part); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
list = append(list, part...)
|
||||
}
|
||||
}
|
||||
}
|
||||
items := make([]contractResp, 0, len(list))
|
||||
for i := range list {
|
||||
@@ -153,6 +206,7 @@ func (c *BackendCrmContractController) List() {
|
||||
// 全租户合同统计(排除已作废),供列表页顶部汇总卡片使用。
|
||||
// 支持按周期筛选:period=month/quarter/year(当期)或 custom(start_date~end_date),
|
||||
// 按合同有效期 [生效日期, 结束日期] 与所选周期重叠统计。
|
||||
// 「合同总利润」仅统计非甲方(收款方)合同:我方为甲方的合同是采购付款,只有支出、不存在利润。
|
||||
func (c *BackendCrmContractController) Stats() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
@@ -168,7 +222,8 @@ func (c *BackendCrmContractController) Stats() {
|
||||
}
|
||||
|
||||
table := new(models.TenantCrmContract).TableName()
|
||||
where := "tenant_id = ? AND delete_time IS NULL AND status <> 3"
|
||||
// 我方不参与的合同(our_role=0,如上中游项目的链条合同)不属于我方经营合同,不计入经营统计
|
||||
where := "tenant_id = ? AND delete_time IS NULL AND status <> 3 AND our_role <> 0"
|
||||
args := []interface{}{tenantID}
|
||||
// 按合同有效期 [生效日期, 结束日期] 与所选周期 [start, end] 重叠来统计:
|
||||
// 合同生效日 <= 周期末 且 合同结束日 >= 周期初;日期为空视为不限制对应边界。
|
||||
@@ -180,8 +235,10 @@ func (c *BackendCrmContractController) Stats() {
|
||||
where += " AND (effective_date IS NULL OR effective_date <= ?)"
|
||||
args = append(args, end)
|
||||
}
|
||||
// 「合同总利润」只汇总非甲方(收款方)合同:我方为甲方时是采购付款方,合同金额即支出,不存在利润
|
||||
raw := "SELECT COUNT(*) AS total, IFNULL(SUM(total_amount),0) AS total_amount, " +
|
||||
"IFNULL(SUM(total_cost),0) AS total_cost, IFNULL(SUM(total_profit),0) AS total_profit " +
|
||||
"IFNULL(SUM(total_cost),0) AS total_cost, " +
|
||||
"IFNULL(SUM(CASE WHEN our_role <> 1 THEN total_profit ELSE 0 END),0) AS total_profit " +
|
||||
"FROM " + table + " WHERE " + where
|
||||
var rows []orm.Params
|
||||
if _, err := models.Orm.Raw(raw, args...).Values(&rows); err != nil || len(rows) == 0 {
|
||||
@@ -270,11 +327,15 @@ func (c *BackendCrmContractController) Create() {
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
now := time.Now()
|
||||
ourRole := int8(2)
|
||||
if p.OurRole != nil {
|
||||
ourRole = *p.OurRole
|
||||
}
|
||||
row := models.TenantCrmContract{
|
||||
TenantID: tenantID,
|
||||
ContractNo: strings.TrimSpace(p.ContractNo),
|
||||
ContractName: strings.TrimSpace(p.ContractName),
|
||||
OurRole: pickInt8(p.OurRole, 2, 4),
|
||||
OurRole: normalizeOurRole(ourRole),
|
||||
PartyCount: pickInt8(p.PartyCount, 2, 4),
|
||||
OwnerUserID: firstNonEmpty(p.OwnerUserID, pipelineUID(claims)),
|
||||
OwnerUserName: firstNonEmpty(p.OwnerUserName, resolveUserName(claims)),
|
||||
@@ -338,6 +399,8 @@ func (c *BackendCrmContractController) Create() {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 参与方填写的签约人(姓名 + 电话)同步写入对应企业的联系人通讯录
|
||||
syncContractPartyContacts(tenantID, row.Parties)
|
||||
crmWriteLog(tenantID, 3, row.ID, "create", "创建合同:"+row.ContractName, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID})
|
||||
}
|
||||
@@ -386,8 +449,9 @@ func (c *BackendCrmContractController) Update() {
|
||||
if strings.TrimSpace(p.ContractCategory) != "" {
|
||||
row.ContractCategory = strings.TrimSpace(p.ContractCategory)
|
||||
}
|
||||
if p.OurRole != 0 {
|
||||
row.OurRole = p.OurRole
|
||||
if p.OurRole != nil {
|
||||
// 0=我方不参与也是合法值,用指针判断避免误吞
|
||||
row.OurRole = normalizeOurRole(*p.OurRole)
|
||||
}
|
||||
if p.PartyCount != 0 {
|
||||
row.PartyCount = p.PartyCount
|
||||
@@ -451,6 +515,8 @@ func (c *BackendCrmContractController) Update() {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 参与方填写的签约人(姓名 + 电话)同步写入对应企业的联系人通讯录
|
||||
syncContractPartyContacts(tenantID, row.Parties)
|
||||
crmWriteLog(tenantID, 3, row.ID, "update", "更新合同:"+row.ContractName, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID})
|
||||
}
|
||||
@@ -496,7 +562,7 @@ func (c *BackendCrmContractController) Delete() {
|
||||
}
|
||||
|
||||
// ChangeStatus POST /backend/crm/contract/:id/status
|
||||
// 合同状态流转:1=草稿 2=已完成 3=已作废 4=履约中 5=执行异常。
|
||||
// 合同状态流转:1=草稿 2=已完成 3=已作废 4=履约中 5=异常。
|
||||
// 向导创建的合同默认为草稿,签订 / 履约等状态在列表中手动流转;各状态间可互切。
|
||||
func (c *BackendCrmContractController) ChangeStatus() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
@@ -517,7 +583,7 @@ func (c *BackendCrmContractController) ChangeStatus() {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
// 合法目标状态:2已完成 / 3已作废 / 4履约中 / 5执行异常
|
||||
// 合法目标状态:2已完成 / 3已作废 / 4履约中 / 5异常
|
||||
if p.Status < 2 || p.Status > 5 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的状态值")
|
||||
return
|
||||
@@ -561,7 +627,7 @@ func contractStatusName(s int8) string {
|
||||
case 4:
|
||||
return "履约中"
|
||||
case 5:
|
||||
return "执行异常"
|
||||
return "异常"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
@@ -600,6 +666,42 @@ func buildContractResp(row *models.TenantCrmContract) contractResp {
|
||||
}
|
||||
}
|
||||
|
||||
// syncContractPartyContacts 把合同参与方填写的签约人(姓名 + 电话)写入对应企业的联系人通讯录:
|
||||
// 新建的客户 / 供应商往往还没有联系人,用户只能手填签约人姓名与电话,
|
||||
// 这里自动建档(仅姓名 + 电话,其余资料留空,可在联系人管理中继续补全),
|
||||
// 避免这些信息只停留在合同文本里;同名或同号已存在时不会重复建档。
|
||||
// 仅处理关联了客户 / 供应商库的参与方:本公司方(ref_type=0)与手工填写未关联的名称(ref_id=0)跳过。
|
||||
func syncContractPartyContacts(tenantID, partiesJSON string) {
|
||||
partiesJSON = strings.TrimSpace(partiesJSON)
|
||||
if models.Orm == nil || partiesJSON == "" || partiesJSON == "[]" {
|
||||
return
|
||||
}
|
||||
var parties []struct {
|
||||
RefType int `json:"ref_type"`
|
||||
RefID uint64 `json:"ref_id"`
|
||||
SignerName string `json:"signer_name"`
|
||||
SignerPhone string `json:"signer_phone"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(partiesJSON), &parties); err != nil {
|
||||
return
|
||||
}
|
||||
for _, p := range parties {
|
||||
// ref_type:0=本公司 1=客户 2=供应商;未关联库(ref_id=0)时无处可写
|
||||
if p.RefID == 0 || (p.RefType != 1 && p.RefType != 2) {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(p.SignerName)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
companyType := crmContactCompanyType(p.RefType)
|
||||
if !crmContactCompanyExists(tenantID, companyType, p.RefID) {
|
||||
continue
|
||||
}
|
||||
crmEnsureCompanyContact(tenantID, companyType, p.RefID, name, strings.TrimSpace(p.SignerPhone))
|
||||
}
|
||||
}
|
||||
|
||||
// resolveProject 校验项目归属当前租户并返回 (id, 标准项目名称)。
|
||||
func (c *BackendCrmContractController) resolveProject(tenantID string, projectID uint64) (uint64, string, bool) {
|
||||
var proj models.TenantCrmProject
|
||||
@@ -806,6 +908,15 @@ func genContractNo(tenantID string) string {
|
||||
return fmt.Sprintf("HT-%s-%d", time.Now().Format("20060102"), time.Now().UnixNano()%100000)
|
||||
}
|
||||
|
||||
// normalizeOurRole 归一我方角色:0=我方不参与(如上中游项目中上游与中游主体之间的合同,
|
||||
// 我方不是签约方),1=甲方 2=乙方 3=丙方 4=丁方,越界回退乙方。
|
||||
func normalizeOurRole(v int8) int8 {
|
||||
if v == 0 {
|
||||
return 0
|
||||
}
|
||||
return pickInt8(v, 2, 4)
|
||||
}
|
||||
|
||||
// pickInt8 取值约束:v 落在 [min, max] 内返回 v,否则返回 def。
|
||||
func pickInt8(v, def, max int8) int8 {
|
||||
if v >= 1 && v <= max {
|
||||
|
||||
Reference in New Issue
Block a user