You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
17 KiB
304 lines
17 KiB
# -*- coding: utf-8 -*-
|
|
"""Generate all crm-opportunity .bru files."""
|
|
import io, os, sys
|
|
sys.path.insert(0, 'D:/code/crm-backend-matt/.scratch')
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
from bru_lib import (
|
|
write_bru, write_folder_bru, bru_form, build_docs, env_json,
|
|
page_json, req_form_example, req_query_example,
|
|
)
|
|
|
|
# ---------- OpportunityDTO rows + JSON (shared) ----------
|
|
OPP_DTO_ROWS = [
|
|
('id', 'string', '商机ID(Long,序列化为 string)'),
|
|
('createTime', 'string', '创建时间 yyyy-MM-dd HH:mm:ss'),
|
|
('updateTime', 'string', '更新时间 yyyy-MM-dd HH:mm:ss'),
|
|
('oppName', 'string', '商机名称'),
|
|
('oppSource', 'string', '商机来源字典 code'),
|
|
('oppType', 'string', '商机类型字典 code'),
|
|
('industryCode', 'string', '行业字典 code'),
|
|
('bidForm', 'string', '招标形式字典 code'),
|
|
('localityType', 'string', '项目属地字典 code'),
|
|
('partyAClear', 'number', '甲方是否明确 0/1'),
|
|
('partyA', 'string', '甲方名称'),
|
|
('provinceCode', 'string', '省份国标 code'),
|
|
('cityCode', 'string', '市/区国标 code'),
|
|
('remark', 'string', '备注'),
|
|
('projectAmount', 'number', '项目金额(列表展示为「预计金额」)'),
|
|
('ownerUserId', 'string', '负责人ID(Long,序列化为 string)'),
|
|
('ownerNameSnapshot', 'string', '负责人姓名快照'),
|
|
('ownerDeptId', 'string', '负责人部门ID快照'),
|
|
('poolReason', 'string', '进入公海原因字典 code'),
|
|
('oppStatus', 'number', '商机状态(轴1:1待领取/2推进中/3暂缓中/4已关闭/5已转项目)'),
|
|
('currentStageId', 'string', '当前阶段节点ID(轴2)'),
|
|
('stageTemplateVersion', 'string', '阶段模板版本号冗余'),
|
|
('sourceLeadId', 'string', '来源线索ID'),
|
|
('primaryCustomerId', 'string', '主要意向客户ID'),
|
|
('primaryCustomerNameSnapshot', 'string', '主客户名快照(列表「意向客户」列)'),
|
|
('claimTime', 'string', '领取时刻'),
|
|
('lastValidFollowTime', 'string', '最近有效跟进时间'),
|
|
('oppSourceName', 'string', '商机来源名称(出参回显,字典快照)'),
|
|
('industryName', 'string', '行业名称(出参回显,字典快照)'),
|
|
('provinceName', 'string', '省份名称(出参回显)'),
|
|
('cityName', 'string', '市/区名称(出参回显)'),
|
|
('ownerDeptName', 'string', '负责人部门名称(出参回显)'),
|
|
('currentStageName', 'string', '当前工作节点名称(出参回显)'),
|
|
('stageStayDays', 'number', '节点停留时间(天,出参回显)'),
|
|
('nextStageName', 'string', '下一节点名称(出参回显,展示用建议下一步)'),
|
|
('lastFollowTime', 'string', '最近跟进时间(出参回显)'),
|
|
('schemeCardStatus', 'number', '方案卡状态(出参回显;无主客户卡=未创建0)'),
|
|
('schemeBudget', 'number', '方案预算(出参回显)'),
|
|
('followed', 'boolean', '当前用户是否已关注(出参回显)'),
|
|
('poolStageNameSnapshot', 'string', '入池前工作节点名称(出参回显,公海视图专属)'),
|
|
]
|
|
|
|
OPP_DTO_JSON_DETAIL = ( # for detail: data is one object
|
|
'{\n'
|
|
' "id": "1",\n'
|
|
' "createTime": "2024-01-01 12:00:00",\n'
|
|
' "updateTime": "2024-01-01 12:00:00",\n'
|
|
' "oppName": "示例商机",\n'
|
|
' "oppSource": "opp_source_01",\n'
|
|
' "oppType": "opp_type_01",\n'
|
|
' "industryCode": "industry_01",\n'
|
|
' "bidForm": "bid_01",\n'
|
|
' "localityType": "local_01",\n'
|
|
' "partyAClear": 1,\n'
|
|
' "partyA": "示例甲方",\n'
|
|
' "provinceCode": "110000",\n'
|
|
' "cityCode": "110100",\n'
|
|
' "remark": "备注示例",\n'
|
|
' "projectAmount": 1000000,\n'
|
|
' "ownerUserId": "1",\n'
|
|
' "ownerNameSnapshot": "张三",\n'
|
|
' "ownerDeptId": "1",\n'
|
|
' "poolReason": null,\n'
|
|
' "oppStatus": 2,\n'
|
|
' "currentStageId": "1",\n'
|
|
' "stageTemplateVersion": "v1",\n'
|
|
' "sourceLeadId": "1",\n'
|
|
' "primaryCustomerId": "1",\n'
|
|
' "primaryCustomerNameSnapshot": "示例客户",\n'
|
|
' "claimTime": "2024-01-01 12:00:00",\n'
|
|
' "lastValidFollowTime": "2024-01-02 10:00:00",\n'
|
|
' "oppSourceName": "线索转入",\n'
|
|
' "industryName": "示例行业",\n'
|
|
' "provinceName": "北京市",\n'
|
|
' "cityName": "北京市",\n'
|
|
' "ownerDeptName": "华北大区",\n'
|
|
' "currentStageName": "需求确认",\n'
|
|
' "stageStayDays": 3,\n'
|
|
' "nextStageName": "方案报价",\n'
|
|
' "lastFollowTime": "2024-01-02 10:00:00",\n'
|
|
' "schemeCardStatus": 1,\n'
|
|
' "schemeBudget": 800000,\n'
|
|
' "followed": true,\n'
|
|
' "poolStageNameSnapshot": null\n'
|
|
' }'
|
|
)
|
|
|
|
# ---------- folders ----------
|
|
print('== folders ==')
|
|
write_folder_bru('商机管理', '商机管理', 20)
|
|
write_folder_bru('销售机会', '销售机会', 21)
|
|
write_folder_bru('商机公海', '商机公海', 22)
|
|
for i, name in enumerate(['商机新建', '商机详情', '商机流转', '协作', '方案卡', '商机阶段', '详情子域'], start=10):
|
|
write_folder_bru(f'商机管理/{name}', name, i)
|
|
for i, name in enumerate(['最近访问', '我负责的', '我参与的', '我关注的'], start=10):
|
|
write_folder_bru(f'销售机会/{name}', name, i)
|
|
|
|
# ================================================================
|
|
# 商机新建
|
|
# ================================================================
|
|
print('== 商机新建 ==')
|
|
|
|
# --- 1. POST /api/opportunity 新建商机
|
|
params = [
|
|
('oppSource', 'string', '是', '-', '商机来源字典 code(opp_source_01=线索转入触发关联线索必填)'),
|
|
('sourceLeadId', 'string', '条件', '-', '关联线索 id(Long,序列化为 string;oppSource=线索转入时必填)'),
|
|
('opportunityName', 'string', '是', '-', '商机名称'),
|
|
('oppType', 'string', '否', '-', '商机类型字典 code'),
|
|
('industryCode', 'string', '否', '-', '行业字典 code'),
|
|
('bidForm', 'string', '否', '-', '招标形式字典 code'),
|
|
('localityType', 'string', '否', '-', '项目属地字典 code(本地/异地)'),
|
|
('partyAClear', 'number', '否', '0', '甲方是否明确 0/1'),
|
|
('partyA', 'string', '否', '-', '甲方'),
|
|
('customerId', 'string', '否', '-', '主要意向客户 id(Long,序列化为 string;A4 未就绪前可空)'),
|
|
('customerName', 'string', '否', '-', '主要意向客户名快照'),
|
|
('provinceCode', 'string', '否', '-', '省国标 code'),
|
|
('cityCode', 'string', '否', '-', '市国标 code'),
|
|
('remark', 'string', '否', '-', '备注'),
|
|
]
|
|
kv = [('oppSource','opp_source_01'),('sourceLeadId','1'),('opportunityName','示例商机'),
|
|
('oppType','opp_type_01'),('industryCode','industry_01'),('bidForm','bid_01'),
|
|
('localityType','local_01'),('partyAClear','1'),('partyA','示例甲方'),
|
|
('customerId','1'),('customerName','示例客户'),('provinceCode','110000'),
|
|
('cityCode','110100'),('remark','备注示例')]
|
|
docs = build_docs(
|
|
'新建商机', 'POST', '/api/opportunity',
|
|
'商机侧新建入口(薄适配层,与线索侧转商机 port 并列的第二条建商机入口)。`oppSource=线索转入`(opp_source_01)时须选【关联线索】(下拉数据源见 `GET /api/opportunity/convertible-leads`);其余来源 `sourceLeadId` 可空。当前登录人自动为负责人/创建人。',
|
|
params,
|
|
req_form_example('POST', '/api/opportunity', kv),
|
|
'新建商机 ID(Long,序列化为 string)',
|
|
env_json('"1"'),
|
|
)
|
|
write_bru('商机管理/商机新建', '新建商机.bru',
|
|
bru_form(1, '新建商机', 'post', '/api/opportunity', kv, docs))
|
|
|
|
# --- 2. GET /api/opportunity/convertible-leads
|
|
docs = build_docs(
|
|
'可关联线索下拉候选', 'GET', '/api/opportunity/convertible-leads',
|
|
'新建商机弹窗中「关联线索」下拉数据源。返回当前登录人可关联的线索:状态 = 已领取 / 跟进中,且未被商机占用(未被任何商机的 source_lead_id 引用)。',
|
|
[],
|
|
req_query_example('GET', '/api/opportunity/convertible-leads', []),
|
|
[
|
|
('id', 'string', '线索 id(Long,序列化为 string,回填 sourceLeadId)'),
|
|
('leadName', 'string', '线索名称(下拉展示)'),
|
|
('phone', 'string', '联系电话(下拉辅助展示)'),
|
|
],
|
|
env_json('[\n {\n "id": "1",\n "leadName": "示例线索",\n "phone": "13800000000"\n }\n ]'),
|
|
)
|
|
write_bru('商机管理/商机新建', '可关联线索下拉.bru',
|
|
bru_form(2, '可关联线索下拉', 'get', '/api/opportunity/convertible-leads', [], docs))
|
|
|
|
|
|
# ================================================================
|
|
# 商机详情
|
|
# ================================================================
|
|
print('== 商机详情 ==')
|
|
|
|
docs = build_docs(
|
|
'商机详情', 'GET', '/api/opportunity/detail',
|
|
'商机详情页顶部信息 + 全量字段回显。返回 OpportunityDTO(含所有主表字段 + 出参回显字段:字典/部门/区划名、计算列、当前用户是否已关注等)。',
|
|
[('id', 'string', '是', '-', '商机 ID(Long,序列化为 string)')],
|
|
req_query_example('GET', '/api/opportunity/detail', [('id', '1')]),
|
|
OPP_DTO_ROWS,
|
|
env_json(OPP_DTO_JSON_DETAIL),
|
|
)
|
|
write_bru('商机管理/商机详情', '商机详情.bru',
|
|
bru_form(1, '商机详情', 'get', '/api/opportunity/detail', [('id', '1')], docs))
|
|
|
|
edit_params = [
|
|
('id', 'string', '是', '-', '商机 ID(Long,序列化为 string)'),
|
|
('oppName', 'string', '否', '-', '商机名称'),
|
|
('oppSource', 'string', '否', '-', '商机来源字典 code'),
|
|
('oppType', 'string', '否', '-', '商机类型字典 code'),
|
|
('industryCode', 'string', '否', '-', '行业字典 code'),
|
|
('bidForm', 'string', '否', '-', '招标形式字典 code'),
|
|
('localityType', 'string', '否', '-', '项目属地字典 code'),
|
|
('partyAClear', 'number', '否', '-', '甲方是否明确 0/1'),
|
|
('partyA', 'string', '否', '-', '甲方名称'),
|
|
('provinceCode', 'string', '否', '-', '省份国标 code'),
|
|
('cityCode', 'string', '否', '-', '市/区国标 code'),
|
|
('remark', 'string', '否', '-', '备注'),
|
|
('projectAmount', 'number', '否', '-', '项目金额'),
|
|
('primaryCustomerId', 'string', '否', '-', '主要意向客户ID(Long,序列化为 string)'),
|
|
('primaryCustomerNameSnapshot', 'string', '否', '-', '主客户名快照'),
|
|
]
|
|
edit_kv = [('id','1'),('oppName','示例商机'),('oppType','opp_type_01'),('industryCode','industry_01'),
|
|
('bidForm','bid_01'),('localityType','local_01'),('partyAClear','1'),('partyA','示例甲方'),
|
|
('provinceCode','110000'),('cityCode','110100'),('remark','备注示例'),
|
|
('projectAmount','1000000'),('primaryCustomerId','1'),('primaryCustomerNameSnapshot','示例客户')]
|
|
docs = build_docs(
|
|
'编辑商机', 'POST', '/api/opportunity/edit',
|
|
'商机详情页编辑保存(非终态可编辑;全量字段覆盖)。入参为 OpportunityDTO,仅列出常用可编辑字段;已关闭/已转项目等终态由服务层拒绝。',
|
|
edit_params,
|
|
req_form_example('POST', '/api/opportunity/edit', edit_kv),
|
|
None,
|
|
env_json('null'),
|
|
)
|
|
write_bru('商机管理/商机详情', '编辑商机.bru',
|
|
bru_form(2, '编辑商机', 'post', '/api/opportunity/edit', edit_kv, docs))
|
|
|
|
|
|
# ================================================================
|
|
# 商机流转 (Transition)
|
|
# ================================================================
|
|
print('== 商机流转 ==')
|
|
|
|
def transition_bru(seq, filename, name, path, params, kv, desc):
|
|
d = build_docs(name, 'POST', path, desc, params,
|
|
req_form_example('POST', path, kv), None, env_json('null'))
|
|
write_bru('商机管理/商机流转', filename,
|
|
bru_form(seq, name, 'post', path, kv, d))
|
|
|
|
# claim
|
|
transition_bru(1, '领取商机.bru', '领取商机', '/api/opportunity/claim',
|
|
[('id','string','是','-','商机ID(Long,序列化为 string)'),
|
|
('ownerNameSnapshot','string','否','-','负责人姓名快照(前端预填当前登录人姓名)'),
|
|
('ownerDeptId','string','否','-','负责人部门ID快照(Long,序列化为 string;前端预填当前登录人部门)')],
|
|
[('id','1'),('ownerNameSnapshot','张三'),('ownerDeptId','1')],
|
|
'状态机 E1:待领取 → 推进中。当前登录人成为负责人,`claim_time` 落时刻。')
|
|
|
|
transition_bru(2, '批量领取.bru', '批量领取', '/api/opportunity/claim-batch',
|
|
[('ids','string[]','是','-','商机ID列表(逗号分隔,Long序列化为 string)'),
|
|
('ownerNameSnapshot','string','否','-','负责人姓名快照'),
|
|
('ownerDeptId','string','否','-','负责人部门ID快照')],
|
|
[('ids','1,2,3'),('ownerNameSnapshot','张三'),('ownerDeptId','1')],
|
|
'状态机 E1 批量版。每条独立事务;单条失败不影响其他条。')
|
|
|
|
transition_bru(3, '分配商机.bru', '分配商机', '/api/opportunity/assign',
|
|
[('id','string','是','-','商机ID(Long,序列化为 string)'),
|
|
('userId','string','是','-','被分配的销售用户ID(Long,序列化为 string)'),
|
|
('ownerNameSnapshot','string','否','-','负责人姓名快照'),
|
|
('ownerDeptId','string','否','-','负责人部门ID快照')],
|
|
[('id','1'),('userId','2'),('ownerNameSnapshot','李四'),('ownerDeptId','1')],
|
|
'状态机 E2:待领取 → 推进中。指派他人为负责人(管理员/主管权限)。')
|
|
|
|
transition_bru(4, '批量分配.bru', '批量分配', '/api/opportunity/assign-batch',
|
|
[('ids','string[]','是','-','商机ID列表(逗号分隔)'),
|
|
('userId','string','是','-','被分配的销售用户ID'),
|
|
('ownerNameSnapshot','string','否','-','负责人姓名快照'),
|
|
('ownerDeptId','string','否','-','负责人部门ID快照')],
|
|
[('ids','1,2,3'),('userId','2'),('ownerNameSnapshot','李四'),('ownerDeptId','1')],
|
|
'状态机 E2 批量版。每条独立事务。')
|
|
|
|
transition_bru(5, '抛公海.bru', '抛公海', '/api/opportunity/release-pool',
|
|
[('id','string','是','-','商机ID'),
|
|
('poolReason','string','否','-','进入公海原因字典 code')],
|
|
[('id','1'),('poolReason','pool_reason_01')],
|
|
'状态机 E3:推进中 → 待领取。清空负责人,商机重回公海池。')
|
|
|
|
transition_bru(6, '暂缓.bru', '暂缓', '/api/opportunity/pause',
|
|
[('id','string','是','-','商机ID'),
|
|
('pauseReason','string','否','-','暂缓原因字典 code'),
|
|
('pauseExpectedRestartDate','string','否','-','预计重启日期 yyyy-MM-dd'),
|
|
('pauseRemark','string','否','-','暂缓备注')],
|
|
[('id','1'),('pauseReason','pause_reason_01'),('pauseExpectedRestartDate','2024-06-01'),('pauseRemark','等待客户预算落地')],
|
|
'状态机 E4:推进中 → 暂缓中。')
|
|
|
|
transition_bru(7, '取消暂缓.bru', '取消暂缓', '/api/opportunity/resume',
|
|
[('id','string','是','-','商机ID')],
|
|
[('id','1')],
|
|
'状态机 E5:暂缓中 → 推进中。')
|
|
|
|
transition_bru(8, '关闭商机.bru', '关闭商机', '/api/opportunity/close',
|
|
[('id','string','是','-','商机ID'),
|
|
('closeReason','string','否','-','关闭原因字典 code'),
|
|
('closeRemark','string','否','-','关闭备注')],
|
|
[('id','1'),('closeReason','close_reason_01'),('closeRemark','客户放弃项目')],
|
|
'状态机 E6:推进中/暂缓中 → 已关闭。')
|
|
|
|
transition_bru(9, '批量关闭.bru', '批量关闭', '/api/opportunity/close-batch',
|
|
[('ids','string[]','是','-','商机ID列表(逗号分隔)'),
|
|
('closeReason','string','否','-','关闭原因字典 code'),
|
|
('closeRemark','string','否','-','关闭备注')],
|
|
[('ids','1,2,3'),('closeReason','close_reason_01'),('closeRemark','客户放弃项目')],
|
|
'状态机 E6 批量版。每条独立事务。')
|
|
|
|
transition_bru(10, '重启商机.bru', '重启商机', '/api/opportunity/reopen',
|
|
[('id','string','是','-','商机ID'),
|
|
('reopenReason','string','否','-','重启原因')],
|
|
[('id','1'),('reopenReason','客户重新启动项目')],
|
|
'状态机 E7:已关闭 → 推进中。')
|
|
|
|
transition_bru(11, '移交商机.bru', '移交商机', '/api/opportunity/handover',
|
|
[('id','string','是','-','商机ID'),
|
|
('expectedOwnerUserId','string','是','-','期望的当前负责人ID(乐观并发校验,防脏移交)'),
|
|
('newOwnerUserId','string','是','-','新负责人用户ID'),
|
|
('ownerNameSnapshot','string','否','-','新负责人姓名快照'),
|
|
('ownerDeptId','string','否','-','新负责人部门ID快照')],
|
|
[('id','1'),('expectedOwnerUserId','1'),('newOwnerUserId','2'),('ownerNameSnapshot','李四'),('ownerDeptId','1')],
|
|
'状态机 E9:推进中/暂缓中自环换 owner。`expectedOwnerUserId` 提供乐观并发校验:与库中当前负责人不符则拒绝。原负责人保留在团队白名单(落入「我参与的」)。')
|
|
|
|
print('part1 done — 商机新建/详情/流转 完成')
|
|
|