# -*- 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 — 商机新建/详情/流转 完成')