unis_crm/sql/import_oms_existing_opportu...

508 lines
20 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

-- Import OMS existing project-space opportunities into CRM.
--
-- Source workbook: /Users/kangwenjing/Downloads/OMS存量项目空间研发转录CRM0423.xlsx
-- Source sheet: OMS已立项
--
-- Re-runnable behavior:
-- - crm_customer is reused by generated customer_code, or by customer_name + default owner/null owner.
-- - crm_opportunity is upserted by opportunity_code, which comes from OMS 项目ID.
-- - The Excel 售前ID/售前 columns are imported as pre_sales_id/pre_sales_name.
-- - owner_user_id is still required by crm_opportunity, so this script uses a default enabled user.
--
-- Mapping notes:
-- - 项目ID -> crm_opportunity.opportunity_code
-- - 项目名称 -> crm_opportunity.opportunity_name
-- - 最终客户名称/区域/行业 -> crm_customer
-- - 区域 -> crm_customer.province and crm_opportunity.project_location, normalized with 省/市/自治区 suffix
-- - 售前ID/售前 -> crm_opportunity.pre_sales_id/pre_sales_name
-- - 下单时间 -> crm_opportunity.expected_close_date
-- - 项目状态 = 已下单 -> stage/status won, archived true
-- - 项目状态 = 重点跟进 -> stage S4A when dictionary exists, otherwise business_negotiation
-- - 项目状态 = 商机 -> stage S0 when dictionary exists, otherwise initial_contact
--
-- If your SQL client reports "current transaction is aborted", run this first:
-- rollback;
--
-- This script intentionally avoids an explicit begin/commit so GUI SQL clients can
-- execute it statement-by-statement without getting stuck in an aborted transaction.
rollback;
drop table if exists tmp_oms_opportunity_import;
drop table if exists tmp_oms_area_map;
drop table if exists tmp_oms_default_owner;
drop table if exists tmp_oms_dict_values;
drop table if exists tmp_oms_stage_codes;
create temp table tmp_oms_opportunity_import (
row_no integer not null,
project_status varchar(50) not null,
project_id varchar(50) not null,
project_name varchar(200) not null,
confidence_pct varchar(1) not null,
customer_name varchar(200) not null,
province varchar(50),
industry varchar(50),
pre_sales_id bigint,
pre_sales_name varchar(100) not null,
amount numeric(18, 2) not null,
order_date date
) on commit preserve rows;
insert into tmp_oms_opportunity_import (
row_no,
project_status,
project_id,
project_name,
confidence_pct,
customer_name,
province,
industry,
pre_sales_id,
pre_sales_name,
amount,
order_date
) values
(2, '重点跟进', 'V001385', '贵州省公安厅云桌面增补项目', 'B', '贵州省公安厅', '贵州', '政府', 104, '陈畅', 71152, '2026-04-30'),
(3, '重点跟进', 'V001427', '低压电工上岗证鉴定中心设备采购项目', 'B', '东莞职业技术学院', '广东', '教育科研', 105, '杨钊', 80000, '2026-04-30'),
(4, '重点跟进', 'V001496', '江门市中心医院云终端设备的采购项目', 'B', '江门市中心医院', '广东', '医疗', 105, '杨钊', 500000, '2026-04-08'),
(5, '重点跟进', 'V001464', '兰州石化职业技术大学云桌面升级改造项目', 'B', '兰州石化职业技术大学', '甘肃', '教育科研', 128, '骆栋', 1600000, '2026-05-01'),
(6, '重点跟进', 'V001527', '池州职业技术学院云桌面', 'B', '池州职业技术学院', '安徽', '教育科研', 109, '韦东东', 2200000, null),
(7, '重点跟进', 'V001377', '马鞍山GA云桌面扩容', 'B', '马鞍山公安局', '安徽', '政府', 109, '韦东东', 100000, null),
(8, '重点跟进', 'V001454', '宁阳县职业中等专业学校云桌面续保2026', 'A', '宁阳县职业中等专业学校', '山东', '教育科研', 110, '魏光耀', 23850, '2026-05-01'),
(9, '重点跟进', 'V001434', '河北省电视台云桌面定制开发', 'C', '河北省电视台', '河北', '政府', 110, '魏光耀', 67200, '2026-04-01'),
(10, '重点跟进', 'V001474', '邯郸市邯山区阳光实验中学云桌面项目2026', 'A', '邯郸市邯山区阳光实验中学', '河北', '教育科研', 110, '魏光耀', 133000, '2026-04-01'),
(11, '重点跟进', 'V001446', '山东旅游职业学院云教室项目', 'C', '山东旅游职业学院', '山东', '教育科研', 110, '魏光耀', 529671, '2026-05-01'),
(12, '商机', 'V001253', '中国生物云桌面项目', 'B', '中国生物技术股份有限公司', '北京', '企业', 110, '魏光耀', 250000, '2026-10-01'),
(13, '商机', 'V001441', '中铁第五勘察设计院云桌面项目', 'B', '中铁第五勘察设计院集团有限公司', '北京', '企业', 110, '魏光耀', 92915, '2026-06-01'),
(14, '商机', 'V001365', '泰山玻璃纤维(太原)云桌面项目', 'B', '泰山玻璃纤维有限公司', '北京', '企业', 110, '魏光耀', 90276.5, '2026-04-03'),
(15, '商机', 'V001437', '北京生物国产化云桌面项目', 'C', '北京生物制品研究所有限责任公司', '北京', '企业', 110, '魏光耀', 91100, '2026-10-01'),
(16, '商机', 'V001445', '清华大学生命科学学院云桌面项目', 'B', '清华大学', '北京', '教育科研', 110, '魏光耀', 89032, '2026-06-01'),
(17, '重点跟进', 'V001521', '北京北重汽轮电机C3300终端扩容', 'A', '北京北重汽轮电机有限责任公司', '北京', '企业', 110, '魏光耀', 33600, '2026-04-30'),
(18, '重点跟进', 'V001507', '青岛物元半导体云桌面扩容2026', 'A', '物元半导体技术(青岛)有限公司', '山东', '企业', 110, '魏光耀', 217107, '2026-06-30'),
(19, '重点跟进', 'V001513', '东方物探云桌面扩容项目', 'A', '中国石油集团东方地球物理勘探有限责任公司', '北京', '电力能源', 110, '魏光耀', 69260.4, '2026-04-30'),
(20, '重点跟进', 'V001494', '天津国土资源和房屋职业学院云教室项目终端增补2026', 'A', '天津国土资源和房屋职业学院', '天津', '教育科研', 110, '魏光耀', 899000, '2026-06-30'),
(21, '重点跟进', 'V001530', '山东博源精密机械云桌面', 'B', '山东博源精密机械有限公司', '山东', '企业', 110, '魏光耀', 758400, '2026-06-30'),
(22, '重点跟进', 'V001153', '中建集团云桌面项目', 'A', '中国建筑集团有限公司', '北京', '企业', 110, '魏光耀', 1292280, '2026-06-30'),
(23, '重点跟进', 'V001166', '教育考试院云桌面项目', 'A', '教育部教育考试院', '北京', '教育科研', 110, '魏光耀', 84965, '2026-06-30'),
(24, '商机', 'V001313', '吉林移动云桌面扩容', 'A', '吉林移动', '吉林', '企业', 106, '杨坤融', 890000, '2026-06-12'),
(25, '重点跟进', 'V001399', '佳木斯大学云桌面项目', 'B', '佳木斯大学', '内蒙古', '教育科研', 105, '杨钊', 895210, '2026-05-15'),
(26, '商机', 'V001523', '雅江集团云桌面项目', 'C', '中国雅江集团有限公司', '四川', '企业', 105, '杨钊', 5000000, '2026-04-30'),
(27, '商机', 'V001515', '福建闽侯公安', 'B', '闽侯公安局', '福建', '政府', 130, '张锦杰', 150000, '2026-04-30'),
(28, '商机', 'V001435', '东亚银行Citrix桌面云国产化项目', 'B', '上海东亚银行', '上海', '金融', 130, '张锦杰', 300000, '2026-06-01'),
(29, '商机', 'V001522', '上海泽丰半导体云桌面项目', 'C', '上海泽丰半导体', '上海', '企业', 130, '张锦杰', 800000, '2026-06-01'),
(30, '商机', 'V001416', '海南移动自研1500点桌面云', 'C', '海南省移动', '海南', '企业', 130, '张锦杰', 4500000, '2026-06-30'),
(31, '重点跟进', 'V000001', '江西省余江区总医院人民医院项目', 'A', '江西省余江区总医院人民医院', '江西', '医疗', 130, '张锦杰', 11000, '2026-04-02'),
(32, '重点跟进', 'V001281', '厦门翔安机场口岸通关港口', 'C', '厦门翔安机场', '福建', '企业', 130, '张锦杰', 310000, null),
(33, '重点跟进', 'V001299', '抚州公安300点VDI利旧', 'A', '抚州市公安局', '江西', '政府', 130, '张锦杰', 300000, '2026-04-30'),
(34, '重点跟进', 'V001493', '江西新建区人民医院-二期扩容', 'B', '江西新建区人民医院', '江西', '医疗', 130, '张锦杰', 422830, '2026-05-15');
create temp table tmp_oms_default_owner (
owner_user_id bigint not null,
owner_source text not null
) on commit preserve rows;
create temp table tmp_oms_area_map (
raw_area varchar(50) not null primary key,
normalized_area varchar(50) not null
) on commit preserve rows;
create temp table tmp_oms_stage_codes (
initial_stage varchar(50) not null,
follow_stage varchar(50) not null
) on commit preserve rows;
create temp table tmp_oms_dict_values (
opportunity_type varchar(50) not null
) on commit preserve rows;
insert into tmp_oms_stage_codes (initial_stage, follow_stage)
values ('initial_contact', 'business_negotiation');
insert into tmp_oms_dict_values (opportunity_type)
values ('新建');
insert into tmp_oms_area_map (raw_area, normalized_area)
select distinct
province as raw_area,
case province
when '北京' then '北京市'
when '上海' then '上海市'
when '天津' then '天津市'
when '重庆' then '重庆市'
when '内蒙古' then '内蒙古自治区'
when '广西' then '广西壮族自治区'
when '西藏' then '西藏自治区'
when '宁夏' then '宁夏回族自治区'
when '新疆' then '新疆维吾尔自治区'
when '香港' then '香港特别行政区'
when '澳门' then '澳门特别行政区'
when '河北' then '河北省'
when '山西' then '山西省'
when '辽宁' then '辽宁省'
when '吉林' then '吉林省'
when '黑龙江' then '黑龙江省'
when '江苏' then '江苏省'
when '浙江' then '浙江省'
when '安徽' then '安徽省'
when '福建' then '福建省'
when '江西' then '江西省'
when '山东' then '山东省'
when '河南' then '河南省'
when '湖北' then '湖北省'
when '湖南' then '湖南省'
when '广东' then '广东省'
when '海南' then '海南省'
when '四川' then '四川省'
when '贵州' then '贵州省'
when '云南' then '云南省'
when '陕西' then '陕西省'
when '甘肃' then '甘肃省'
when '青海' then '青海省'
when '台湾' then '台湾省'
else province
end as normalized_area
from tmp_oms_opportunity_import
where province is not null
and btrim(province) <> '';
do $$
declare
uid_column text;
username_filter text := '';
status_filter text := '';
deleted_filter text := '';
default_owner_sql text;
invalid_confidence text;
duplicate_project_ids text;
begin
select string_agg(format('第%s行:%s', row_no, confidence_pct), ', ' order by row_no)
into invalid_confidence
from tmp_oms_opportunity_import
where confidence_pct not in ('A', 'B', 'C');
if invalid_confidence is not null then
raise exception 'OMS存量商机导入失败把握度仅支持 A/B/C异常数据%', invalid_confidence;
end if;
select string_agg(project_id, ', ' order by project_id)
into duplicate_project_ids
from (
select project_id
from tmp_oms_opportunity_import
group by project_id
having count(*) > 1
) duplicated;
if duplicate_project_ids is not null then
raise exception 'OMS存量商机导入失败项目ID重复无法按 opportunity_code 幂等导入:%', duplicate_project_ids;
end if;
if to_regclass('public.sys_user') is null then
raise exception 'OMS存量商机导入失败sys_user 表不存在,无法确定商机默认负责人 owner_user_id';
end if;
select case
when exists (
select 1
from information_schema.columns
where table_schema = 'public'
and table_name = 'sys_user'
and column_name = 'user_id'
) then 'user_id'
when exists (
select 1
from information_schema.columns
where table_schema = 'public'
and table_name = 'sys_user'
and column_name = 'id'
) then 'id'
else null
end
into uid_column;
if uid_column is null then
raise exception 'OMS存量商机导入失败sys_user 缺少 user_id/id 字段,无法写入 owner_user_id';
end if;
if exists (
select 1
from information_schema.columns
where table_schema = 'public'
and table_name = 'sys_user'
and column_name = 'status'
) then
status_filter := ' and coalesce(u.status, 1) = 1';
end if;
if exists (
select 1
from information_schema.columns
where table_schema = 'public'
and table_name = 'sys_user'
and column_name = 'is_deleted'
) then
deleted_filter := ' and coalesce(u.is_deleted, 0) = 0';
end if;
if exists (
select 1
from information_schema.columns
where table_schema = 'public'
and table_name = 'sys_user'
and column_name = 'username'
) then
username_filter := 'case when btrim(coalesce(u.username::text, '''')) = ''admin'' then 0 else 1 end,';
end if;
default_owner_sql := format(
'insert into tmp_oms_default_owner (owner_user_id, owner_source)
select u.%1$I::bigint, ''default sys_user for OMS import''
from public.sys_user u
where u.%1$I is not null%2$s%3$s
order by %4$s u.%1$I asc
limit 1',
uid_column,
status_filter,
deleted_filter,
username_filter
);
execute default_owner_sql;
if not exists (select 1 from tmp_oms_default_owner) then
raise exception 'OMS存量商机导入失败未找到可用的默认商机负责人请先确认 sys_user 中存在启用用户';
end if;
end $$;
do $$
begin
if to_regclass('public.sys_dict_item') is not null then
execute $sql$
update tmp_oms_stage_codes
set initial_stage = coalesce((
select s.item_value
from public.sys_dict_item s
where s.type_code = 'sj_xmjd'
and s.item_value = 'S0'
and coalesce(s.status, 1) = 1
and coalesce(s.is_deleted, 0) = 0
limit 1
), initial_stage),
follow_stage = coalesce((
select s.item_value
from public.sys_dict_item s
where s.type_code = 'sj_xmjd'
and s.item_value = 'S4A'
and coalesce(s.status, 1) = 1
and coalesce(s.is_deleted, 0) = 0
limit 1
), follow_stage)
$sql$;
execute $sql$
update tmp_oms_dict_values
set opportunity_type = coalesce((
select s.item_value
from public.sys_dict_item s
where s.type_code = 'sj_jslx'
and s.item_label = '新建'
and coalesce(s.status, 1) = 1
and coalesce(s.is_deleted, 0) = 0
order by s.sort_order asc nulls last
limit 1
), opportunity_type)
$sql$;
end if;
if to_regclass('public.cnarea') is not null then
execute $sql$
update tmp_oms_area_map m
set normalized_area = coalesce((
select c.name
from public.cnarea c
where c.level = 1
and (
c.name = m.raw_area
or c.short_name = m.raw_area
or replace(c.name, '', '') = m.raw_area
or replace(replace(replace(replace(replace(c.name, '', ''), '自治区', ''), '壮族', ''), '回族', ''), '维吾尔', '') = m.raw_area
)
order by c.area_code asc, c.id asc
limit 1
), m.normalized_area)
$sql$;
end if;
end $$;
with customer_source as (
select distinct on (t.customer_name, d.owner_user_id)
'OMS-CUS-' || substr(md5(btrim(t.customer_name) || ':' || d.owner_user_id::text), 1, 16) as customer_code,
t.customer_name,
t.industry,
coalesce(am.normalized_area, t.province) as province,
d.owner_user_id
from tmp_oms_opportunity_import t
left join tmp_oms_area_map am on am.raw_area = t.province
cross join tmp_oms_default_owner d
order by t.customer_name, d.owner_user_id, t.row_no
)
insert into crm_customer (
customer_code,
customer_name,
customer_type,
industry,
province,
owner_user_id,
source,
status,
remark,
created_at,
updated_at
)
select
cs.customer_code,
cs.customer_name,
null,
cs.industry,
cs.province,
cs.owner_user_id,
'OMS存量导入',
'following',
'来自 OMS 存量项目空间研发转录CRM0423',
now(),
now()
from customer_source cs
where not exists (
select 1
from crm_customer c
where btrim(c.customer_name) = btrim(cs.customer_name)
and (c.owner_user_id = cs.owner_user_id or c.owner_user_id is null)
)
on conflict (customer_code) do update
set customer_name = excluded.customer_name,
industry = coalesce(nullif(crm_customer.industry, ''), excluded.industry),
province = coalesce(nullif(crm_customer.province, ''), excluded.province),
owner_user_id = coalesce(crm_customer.owner_user_id, excluded.owner_user_id),
source = coalesce(nullif(crm_customer.source, ''), excluded.source),
updated_at = now();
insert into crm_opportunity (
opportunity_code,
opportunity_name,
customer_id,
owner_user_id,
project_location,
operator_name,
amount,
expected_close_date,
confidence_pct,
stage,
opportunity_type,
product_type,
source,
sales_expansion_id,
channel_expansion_id,
pre_sales_id,
pre_sales_name,
competitor_name,
archived,
archived_at,
pushed_to_oms,
oms_push_time,
description,
status,
created_at,
updated_at
)
select
t.project_id,
t.project_name,
customer_match.id,
d.owner_user_id,
coalesce(am.normalized_area, t.province),
null,
t.amount,
t.order_date,
t.confidence_pct,
case
when t.project_status = '已下单' then 'won'
when t.project_status = '重点跟进' then sc.follow_stage
else sc.initial_stage
end,
dv.opportunity_type,
'VDI云桌面',
'OMS存量导入',
null,
null,
t.pre_sales_id,
t.pre_sales_name,
null,
t.project_status = '已下单',
case when t.project_status = '已下单' then coalesce(t.order_date::timestamptz, now()) else null end,
true,
now(),
concat_ws(E'\n',
'OMS存量项目空间研发转录CRM0423导入',
'原项目状态:' || t.project_status,
'原项目ID' || t.project_id
),
case when t.project_status = '已下单' then 'won' else 'active' end,
now(),
now()
from tmp_oms_opportunity_import t
left join tmp_oms_area_map am on am.raw_area = t.province
cross join tmp_oms_default_owner d
cross join tmp_oms_stage_codes sc
cross join tmp_oms_dict_values dv
join lateral (
select c.id
from crm_customer c
where c.customer_code = 'OMS-CUS-' || substr(md5(btrim(t.customer_name) || ':' || d.owner_user_id::text), 1, 16)
or (
btrim(c.customer_name) = btrim(t.customer_name)
and (c.owner_user_id = d.owner_user_id or c.owner_user_id is null)
)
order by
case
when c.customer_code = 'OMS-CUS-' || substr(md5(btrim(t.customer_name) || ':' || d.owner_user_id::text), 1, 16) then 0
when c.owner_user_id = d.owner_user_id then 1
else 2
end,
c.id
limit 1
) customer_match on true
on conflict (opportunity_code) do update
set opportunity_name = excluded.opportunity_name,
customer_id = excluded.customer_id,
owner_user_id = excluded.owner_user_id,
project_location = excluded.project_location,
amount = excluded.amount,
expected_close_date = excluded.expected_close_date,
confidence_pct = excluded.confidence_pct,
stage = excluded.stage,
opportunity_type = excluded.opportunity_type,
product_type = excluded.product_type,
source = excluded.source,
pre_sales_id = excluded.pre_sales_id,
pre_sales_name = excluded.pre_sales_name,
archived = excluded.archived,
archived_at = excluded.archived_at,
pushed_to_oms = excluded.pushed_to_oms,
oms_push_time = coalesce(crm_opportunity.oms_push_time, excluded.oms_push_time),
description = excluded.description,
status = excluded.status,
updated_at = now();
select
count(*) as imported_rows,
count(*) filter (where project_status = '已下单') as won_rows,
count(*) filter (where project_status <> '已下单') as active_rows,
sum(amount) as total_amount
from tmp_oms_opportunity_import;
select
owner_user_id as default_owner_user_id,
owner_source
from tmp_oms_default_owner;