Files
payg/payg.cfm
T
2026-09-01 15:28:17 +03:00

793 lines
41 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<cfsilent>
<cfimport prefix="m" taglib="lib"/>
<cfimport prefix="c" taglib="lib/controls"/>
<cfimport prefix="d" taglib="lib/data"/>
<cfimport prefix="layout" taglib="layout"/>
</cfsilent>
<m:prepare_ls entity="payg" accessObject="" pageInfoOut="pageInfo" trackOut="tr"/>
<!---
<cfparam name="dt_finish" type="date" default=#createDateTime(year(Now()),month(Now()),1,0,0,0)#/>
<cfparam name="dt_start" type="date" default=#dateAdd('m',-1,dt_finish)#/> --->
<cfparam name="dt_finish" type="date" default=#dateAdd('d',-1,createDateTime(year(Now()),month(Now()),1,0,0,0))#/>
<cfparam name="dt_start" type="date" default=#dateAdd('m',-1,createDateTime(year(Now()),month(Now()),1,0,0,0))#/>
<cfset hours=dateDiff('h', dt_start, dateAdd('d', 1, dt_finish))/>
<cfparam name="only_articles_in_spec" type="boolean" default=false/>
<cfparam name="wz" default=""/>
<cfparam name="code" default=""/>
<!--- *** коды лучше взять из каталога --->
<cfquery name="qCode" datasource="dwh" cachedWithin=#createTimeSpan(0, 0, 20, 0)#>
select distinct p.code
from elma.deals d
join elma.deals_services siv on (d.__id=ANY(siv.deal))
join elma.service_parametrs p on (p.__id = ANY(siv.params))
join elma.companies k on (d._companies=k.__id)
join elma.additional_agreements aa on (aa.__id = ANY(siv.additional_agreement))
join elma.aggrements a on (aa.contract_uid = a.__id)
where p.type=3 AND aa.is_actual
order by p.code
</cfquery>
<cfoutput>qCode:#getTickCount()-request.startTickCount#</cfoutput>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<!--- <cfdump var=#qCode#/> --->
<cfquery name="qContragent" datasource="dwh" cachedWithin=#createTimeSpan(0, 0, 22, 0)#>
select upper(trim(k.id_klienta)) as wz, k.__id::text as contragent_uid, k.__name as contragent,
c.__name as responsible, c.email as responsible_email, c.__status_status as responsible_status
from elma.companies k
LEFT OUTER JOIN elma.comp_users_ext c on (k.responsible=c.__id)
where k."__deletedAt" IS NULL
<cfif len(wz)>
AND k.id_klienta like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#wz#%"/>
</cfif>
</cfquery>
<cfoutput>qContragent:#getTickCount()-request.startTickCount#</cfoutput>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfquery name="qSpec" datasource="dwh" cachedWithin=#createTimeSpan(0, 0, 23, 0)#>
WITH
status(status, status_id, probability_pc)
AS (VALUES
('Закрыта неуспешно',7,0),('Закрыта успешно',6,100),('Договор подписан',5,100),('Договор на подписании',15,90),('Договор на согласовании',12,70),
('Тестирование',8,50),('Отправлено ТКП',10,20),('Внутреннее согласование',37,10),('Проработка решения',4,10),('Сбор потребностей',1,0)
)
select upper(trim(k.id_klienta)) as wz, k.__id::text as contragent_uid,
p.code,
SUBSTRING(p.code FROM 1 FOR LENGTH(p.code) - 2) as sku_code,
p.price, p.discount as component_discount, k.__name as contragent, a.__name as contract, a.__id::text as contract_uid, d.__name as deal, d.__id::text as deal_uid, siv.abstractive_service as abstract_service, siv.modifier, siv.user_description, siv.hash as line_key
,siv.date_nop as serviceStart, siv.date_end as serviceEnd, siv.discount as service_discount, siv.cnt as service_quantity
/*,p.__name as line_key*/, p.ed_izm, p.name as component, p.type, p.cnt*coalesce(siv.cnt,1) as component_quantity,
aa.__name as additional_agreement, aa.__id::text as additional_agreement_uid
--, aa.is_actual
,siv.end_customer as end_customer_uid
,z.id_klienta as end_customer_wz
,z.__name as end_customer_name
,ft.free_tier
,s.probability_pc
,siv.__id::text as siv_uid
from elma.deals d
JOIN STATUS s ON (d.__status_status = s.status_id)
join elma.deals_services siv on (d.__id=ANY(siv.deal))
--join elma.service_parametrs p on (p.__id = ANY(siv.params))
LEFT JOIN LATERAL unnest(siv.params) AS arr(param_id) ON TRUE
JOIN elma.service_parametrs p ON (p.__id = arr.param_id)
join elma.companies k on (d._companies=k.__id)
join elma.additional_agreements aa on (aa.__id = ANY(siv.additional_agreement))
join elma.aggrements a on (aa.contract_uid = a.__id)
left outer join elma.companies z on (siv.end_customer=z.__id)
left outer join (values ('paas.s3.ceph.vol-m',1),('paas.s3.ceph.get-m',10),('paas.s3.ceph.put-m',1),('paas.s3.ceph.trf-m',100)) as ft(code,free_tier) ON (p.code=ft.code)
where p.type IN (2,3) AND aa.is_actual
AND siv.date_nop < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_finish#/>
AND (siv.date_end > <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/> OR siv.date_end IS NULL)/*****/
AND aa."__deletedAt" IS NULL
AND a."__deletedAt" IS NULL
AND d."__deletedAt" IS NULL
AND siv."__deletedAt" IS NULL
AND p."__deletedAt" IS NULL
AND p.cnt <> 0 --похоже, только так можно отфильтровать паразитные строки с нулем
<cfif len(wz)>
AND k.id_klienta like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#wz#%"/>
</cfif>
<cfif len(code)>
AND p.code like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#code#%"/>
</cfif>
--AND k.id_klienta='WZ01031'
--AND k.id_klienta='WZ01112'
order by k.id_klienta, p.code
</cfquery>
<!--- select * from elma.deals_services where __id='019edf9c-c1e8-7acd-8b63-5369e5be5bf5'
union all
select * from elma.deals_services where __id='019edf9c-c23d-7ad6-a990-643dd09579b8' --->
<!--- здесь имеем список кодов с ценами и WZ. Каждому коду можно поставить в соответствие запрос, который даст по нему выборку. Изредка один запрос может вывести 2 кода, но разбить на 2 запроса легко. Кроме того, запросы можно объединить через union в один - может быть, так будет лучше читаться (быстрее выполняться не будет) --->
<cfoutput>qSpec:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfif isDefined("DEBUG")>
<cfdump var=#qSpec#/>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
</cfif>
<!--- siv means specification item version --->
<cfquery name="qSivAge" datasource="dwh" cachedWithin=#createTimeSpan(0, 0, 25, 0)#>
select max(dt_load) as dt_load from elma.deals_services
</cfquery>
<cfoutput>qSivAge:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfquery name="qComputing" datasource="billing-vc" cachedWithin=#createTimeSpan(0, 0, 25, 0)#>
select upper(trim(m.clientid)) as wz, vdc.name as vdc,
case
WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29'
WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31'
WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28'
WHEN vdc.name LIKE '%-v1cl5%' THEN 'iaas.ngc.a45'
WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28'
WHEN vdc.name LIKE '%-v1cl7%' THEN 'iaas.ngc.a40' -- *** смапить все кластера
else '['||vdc.name||']' end as code,
sum(cpu_used/cpu_speed)/4. as core_h,
sum(mem_used)/1024/4. as gb_h
from ngcloud_ru.capacity_resource
join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id
join ngcloud_ru.tenant t on (t.id=vdc.tenant_id)
LEFT JOIN LATERAL (
SELECT m.clientid
FROM ngcloud_ru.tenant_metadata tm
JOIN ngcloud_ru.metadata m ON tm.metadata_id = m.id
WHERE tm.tenant_id = t.id
ORDER BY tm.ts DESC, tm.metadata_id DESC
LIMIT 1
) m ON true
where length(m.clientid) > 0
AND capacity_resource.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/>
AND capacity_resource.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dateAdd("d",1,dt_finish)#/>
<cfif len(wz)>
AND m.clientid like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#wz#%"/>
</cfif>
group by m.clientid, vdc.name
order by m.clientid, vdc.name;
</cfquery>
<!--- recommended CREATE INDEX idx_tenant_metadata_tenant_ts_desc ON ngcloud_ru.tenant_metadata (tenant_id, ts DESC, metadata_id DESC); --->
<cfoutput>qComputing:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfif isDefined("DEBUG")>
<cfdump var=#qComputing#/>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
</cfif>
<cfquery name="qComputingAge" datasource="billing-vc" cachedWithin=#createTimeSpan(0, 0, 25, 0)#>
select max(capacity_resource.timestamp) as dt_load from ngcloud_ru.capacity_resource
</cfquery>
<cfoutput>qComputingAge:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<!--- переделать storage_profile_types с расшифровкой ssd, sata, можно прямо до каталожного кода --->
<cfquery name="qStorage" datasource="billing-vc" cachedWithin=#createTimeSpan(0, 0, 27, 0)#>
select upper(trim(m.clientid)) as wz, vdc.name as vdc,
case
WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29'
WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31'
WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28'
WHEN vdc.name LIKE '%-v1cl5%' THEN 'iaas.ngc.a45'
WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28'
WHEN vdc.name LIKE '%-v1cl7%' THEN 'iaas.ngc.a40' -- *** смапить все кластера
ELSE '['||vdc.name||']' END
|| CASE
WHEN storage_profile_types.name ILIKE '%FASTSSD%' THEN '.fstssd'
WHEN storage_profile_types.name ILIKE '%SSD%' THEN '.ssd'
WHEN storage_profile_types.name ILIKE '%SAS%' THEN '.sas'
WHEN storage_profile_types.name ILIKE '%SATA%' THEN '.sata'
ELSE '.['|| storage_profile_types.name ||']' END
as code,
storage_profile_types.name as profile,
sum("limit")/1024./4./<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/> as gb_m_limit, -- 4 потому, что сбор раз в 15 минут
sum(used)/1024./4./<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/> as gb_m_used
from ngcloud_ru.capacity_storage
join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id
join ngcloud_ru.tenant t on t.id=vdc.tenant_id
LEFT JOIN LATERAL (
SELECT m.clientid
FROM ngcloud_ru.tenant_metadata tm
JOIN ngcloud_ru.metadata m ON tm.metadata_id = m.id
WHERE tm.tenant_id = t.id
ORDER BY tm.ts DESC, tm.metadata_id DESC
LIMIT 1
) m ON true
join ngcloud_ru.storage_profile_types on capacity_storage.storage_profile_types_id=storage_profile_types.id
where length(m.clientid) > 0
AND capacity_storage.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/>
AND capacity_storage.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dateAdd("d",1,dt_finish)#/>
<cfif len(wz)>
AND m.clientid like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#wz#%"/>
</cfif>
group by m.clientid, vdc.name, storage_profile_types.name
order by m.clientid, vdc.name, storage_profile_types.name;
</cfquery>
<cfoutput>qStorage:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfif isDefined("DEBUG")>
<cfdump var=#qStorage#/>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
</cfif>
<cfquery name="qStorageAge" datasource="billing-vc" cachedWithin=#createTimeSpan(0, 0, 27, 0)#>
select max(capacity_storage.timestamp) as dt_load from ngcloud_ru.capacity_storage
</cfquery>
<cfoutput>qStorageAge:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<!---
"placement" "size_gb" "get_10k" "put_10k" "bytes_sent_gb"
HOT_FREE_LIMIT 1 10 1 100
--->
<!--- надо дисконтировать не ГБ-часы а ГБ-месяцы --->
<cfquery name="qS3Vol" datasource="billing-s3" cachedWithin=#createTimeSpan(0, 0, 29, 0)#>
select /* *** ниже дублирование кода агрегации - round ceil etc, обратить внимание при правке */
(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>) as vol_B
,sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>/1024/1024/1024 as vol_GB
,case
when bucket_stat.placement_id = 1 then 'paas.s3.ceph'
when bucket_stat.placement_id = 2 then 'paas.s3.cphc'
else '['|| TO_CHAR(bucket_stat.placement_id,'FM9') ||']' end as code
,bucket_stat.placement_id
,'WZ'||lpad(bucket_stat.owner, 5, '0') as wz -- *** здесь не число, а строка, поэтому другая формула обратной сборки WZ
,bucket_info.name as bucket
from s3billing.bucket_stat
join s3billing.placement ON s3billing.placement.id=bucket_stat.placement_id
join s3billing.bucket_info ON bucket_info.id=bucket_stat.bucket_id
where bucket_stat.placement_id in (1,2)
AND bucket_stat.timestamp_addition >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/>
AND bucket_stat.timestamp_addition < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dateAdd("d",1,dt_finish)#/>
<cfif len(wz)>
AND 'WZ'||lpad(bucket_stat.owner, 5, '0') like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#wz#%"/> /*выглядит очень неэффективным запросом*/
</cfif>
group by bucket_stat.owner, bucket_stat.placement_id, bucket_info.name
order by bucket_stat.owner, bucket_stat.placement_id, bucket_info.name;
</cfquery>
<cfoutput>qS3Vol:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfif isDefined("DEBUG")>
<cfdump var=#qS3Vol#/>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
</cfif>
<cfquery name="qS3VolAge" datasource="billing-s3" cachedWithin=#createTimeSpan(0, 0, 29, 0)#>
select max(timestamp_addition) as dt_load from s3billing.bucket_stat
</cfquery>
<cfoutput>qS3VolAge:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfquery name="qS3OpsTrf" datasource="billing-s3" cachedWithin=#createTimeSpan(0, 0, 30, 0)#>
select
sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
) as get
,(sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
)/10000) as get_10k
,sum(
CASE WHEN
(category.name LIKE 'put%'
OR category.name LIKE 'post%'
OR category.name LIKE 'patch%'
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
) as put
,(sum(
CASE WHEN
(category.name LIKE 'put%'
OR category.name LIKE 'post%'
OR category.name LIKE 'patch%'
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
)/10000) as put_10k
,(sum(bytes_sent)) as bytes_sent
,(sum(bytes_sent)/1024/1024/1024) as bytes_sent_GB
,case
when bucket_info.placement_id = 1 then 'paas.s3.ceph'
when bucket_info.placement_id = 2 then 'paas.s3.cphc'
else '['|| TO_CHAR(bucket_info.placement_id,'FM9') ||']' end as code --*** зачем делать там ключ bigint
,bucket_info.placement_id
--category.name
, bucket_info.name as bucket
,'WZ'||lpad(user_info.name, 5, '0') as wz
from s3billing.usage_bucket_by_user
join s3billing.user_info on usage_bucket_by_user.user_id=user_info.id
join s3billing.category on usage_bucket_by_user.category_id=category.id
join s3billing.bucket_info on usage_bucket_by_user.bucketid=bucket_info.id
join s3billing.placement on bucket_info.placement_id=placement.id
where 1=1
AND usage_bucket_by_user.time >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/>
AND usage_bucket_by_user.time < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dateAdd("d",1,dt_finish)#/>
<cfif len(wz)>
AND 'WZ'||lpad(user_info.name, 5, '0') like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#wz#%"/> /*выглядит очень неэффективным запросом*/
</cfif>
group by user_info.name, bucket_info.placement_id, bucket_info.name
order by user_info.name, bucket_info.placement_id, bucket_info.name;
</cfquery>
<cfoutput>qS3OpsTrf:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfif isDefined("DEBUG")>
<cfdump var=#qS3OpsTrf#/>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
</cfif>
<cfquery name="qS3OpsTrfAge" datasource="billing-s3" cachedWithin=#createTimeSpan(0, 0, 30, 0)#>
select max(time) as dt_load from s3billing.usage_bucket_by_user
</cfquery>
<cfoutput>qS3OpsTrfAge:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<!--- выданы права на схему и таблицы, чтобы без толку не плодить источники данных --->
<!--- считаются часы работы ВМ с GPU --->
<cfquery name="qGpu" datasource="billing-vc" cachedWithin=#createTimeSpan(0, 0, 31, 0)#>
WITH card (model, cluster, code) as (VALUES
('A100', 'msk1-pub02-i31', 'iaas.ngc.i31.gpu80'),
('H100', 'msk1-pub07-a40', 'iaas.ngc.a40.gpuh100'), -- h200 нет в отчете
('RTX 4090', 'msk1-pcs05-a45', 'iaas.ngc.a45.gpu4090'), -- нет такого в каталоге
('RTX 5090', 'msk1-pcs05-a45', 'iaas.ngc.a45.gpu5090'),
('RTX 6000', 'msk1-pub07-a40', 'iaas.ngc.a40.gpu6k'),
('nvidia_a16-16q', 'msk1-pub07-a40', 'iaas.ngc.a40.gpu16'),
('nvidia_a16-16q', 'msk1-pub01-i29', 'iaas.ngc.i29.gpu16'),
('nvidia_a16-1b', 'msk1-pub07-a40', 'iaas.ngc.a40.gpu1'),
('nvidia_a16-2q', 'msk1-pub07-a40', 'iaas.ngc.a40.gpu2'),
('nvidia_a16-4q', 'msk1-pub07-a40', 'iaas.ngc.a40.gpu4'),
('nvidia_rtx_pro_6000_blackwell_dc-4q', 'msk1-pub07-a40', 'iaas.ngc.a40.gpu6k4')
)
SELECT item.key AS gpu_model, c.code
,SUM(item.value::int*p.value)/12. AS gpu_h -- 5 minute sampling
,vm.obj_id, upper(trim(vm.client_id)) as WZ
FROM gpu.vm
CROSS JOIN LATERAL jsonb_each(vm.gpu) AS item(key, value)
LEFT OUTER JOIN card c ON (item.key=c.model)
JOIN gpu.vm_power_state p ON (vm.obj_uuid=p.obj_uuid)
WHERE NOT vm.is_template
AND p.ts >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dt_start#/>
AND p.ts < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dateAdd("d",1,dt_finish)#/>
GROUP BY vm.client_id, item.key, c.code, vm.obj_id
ORDER BY vm.client_id, item.key, c.code, vm.obj_id;
</cfquery>
<cfoutput>qGpu:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfif isDefined("DEBUG")>
<cfdump var=#qGpu#/>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
</cfif>
<cfquery name="qGpuAge" datasource="billing-vc" cachedWithin=#createTimeSpan(0, 0, 31, 0)#>
select max(dt_load) as dt_load from gpu.vm
</cfquery>
<cfoutput>qGpuAge:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfquery name="qUnifiedMetric" dbType="query">
select wz, code || '.vcpu' as code, core_h as raw_metric, core_h as metric, 'core-h' as unit, vdc as dimension1
from qComputing
<cfif len(code)>
where code || '.vcpu' like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#code#%"/>
</cfif>
union all
select wz, code || '.ram', gb_h as raw_metric, gb_h as metric, 'GByte-h' as unit, vdc as dimension1
from qComputing
<cfif len(code)>
where code || '.ram' like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#code#%"/>
</cfif>
union all
select wz, code, gb_m_used as raw_metric, gb_m_used as metric, 'GByte-m' as unit, vdc ||':'|| profile as dimension1
from qStorage
<cfif len(code)>
where code like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#code#%"/>
</cfif>
union all
select wz, code || '.vol', vol_b as raw_metric, vol_gb as metric, 'GByte-m' as unit, bucket as dimension1
from qS3Vol
<cfif len(code)>
where code || '.vol' like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#code#%"/>
</cfif>
union all
select wz, code || '.get', get as raw_metric, get_10k as metric, '10k' as unit, bucket as dimension1
from qS3OpsTrf
<cfif len(code)>
where code || '.get' like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#code#%"/>
</cfif>
union all
select wz, code || '.put', put as raw_metric, put_10k as metric, '10k' as unit, bucket as dimension1
from qS3OpsTrf
<cfif len(code)>
where code || '.put' like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#code#%"/>
</cfif>
union all
select wz, code || '.trf', bytes_sent as raw_metric, bytes_sent_gb as metric, 'GByte' as unit, bucket as dimension1
from qS3OpsTrf
<cfif len(code)>
where code || '.trf' like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#code#%"/>
</cfif>
union all
select wz, code, gpu_h as raw_metric, gpu_h as metric, 'h' as unit, obj_id as dimension1
from qGpu
<cfif len(code)>
where code like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#code#%"/>
</cfif>
order by wz, code
</cfquery>
<!--- <cfdump var=#qSpec#/>
<cfdump var=#qUnifiedMetric#/> --->
<cfoutput>qUnifiedMetric:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfif isDefined("DEBUG")>
<cfdump var=#qUnifiedMetric#/>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
</cfif>
<!--- загружаем рекордсеты в inmemory db, вместо чтобы мучиться с QoQ --->
<cfset db = new MemoryDbHelper()/>
<cfset db.register("qUnifiedMetric", qUnifiedMetric, "inmem")/>
<cfset db.register("qContragent", qContragent, "inmem")/>
<cfset db.register("qSpec", qSpec, "inmem")/>
<!--- Сопоставляем спецификации с метриками
В спецификации у нас есть контрагент (WZ) - это могут быть многие спецификации из многих договоров
проследить за версионностью спецификаций. в payg-report мы делаем отчет по дням, и для каждого дня проверяем актуальность спецификации для него
в метрике у нас только WZ контрагента (потому что нет объекта биллинга и даже контракта) и расшифрованный код без суффикса типа тарификации (который относится у нас не к продукту, а к цене)
FULL JOIN, потому что нам нужны бесхозные записи с той и другой стороны
При этом важно именно как мы обрабатываем бесхозные записи с каждой стороны
Они образуются за счет несовпадения кодов (нет в договоре - метрика есть, строчки нет. в том числе, если в договоре неправильный артикул)
Частичное совпадение кодов: есть фикс на тот же артикул. Показываем, но учитываем с нулевой ценой (потому что цена на фикс на другие единицы измерения, на месяц)
Отдельный вопрос: как мы обрабатываем Fixed PAYG и что с ним придумать
Контрагента со спекой нельзя INNER JOIN - есть вариант WZ с метрикой, но без спеки. В порядке ошибки может быть метрика без спеки и контрагента (orphan WZ). Либо осознанно записали вместо WZ что-то по своему усмотрению, благо там текстовое поле
Но нас тут совершенно не интересуют контрагенты без спек и метрик
Группировка: по контрагенту,
--->
<cfquery name="qCharge" datasource="inmem">
/*EXPLAIN PLAN FOR*/
SELECT
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
<d:field title="WZ">coalesce(coalesce(c.wz, m.wz), s.wz) as wz</d:field>
<d:field title="WZ spec">s.wz as wzs</d:field>
<d:field title="WZ contragent">c.wz as wzc</d:field>
<d:field title="WZ metric">m.wz as wzm</d:field>
<d:field title="WZ конечника">s.end_customer_wz as wze</d:field>
<d:field title="Артикул">coalesce(s.sku_code, m.code) as code</d:field>
<d:field title="Артикул по специф.">s.code as s_code</d:field><!--- содержит суффикс типа тарификации --->
<d:field title="SKU code по специф.">s.sku_code as sku_code</d:field><!--- без суффикса --->
<d:field title="Артикул по метрикам">m.code as m_code</d:field><!--- без суффикса --->
<d:field title="Кол-во компонента по спец." cfSqlType="CF_SQL_NUMERIC">s.component_quantity</d:field>
<d:field title="Кол-во сервиса по спец." cfSqlType="CF_SQL_NUMERIC">s.service_quantity</d:field>
<d:field title="Скидка на сервис" cfSqlType="CF_SQL_NUMERIC">s.service_discount</d:field>
<d:field title="Free Tier" cfSqlType="CF_SQL_NUMERIC">coalesce(s.free_tier,0) as free_tier</d:field>
<d:field>m.dimension1</d:field>
<d:field title="Тип по спец.">s.type</d:field>
<d:field title="Ед.изм.">m.unit</d:field>
<d:field title="Ед.изм.">s.ed_izm</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">coalesce(s.service_quantity*(100.-s.service_discount)/100.*s.price*(100.-s.component_discount)/100., 0.) as discounted_price</d:field>
<d:field title="Дата НОУ" cfSqlType="CF_SQL_TIMESTAMP">s.serviceStart</d:field>
<d:field title="Дата оконч." cfSqlType="CF_SQL_TIMESTAMP">s.serviceEnd</d:field>
<d:field title="Ключ строки">s.line_key</d:field>
<d:field title="Цена с НДС" cfSqlType="CF_SQL_NUMERIC">s.price</d:field>
<d:field title="Скидка % на компонент" cfSqlType="CF_SQL_NUMERIC">s.component_discount</d:field>
<d:field title="Сырая метрика" cfSqlType="CF_SQL_NUMERIC">m.raw_metric</d:field>
<d:field title="Приведенная метрика" cfSqlType="CF_SQL_NUMERIC">m.metric</d:field>
<d:field title="Контрагент">c.contragent</d:field>
<d:field title="contragent_uid">c.contragent_uid</d:field>
<d:field title="Ответственный">c.responsible</d:field>
<d:field title="Email отв-го">c.responsible_email</d:field>
<d:field title="Статус отв-го">c.responsible_status</d:field>
<d:field title="Договор">s.contract</d:field>
<d:field title="contract_uid">s.contract_uid</d:field>
<d:field title="Допник">s.additional_agreement</d:field>
<d:field title="additional_agreement_uid">s.additional_agreement_uid</d:field>
<d:field title="Сделка">s.deal</d:field>
<d:field title="deal_uid">s.deal_uid</d:field>
<d:field title="Абстрактная услуга">s.abstract_service</d:field>
<d:field title="Модификатор">s.modifier</d:field>
<d:field title="Компонент">s.component</d:field>
<d:field title="Клиентс. назв.">s.user_description</d:field>
<d:field title="Pricing Model">CASE s.type WHEN 1 THEN 'install' WHEN 2 THEN 'fix' WHEN 3 THEN 'payg' ELSE NULL END as pricing_model</d:field>
</d:field_set>
FROM qUnifiedMetric m
FULL JOIN qSpec s ON (m.wz=s.wz AND m.code=s.sku_code) -- сервис или компонент, но не цена с типом тарификации, суффикс типа тарификации отрезан
LEFT JOIN qContragent c ON ((coalesce(s.wz,m.wz))=(c.wz)) -- пробелы в составе WZ стоили нескольких часов работы
--(поскольку trim слева в джойне катастрофически замедляет работу, делаем чистку выше при формировании исходных резалтсетов)
WHERE 1=1
<cfif only_articles_in_spec>
AND EXISTS(select * from qSpec qs where qs.wz=m.wz AND qs.type=3)
<!--- вообще-то есть смысл добавить и все строки, где что-то намеряли --->
</cfif>
<cfif len(wz)>
AND (
m.wz like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#wz#%"/>
OR s.wz like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#wz#%"/>
OR c.wz like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#wz#%"/>
)
<!--- вообще-то есть смысл добавить и все строки, где что-то намеряли --->
</cfif>
--AND s.wz='WZ01031'
--AND s.wz='WZ01762
<!---
AND 1=0
--->
ORDER BY coalesce(coalesce(c.wz, m.wz), s.wz), coalesce(s.code, m.code)
</cfquery>
<!--- <cfdump var=#qCharge#/> --->
<cfoutput>qCharge:#getTickCount()-request.startTickCount#</cfoutput><cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
<cfif isDefined("DEBUG")>
<cfdump var=#qCharge#/>
<cfif !isDefined("output_xls") AND !isDefined("output_json")><cfflush/></cfif>
</cfif>
<cfif isDefined("output_xls")>
<layout:xml qRead=#qCharge# titleMap=#titleMap# filename="#pageInfo.entity#.xml"/>
<cfabort/>
</cfif><cfif isDefined("output_json")>
<layout:json qRead=#qCharge# titleMap=#titleMap# filename="#pageInfo.entity#.json"/>
<cfabort/>
</cfif><!---
---><layout:page section="header" pageInfo=#pageInfo#>
<layout:attribute name="title">
<cfoutput><b>Отчет по PAYG</b></cfoutput>
</layout:attribute>
<layout:attribute name="controls">
<!---skip filter link, filter is not implemented--->
<!---<layout:language_switch/>--->
</layout:attribute>
</layout:page>
Факт потребления по основной базе биллинга, спецификации из DWH. Без детализации по бакетам, VDC и т.д.
Все запросы кэшированы на 25 минут.<br/>
Внимание! округление вверх при расчете метрик get, put S3. Расчетные суммы могут содержать погрешности округления, поэтому окончательный расчет стоимости (количество * цену) должен выполняться в бухгалтерской программе.
<b>Если у клиента больше 1 действующего допника с услугами PAYG, то данные в отчете могут быть замножены, в этом случае считать вручную</b>
<br/>
Даты актуальности пока проверять вручную. Обращать внимание на замножение строк.
<cfoutput>
<form method="post" action="">
Период с <input type="text" name="dt_start" value="#dateFormat(dt_start,'YYYY-MM-DD')#"/>
по <input type="text" name="dt_finish" value="#dateFormat(dt_finish,'YYYY-MM-DD')#"/>
Только PAYG <input type="checkbox" name="only_articles_in_spec" value="1"<cfif only_articles_in_spec> checked</cfif>/>
WZ <input type="text" name="wz" value="#wz#"/>по вхождению, с регистром
Код <input type="text" name="code" value="#code#"/>по вхождению
<input type="submit" style="cursor:pointer;"/>
<input type="submit" name="DEBUG" value="DEBUG" style="cursor:pointer;"/><!--- #dateFormat( input, "yyyy-mm-dd" )#T#timeFormat( input, "HH:mm:ss.lXX" )# --->
</form>
Часов в периоде: <b>#hours#</b> Строк в отчете: <b>#qCharge.recordCount#</b><br/>
Актуальность данных:
VCD Computing: <b<cfif datediff("h",qComputingAge.dt_load, Now()) GT 24> class="err"</cfif>>>#dateFormat(qComputingAge.dt_load,'YYYY-MM-DD')# #timeFormat(qComputingAge.dt_load,'HH:mm:ss.lXX')#</b>
VCD Storage: <b<cfif datediff("h",qStorageAge.dt_load, Now()) GT 24> class="err"</cfif>>>#dateFormat(qStorageAge.dt_load,'YYYY-MM-DD')# #timeFormat(qStorageAge.dt_load,'HH:mm:ss.lXX')#</b>
S3 хранение: <b<cfif datediff("h",qS3VolAge.dt_load, Now()) GT 24> class="err"</cfif>>>#dateFormat(qS3VolAge.dt_load,'YYYY-MM-DD')# #timeFormat(qS3VolAge.dt_load,'HH:mm:ss.lXX')#</b>
S3 операции и трафик: <b<cfif datediff("h",qS3OpsTrfAge.dt_load, Now()) GT 24> class="err"</cfif>>>#dateFormat(qS3OpsTrfAge.dt_load,'YYYY-MM-DD')# #timeFormat(qS3OpsTrfAge.dt_load,'HH:mm:ss.lXX')#</b>
<br/>
<a href="#request.thisPage#?output_xls" title="экспорт в Excel" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/xls.gif" style="vertical-align:text-bottom;"/></a>
<a href="#request.thisPage#?output_json" title="экспорт в json" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/json.svg" style="vertical-align:text-bottom;" width="13" height="13"/></a>
</cfoutput>
<table class="worktable">
<thead>
<td width="1%"></td>
<!--- <th width="3%">WZ</th> --->
<th width="7%">Услуга</th>
<th width="7%">Артикул специф.</th>
<th width="5%">Кол-во специф.</th>
<th width="5%">Артикул метр.</th>
<th width="10%">Детализация</th>
<th width="5%">Кол-во метр.</th>
<th width="5%">Free Tier.</th>
<th width="5%">Опл. объем</th>
<th width="3%">Ед.изм.</th>
<th width="3%">Цена &#8381; со скид., с НДС</th>
<th width="3%">Стоимость &#8381; с НДС</th>
<td width="3%">&nbsp;&nbsp;&nbsp;</td>
<td width="10%">Компонент</td>
<td width="15%">Польз. наим.</td>
<td width="3%">GPL с НДС</td>
<td width="3%">Скидка%</td>
<td width="3%">Дата НОУ</td>
<td width="3%">Дата КОУ</td>
<td width="5%">Ключ строки</td>
<td width="3%">Сырая метрика</td>
<td width="3%">Приведен&shy;ная метрика</td>
<td width="3%">Цена &#8381; до ск. с НДС</td>
<td width="3%">Цена &#8381; co ск. с НДС без окр.</td>
<td width="3%">Service Quantity</td>
<td width="3%">Service Discount</td>
<td width="3%">Component Quantity</td>
<td width="3%">Component Discount</td>
<td width="5%">Договор</td>
<td width="10%">Допник</td>
<td width="10%">Сделка</td>
<td width="3%">Конечник</td>
</thead>
<cfoutput query="qCharge" group="wz">
<cfset var accum_cost_svc = 0/>
<cfset var accum_metric_svc = 0/>
<cfset var acc=0/>
<tr>
<td bgcolor="##ccc"></td>
<td bgcolor="##ccc" colspan="99">
<b>
<a href="contragent.cfm?__id=#contragent_uid#&#tr.fwx#">#wz#</a>
#contragent#
<a href="contragent_rpt.cfm?__id=#contragent_uid#&#tr.fwx#">(сводка)</a>
</b>
ответственный:
<span <cfif responsible_status NEQ 2> class="err"</cfif>>#responsible# <a href="mailto:#responsible_email#">#responsible_email#</a></span>
</td>
</tr>
<cfoutput group="code">
<cfoutput group="serviceStart">
<cfset accum_metric_svc += isNumeric(metric) ? metric : 0/>
<cfset accum_metric_dimension_1 = 0/>
<cfset current_price=0/>
<cfoutput><!--- dimensions --->
<cfif type EQ 3>
<cfset accum_metric_dimension_1 += isNumeric(metric) ? metric : 0/>
<cfset current_price=gaussianRound(discounted_price, 2)/>
</cfif>
<!--- детализация --->
<tr<cfif !len(unit)> class="gray"</cfif>>
<td>#currentRow#</a></td>
<td>#abstract_service#<cfif len(modifier)>: #modifier#</cfif></a></td>
<td><div<cfif type EQ 3> style="background:lightgreen;color:black;"</cfif>>#s_code#</div></td>
<td class=<cfif type EQ 3 AND component_quantity NEQ 1>"err r"<cfelse>"r"</cfif>>#component_quantity#</td>
<td>#m_code#</td>
<td>#dimension1#</td>
<td class="r" style="font-size:110%; padding:0 1em;">#request.roundSafe(metric,3)#</td>
<td class="r">#free_tier#</td>
<td></td>
<td style="font-size:90%;">#unit# <cfif len(ed_izm)>[#ed_izm#]</cfif></td><!--- #len(unit)?unit:'['& ed_izm &']'# --->
<td class="r">#gaussianRound(discounted_price,2)#</td>
<td class="r"></td>
<td></td>
<td>#component#</td>
<td>#user_description#</td>
<td class="r">#gaussianRoundSafe(price,2)#</td>
<td class="r">#component_discount#</td>
<td class="r">#dateFormat(serviceStart,'YYYY-MM-DD')#</td>
<td class="r">#dateFormat(serviceEnd,'YYYY-MM-DD')#</td>
<td class="r">#line_key#</td>
<td class="r" style="color:##bbb">#request.roundSafe(raw_metric,3)#</td>
<td class="r">#request.roundSafe(metric,3)#</td>
<!--- сырые исходные данные без округлений --->
<td class="r">#price#</td>
<td class="r">#discounted_price#</td><!--- style="word-wrap:break-word" --->
<td class="r">#service_quantity#</td>
<td class="r">#service_discount#</td>
<td class="r">#component_quantity#</td>
<td class="r">#component_discount#</td>
<td><a href="contract.cfm?__id=#contract_uid#&#tr.fwx#">#contract#</a></td>
<td><a href="additional_agreement.cfm?__id=#additional_agreement_uid#&#tr.fwx#">#additional_agreement#</a></td>
<td><a href="deal.cfm?__id=#deal_uid#&#tr.fwx#">#deal#</a></td>
<td>#WZE#</td>
</tr>
</cfoutput>
<cfset final_metric_dimension_1 = ceiling(accum_metric_dimension_1)/>
<cfset cost_dimension_1 = precisionEvaluate("gaussianRound(max(final_metric_dimension_1 - free_tier, 0)*current_price, 2)")/>
<cfset accum_cost_svc = precisionEvaluate("accum_cost_svc + cost_dimension_1")/><!--- простое сложение дает ошибку округления и лишние знаки, но и precisionEvaluate не выглядит слишком надежным --->
<tr>
<td colspan="4"></td>
<td colspan="2" class="b r">Итого #code#:</td>
<td class="b r" style="font-size:110%; padding:0 1em;"><cfif final_metric_dimension_1 NEQ 0 AND isNumeric(final_metric_dimension_1)>#round(final_metric_dimension_1,3)#</cfif></td>
<td class="b r">#free_tier#</td>
<td class="b r" style="font-size:120%; padding:0 1em;"><cfif final_metric_dimension_1 NEQ 0 AND isNumeric(final_metric_dimension_1)>#max(ceiling(final_metric_dimension_1) - free_tier, 0)#</cfif></td>
<td class="b c">#unit# <cfif len(ed_izm)>[#ed_izm#]</cfif></td>
<td class="b r">#current_price#</td>
<td class="b r">#gaussianRoundSafe(cost_dimension_1,2)#</td>
<td></td>
<td colspan="100"></td>
</tr>
</cfoutput><!--- serviceStart --->
</cfoutput><!--- code --->
<tr>
<td bgcolor="##eee" style="border-bottom:2px solid gray;"></td>
<td bgcolor="##eee" colspan="10" class="r b" style="border-bottom:2px solid gray;">Итого <a href="contragent.cfm?__id=#contragent_uid#&#tr.fwx#">#wz#</a> #contragent#:</td>
<td bgcolor="##eee" class="r b nw" style="border-bottom:2px solid gray; font-size:130%;">#accum_cost_svc#</td>
<td bgcolor="##eee" style="border-bottom:2px solid gray;"></td>
<td bgcolor="##eee" colspan="99" style="border-bottom:2px solid gray;"></td>
</tr >
</cfoutput><!--- wz --->
</table>
<!--- <cfdump var=#qCharge#/> --->
<cfdump var=#this.datasources[request.ds].connectionString#/>
<cfdump var=#this.datasources["billing-vc"].connectionString#/>
<cfdump var=#this.datasources["billing-s3"].connectionString#/>
<layout:page section="footer"/>
<!--- select * from s3billing.billing_per_user_fix_pl('2025-05-01 00:00:00','2025-06-01 00:00:00','1395'); --->
<!--- select t.id as tenant_id,
from ngcloud_ru.tenant t
left outer join
(select tenant_id, ts, metadata_id,
ROW_NUMBER() OVER (PARTITION BY tenant_id ORDER BY ts desc, metadata_id desc)
as rn
from ngcloud_ru.tenant_metadata) tm
on (t.id = tm.tenant_id AND tm.rn = 1)
left outer join ngcloud_ru.metadata m on (tm.metadata_id=m.id) --->
<cfscript>
string function gaussianRoundSafe(any num, numeric precision = 0, string default="") {
return (isNumeric(arguments.num)) ? gaussianRound(arguments.num, arguments.precision) : arguments.default;
}
function gaussianRound(numeric number, numeric precision = 0) {
var factor = 10 ^ precision;
var scaledNumber = number * factor;
var absScaled = abs(scaledNumber);
var sign = sgn(scaledNumber);
var floored = int(absScaled);
// If it's not ending in exactly .5, use standard rounding or precision scaling
if (absScaled - floored != 0.5) {
return round(number * factor) / factor;
}
// If it is exactly .5, round towards the nearest even number
if (floored % 2 != 0) {
// Nearest even is UP
return (floored + 1) * sign / factor;
} else {
// Nearest even is DOWN
return floored * sign / factor;
}
}
</cfscript>