Compare commits

...
21 Commits
Author SHA1 Message Date
smishchuk f1c09cd1f8 042 IAM_URL env 2026-10-28 13:02:03 +03:00
smishchuk f94e399cf8 041 inmem decimal fix, -m workaround 2026-10-02 00:29:21 +03:00
smishchuk 00977e66cd 41 memendger 2026-09-30 20:12:35 +03:00
smishchuk 360298213a 248 is_trial 2026-09-26 14:10:32 +03:00
smishchuk cd4cdaf7f7 040 iam url to prod 2026-09-25 13:26:41 +03:00
smishchuk 86c0787ee0 039 MemoryDbHepler 0.03 precision fix 2026-09-22 16:32:29 +03:00
smishchuk a8c2a65227 038 wz exceptio bugfix 2026-09-21 11:29:12 +03:00
smishchuk 47f9ecc1de 037 exceptions wz 2026-09-18 14:23:45 +03:00
smishchuk e5dae89a1b 036 left join for json, exceptions 2026-08-28 13:16:10 +03:00
smishchuk 9ed9e7fb83 035 TZ europe/moscow 2026-08-25 15:03:36 +03:00
smishchuk dcbc5174c8 033 computetion debug and optimization 2026-08-19 14:50:58 +03:00
smishchuk f9caae3045 json.cfm fix 2026-08-17 13:29:02 +03:00
smishchuk 91b6687220 071 deep debug 2026-08-14 18:07:24 +03:00
smishchuk 5401b7e96d 030 s3vol critical fix 2026-07-30 16:02:05 +03:00
smishchuk 0985d285b9 029 additional fields at root 2026-07-29 13:11:52 +03:00
smishchuk bf964bde34 027 query optimized 2026-07-28 13:08:12 +03:00
smishchuk e8df23c2e2 026 ceiling 2026-07-28 12:38:02 +03:00
smishchuk 849a4c2796 024 improvements 2026-07-26 23:04:05 +03:00
smishchuk f7421c1155 023 sslCertificateInstall 2026-07-20 20:31:27 +03:00
smishchuk 9e0fdd7b7b 022 connection diagnostics 2026-07-20 18:26:30 +03:00
smishchuk 3bc7cea1d1 021 dimensions dimension1 2026-07-20 11:06:58 +03:00
23 changed files with 2837 additions and 612 deletions
-11
View File
@@ -1,11 +0,0 @@
<hr/>
test
<hr/>
<cfflush/>
<!--- <cftry>
<cfdump var=#session#/>
<cfcatch type="any"><cfdump var=#cfcatch#/></cfcatch>
</cftry> --->
<cfmodule template="mod/crm_contract.cfm" wz="WZ01348"/>
<cfdump var=#qComponent#/>
+139 -27
View File
@@ -8,6 +8,7 @@
<!--- Set up the application. --->
<cfset this.Name = "PAYG-REPORT" />
<cfset this.applicationTimeout = createTimeSpan( 0, 3, 0, 0 ) />
<cfset this.requestTimeout = createTimeSpan( 0, 0, 2, 0 )/>
<cfset this.sessionmanagement="Yes"/>
<cfset this.clientmanagement="No"/>
<cfset this.sessiontimeout=CreateTimeSpan(0, 1, 0, 0)/>
@@ -15,6 +16,7 @@
<cfset this.mappings = structNew() />
<cfset this.mappings["/mod"] = getDirectoryFromPath(getCurrentTemplatePath()) & "mod/" />
<cfset this.customTagPaths = expandPath(getDirectoryFromPath(getCurrentTemplatePath()) & "mod")/>
<cfset this.timeZone = "Europe/Moscow"/><!--- для биллинга очень важен часовой пояс, поэтому ставим вручную в коде --->
<!--- <cfset this.nullSupport = true/> --->
<!--- SSO --->
@@ -24,33 +26,65 @@
<cfset this.access_token_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token" />
<cfset this.idpCertUrl = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs" />
<cfset this.client_secret = createObject("java", "java.lang.System").getEnv("IDP_CLIENT_SECRET")/>
<cfset this.iam_url = "https://auth-api.ngcloud.ru/api/v1/auth/user"/>
<!--- <cfset this.iam_url = "https://auth-api.ngcloud.ru/api/v1/auth/user"/> --->
<cfset this.iamServiceUrl = "https://auth-api.ngcloud.ru/api/v1/auth/user"/>
<cfset this.iamServiceUrl = getFromEnv("IAM_URL", "https://auth-api.ngcloud.ru/api/v1/auth/user")/>
<cfset this.redirect_uri = "https://#CGI.SERVER_NAME##CGI.SCRIPT_NAME#"/><!--- куда возвращаться от IDP --->
<cfset this.crm_cache_host = "elma-cache.adl.nubes.ru"/><!--- *** почему-то приходится устанавливать серт sslCertificateInstall, иначе не коннектится --->
<cfset request.crm_cache_base = "https://#this.crm_cache_host#/contract_ls.cfm"/>
<cfset this.datasource = "rpt"/><!--- к буферной базе --->
<cfset this.datasource = "rpt"/><!--- к буферной базе биллинга --->
<cfset this.defaultdatasource = this.datasource/>
<!--- request scope is available in pseudoconstructor
see also https://www.bennadel.com/blog/1437-coldfusion-scope-existence-during-various-request-types-and-events.htm --->
<cfset request.DS = "#this.datasource#"/>
<cfset request.language=""/>
<cfset getDS(this.datasource)/>
<cfset this.datasources["inmem"] = {
class: "org.hsqldb.jdbcDriver",
// Внимание на jdbc:hsqldb:mem:
connectionString: "jdbc:hsqldb:mem:lucee_mem;shutdown=false",
username: "sa",
password: "",
// Защита от "висяка" и исчезновения базы:
// заставляем Lucee держать минимум 1 коннект открытым всегда
connectionLimit: 5,
connectionMin: 1
}
/><!--- inmemory DB --->
<!--- кажется, нужно инициализировать датасорцы в псевдоконструкторе - onRequest не получается --->
<!--- *** проверить, что есть - если есть, не создавать --->
<cffunction
<cffunction
name="OnApplicationStart"
access="public"
returntype="boolean"
output="false"
output="false"
hint="Fires when the application is first created.">
<!--- *** Lucee specific function --->
<cfset sslCertificateInstall(host="elma-cache.adl.nubes.ru", port=443)/><!--- ************************ duplicate address --->
<!--- *** мы не проверяем срок жизни сертификата и его отзыв--->
<!--- Obtain idp certificate OnApplicationStart --->
<cfset application.idpCertificate = getIdpCertificate(this.idpCertUrl)/>
<!--- <cfset this.iamServiceUrl = locateIamService()/> --->
<!--- <cfreturn super.onApplicationStart() /> --->
<cfreturn true />
</cffunction>
<cffunction name="onRequestStart" returnType="boolean">
<cfargument name="targetPage" type="string" required="true" />
<cfif structKeyExists(url, "application_reset")>
<cfset ApplicationStop() />
<cflocation url="index.cfm" addtoken="false" />
</cfif>
<cfreturn true />
</cffunction>
<cffunction
@@ -71,22 +105,17 @@
<cfset request.PERMISSION_READ=1/>
<cfset request.PERMISSION_WRITE=2/>
<!--- <cfset request.UNDEFINED_USR_ID=-1/>
<cfset request.ANONYMOUS_USR_ID=2/>
<cfset request.GUEST_USR_ID=3/> --->
<!--- global settings --->
<cfset request.RECORDS_PER_PAGE=500/>
<cfset request.APP_VERSION="0.00.020"/>
<!--- *************************************************************************************************************** --->
<cfset request.APP_VERSION="0.00.042"/><!--- ************************************************************************ --->
<!--- *************************************************************************************************************** --->
<cfheader name="X-Application-Version" value=#request.APP_VERSION#/>
<cfset request.STAND=getStand()/>
<cfset request.RECORDS_PER_PAGE=500/>
<!--- application constants --->
<!---<cfset request.SMTP_SERVER="172.16.16.16"/>
<cfset request.MAIL_FROM_FOR_NOTIFICATION="Pipeline tracker <pipeline@mail.ru>"/>
<cfset request.SUBJECT_PREFIX="[PIPELINE TRACKER] "/>--->
<!---<cfset request.BASE_URL="https://smtest.dtln.local/pipeline/"/>*** сделать нормально. Внимание! При наличии прокси CGI.SERVER_NAME может быть заменено --->
<cfset local = {} />
@@ -138,8 +167,8 @@
--->
<!--- <cfoutput>onRequest:check for cert started #(getTickCount()-request.startTickCount)#</cfoutput> --->
<cfset var idpCertificateExists = false/>
<cflock scope="application" type="readonly" timeout=1>
<cfset request.APP_NAME=this.Name/>
<cflock scope="application" type = "readonly" timeout=1>
<cfset request.APP_NAME = this.Name/>
<cfset idpCertificateExists = structKeyExists(application,"idpCertificate")/>
</cflock>
@@ -176,16 +205,33 @@
Кладем данные из токена в реквест и наслаждаемся --->
<!--- Вариант: если есть Authorization:bearer, идем с ним к IAM --->
<cfscript>
<cfscript>
var requestData = GetHttpRequestData();
var headers = requestData.headers;
var local_token = false; //для отладки мы можем использовать токен из окружения, но когда он истекает, это легко упустить и потом искать проблему
//var authorizationHeader = server.system.environment?.AUTHORIZATION;
if (structKeyExists(headers,"Authorization")) {
request.auth_header = headers.Authorization;
//writedump(headers.Authorization);
} else {
//request.auth_header = createObject("java", "java.lang.System").getEnv("AUTHORIZATION");
request.auth_header = server.system.environment?.AUTHORIZATION; //так короче
local_token = true;
if (isNull(request.auth_header)){ /*for local debug we can inject long-living token into environment*/
structDelete(request,"auth_header");
} else {
request.auth_header = "Bearer #request.auth_header#";
}
}
if (structKeyExists(headers,"authorization")) { // аутентификация для клиента ЛК
var authorizationHeader = headers.authorization;
// if (structKeyExists(headers,"authorization")) { // аутентификация для клиента ЛК
if (structKeyExists(request,"auth_header")) { // аутентификация для клиента ЛК
var authorizationHeader = request.auth_header;
if (authorizationHeader.startsWith("Bearer ")) {
var bearerToken = Mid(authorizationHeader, 8);
writeDump(bearerToken);
//writeDump(bearerToken);
//var token=toString(binaryDecode(bearerToken, "base64"));
//writeDump(token);
//jwt = new lib.jwt(this.client_secret);
@@ -194,7 +240,7 @@
//}
var result="";
//try {
var iamService = new http(method = "GET", charset = "utf-8", url = #this.iam_url#, timeout="5");
var iamService = new http(method = "GET", charset = "utf-8", url = #this.iamServiceUrl#, timeout="5");
iamService.addParam(type = "HEADER", name = "Accept", value = "application/json");
iamService.addParam(type = "HEADER", name = "Authorization", value = "#authorizationHeader#"); //passthrough
//writedump(this.iamServiceUrl);abort;
@@ -205,14 +251,17 @@
if (prefix.status_code NEQ 200) {
//writedump(this.iamServiceUrl);
//writedump(resp);
//abort;
var iamStatusCode = (isValid("integer", prefix.status_code)) ? val(prefix.status_code) : 500;
throw(message="IAM error", detail="URL: #this.iam_url# Status Code: #prefix.status_code#");
throw(message="IAM error", detail="local_token: #local_token# URL: #this.iamServiceUrl# Status Code: #prefix.status_code#");
//abort;
//throw("IDP response not OK");
}
result = prefix.filecontent;
var idpUserData=deserializeJson(result);
//request.auth.wz=idpUserData.userInfo.ClientId;//GUID!
request.auth.wz=idpUserData.userInfo.ClientId;
//writeDump(idpUserData);
request.auth.login = idpUserData.userInfo.login;
@@ -313,7 +362,16 @@
// после возвращения от IDP нам нужно будет зачистить сессию SSO, сделаем это по слову logout
}
request.auth.wz='WZ01348'; //*******************
//request.auth.wz='WZ01348'; //*******************
//request.auth.wz='WZ01005'; //*******************
//request.auth.wz='WZ01653'; //*******************
//request.auth.wz='WZ02319'; //*******************
//request.auth.wz='WZ01762'; //*******************
//writedump(request.auth);abort;
//********************
//костыль
if (structKeyExists(URL,"wz")) { request.auth.wz=URL.wz }
//*********************
</cfscript>
@@ -392,7 +450,25 @@
<cfreturn ""/>
</cffunction>
<cffunction name="getFromEnv"
access="private"
returntype="string"
output="true"
hint="Obtain value from environment, if not found then return default">
<cfargument name="name" type="string" required="true"/>
<cfargument name="default" type="string" required="true"/>
<cftry>
<cfset var value = createObject("java", "java.lang.System").getEnv(arguments.name)/>
<cfreturn isNull(value)? arguments.default : value/>
<cfcatch>
<cfreturn arguments.default/>
</cfcatch>
</cftry>
</cffunction>
<cffunction
name="getDS"
access="private"
@@ -519,4 +595,40 @@
</cftry>
<cfthrow type="Context validation error" message="RETHROW() called outside TRY-CATCH"/>
</cffunction>
<!--- как определить стенд? Из персистенса. Из окружения --->
<!--- <cffunction name="locateIamService"><!--- *** архитектурный колхоз --->
<!--- почему-то мы не видим других функций, когда нас вызывают onApplicationStart. С другой стороны, а почему вообще должны вызывать без перезагрузки приложения --->
<cfset var stand=""/>
<!--- дублируем код, потому что getStand отсюда почему-то не виден --->
<cftry>
<cfquery name=qConfig>
select value as stand from config
where name='STAND'
</cfquery>
<cfset stand = qConfig.stand/>
<cfswitch expression=#stand#>
<cfcase value=",dev">
<cfreturn "https://auth-api-dev.ngcloud.ru/api/v1" />
</cfcase>
<cfcase value="test">
<cfreturn "https://auth-api-test.ngcloud.ru/api/v1" />
</cfcase>
<cfcase value="prod">
<cfreturn "https://auth-api.ngcloud.ru/api/v1" />
</cfcase>
<cfdefaultcase><!--- proceed to the legacy locator ---></cfdefaultcase>
</cfswitch>
<cfcatch type="ANY">
<!--- do nothing, proceed to the legacy locator --->
<cfrethrow/>
<cfthrow message="IAM service unavailable" detail="there is no IAM for stand #stand# defined"/>
</cfcatch>
</cftry>
</cffunction> --->
</cfcomponent>
+10
View File
@@ -0,0 +1,10 @@
pvdc_map(pvdc,code) as (VALUES
('v1cl1-vsan-pvdc',''),
('v1cl2-pvdc',''),
('v2cl2-1c-pvdc',''),
('v1cl4-vsan-pvdc',''),
('v1cl6-vsan-pvdc',''),
('v1cl7-vsan-pvdc',''),
('hystax-pvdc',''),
('','')
)
+205
View File
@@ -0,0 +1,205 @@
DROP FOREIGN TABLE IF EXISTS vmreports.compute CASCADE;
DROP FOREIGN TABLE IF EXISTS vmreports.storage CASCADE;
DROP FOREIGN TABLE IF EXISTS vmreports.host CASCADE;
DROP FOREIGN TABLE IF EXISTS vmreports.vm_gpu CASCADE;
DROP FOREIGN TABLE IF EXISTS vmreports.vm_power_state CASCADE;
--------------------------------------------------------------------------------
CREATE SCHEMA IF NOT EXISTS vmreports;
ALTER SCHEMA vmreports OWNER TO dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi,appserver,reader,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver,reader;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT, REFERENCES ON TABLES TO appserver,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT ALL ON TABLES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi, appserver;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- TOC entry 721 (class 1259 OID 9816992)
-- Name: compute; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.compute (
"Name" character varying(255),
"UUID_VM" character varying(255),
"VDC" character varying(255),
"Organization_UUID" uuid,
"Organization" character varying(255),
"WZ" character varying(255),
"Cluster" character varying(255),
"Host" character varying(255),
"Host_Model" character varying(255),
"CPU_Model" character varying(255),
"Host_CPU_Cores" double precision,
"Host_CPU_Sockets" double precision,
"Host_CPU_Speed" double precision,
"Host_RAM" double precision,
"Host_CPU_Used" double precision,
"Host_CPU_Used_Avg" double precision,
"VDC_CPU_Reserv" double precision,
"VDC_CPU_Speed" double precision,
"VM_vCPU_count" double precision,
"VM_CPU_Used" double precision,
"VM_CPU_Used_AVG" double precision,
"VM_CPU_Reserv_GHz" double precision,
"VM_CPU_Reserv_perc" double precision,
"VM_RAM_Allocated" double precision,
"VM_RAM_Used" double precision,
"VM_is_ON" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.compute OWNER TO dbo;
--
-- TOC entry 722 (class 1259 OID 9817004)
-- Name: storage; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.storage (
"Name" character varying(255),
"VM_UUID" character varying(255),
"VDC" character varying(255),
"Organization_UUID" uuid,
"Organization" character varying(255),
"WZ" character varying(255),
"Cluster" character varying(255),
"Disk" character varying(255),
"Storage_Policy" character varying(255),
"Disk_Allocated" double precision,
"Disk_Used" double precision,
"VM_is_ON" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.storage OWNER TO dbo;
--
-- TOC entry 720 (class 1259 OID 9816984)
-- Name: host; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.host (
"Name" character varying(255),
"Model" character varying(255),
"Cluster" character varying(255),
"Maintenance" character varying(255),
"CPU_Cores" double precision,
"CPU_Sockets" double precision,
"CPU_Speed" double precision,
"RAM_Total" double precision,
"RAM_Used" double precision,
"CPU_Used" double precision,
"CPU_Used_AVG" double precision,
"CPU_Contention_95" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
"Cluster_v2" character varying(255),
"HT_Active" boolean,
"CPUtoVM" double precision,
"RAMtoVM" double precision
);
ALTER TABLE vmreports.host OWNER TO dbo;
--
-- TOC entry 733 (class 1259 OID 11093092)
-- Name: vm_gpu; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.vm_gpu (
"Name" character varying(255),
"ClientID" character varying(255),
"VCD" character varying(255),
"Org" character varying(255),
"OrgVDC" character varying(255),
"vCenter" character varying(255),
"GPU" jsonb,
"OrgStatus" character varying(255),
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.vm_gpu OWNER TO dbo;
--
-- TOC entry 732 (class 1259 OID 11093087)
-- Name: vm_power_state; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.vm_power_state (
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
value double precision
);
ALTER TABLE vmreports.vm_power_state OWNER TO dbo;
--
-- TOC entry 4761 (class 2606 OID 9816998)
-- Name: compute pk_compute; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.compute
ADD CONSTRAINT pk_compute PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4759 (class 2606 OID 9816990)
-- Name: host pk_host; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.host
ADD CONSTRAINT pk_host PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4763 (class 2606 OID 9817010)
-- Name: storage pk_storage; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.storage
ADD CONSTRAINT pk_storage PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4767 (class 2606 OID 11093098)
-- Name: vm_gpu pk_vm_gpu; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.vm_gpu
ADD CONSTRAINT pk_vm_gpu PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4765 (class 2606 OID 11093091)
-- Name: vm_power_state pk_vm_power_state; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.vm_power_state
ADD CONSTRAINT pk_vm_power_state PRIMARY KEY ("Object_UUID", ts);
--
-- TOC entry 5168 (class 0 OID 0)
-- Dependencies: 10
-- Name: SCHEMA vmreports; Type: ACL; Schema: -; Owner: dbo
--
GRANT ALL ON SCHEMA vmreports TO nifi;
GRANT ALL ON ALL TABLES IN SCHEMA vmreports TO nifi;
GRANT SELECT, REFERENCES ON ALL TABLES IN SCHEMA vmreports TO analytics, analyst;
+3
View File
@@ -0,0 +1,3 @@
truncate table ngcloud_ru.tenant;
alter table ngcloud_ru.tenant ADD clientid varchar(30) NULL;
alter table ngcloud_ru.tenant drop column wzcode;
+3 -1
View File
@@ -107,10 +107,12 @@ ALTER SEQUENCE ngcloud_ru.storage_profile_types_id_seq OWNED BY ngcloud_ru.stora
CREATE TABLE ngcloud_ru.tenant (
id uuid NOT NULL,
name character varying(255) NOT NULL,
wzcode bigint
--wzcode bigint
clientid varchar(30) NULL
);
ALTER TABLE ngcloud_ru.tenant OWNER TO vc_billing;
--
+136
View File
@@ -0,0 +1,136 @@
CREATE SCHEMA IF NOT EXISTS vmreports;
ALTER SCHEMA vmreports OWNER TO dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi;
GRANT ALL ON ALL TABLES IN SCHEMA vmreports TO nifi;
GRANT ALL ON ALL SEQUENCES IN SCHEMA vmreports TO nifi;
--ALTER DEFAULT PRIVILEGES IN SCHEMA vmreports GRANT ALL ON TABLES TO nifi;
--ALTER DEFAULT PRIVILEGES IN SCHEMA vmreports GRANT ALL ON SEQUENCES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA vmreports GRANT ALL ON TABLES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA vmreports GRANT ALL ON SEQUENCES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO appserver;
drop table if exists vmreports.host;
create table vmreports.host(
"Name" varchar(255) NULL,
"Model" varchar(255) NULL,
"Cluster" varchar(255) NULL,
"Cluster_v2" varchar(255) NULL,
"Maintenance" varchar(255) NULL,
"CPU_Cores" float NULL, -- per host, not per socket
"CPU_Sockets" float NULL, -- not used in caclulations
"CPU_Speed" float NULL,
"RAM_Total" float NULL,
"RAM_Used" float NULL,
"CPU_Used" float NULL,
"CPU_Used_AVG" float NULL,
"CPU_Contention_95" float NULL,
"HT_Active" boolean NULL,
"CPUtoVM" float NULL,
"RAMtoVM" float NULL,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_host PRIMARY KEY (ts, "Object_UUID")
);
--grant all on vmreports.host to nifi;
drop table if exists vmreports.compute;
create table vmreports.compute (
"Name" varchar(255) NULL,
"UUID_VM" varchar(255) NULL,
"VDC" varchar(255) NULL,
"Organization_UUID" uuid NULL,
"Organization" varchar(255) NULL,
"WZ" varchar(255) NULL,
"Cluster" varchar(255) NULL,
"Host" varchar(255) NULL,
"Host_Model" varchar(255) NULL,
"CPU_Model" varchar(255) NULL,
"Host_CPU_Cores" float NULL,
"Host_CPU_Sockets" float NULL,
"Host_CPU_Speed" float NULL,
"Host_RAM" float NULL,
"Host_CPU_Used" float NULL,
"Host_CPU_Used_Avg" float NULL,
"VDC_CPU_Reserv" float NULL,
"VDC_CPU_Speed" float NULL,
"VM_vCPU_count" float NULL,
"VM_CPU_Used" float NULL,
"VM_CPU_Used_AVG" float NULL,
"VM_CPU_Reserv_GHz" float NULL,
"VM_CPU_Reserv_perc" float NULL,
"VM_RAM_Allocated" float NULL,
"VM_RAM_Used" float NULL,
"VM_is_ON" float NULL,
--"Summary": false,
--"Grand Total": false,
--"Group UUID": null,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_compute PRIMARY KEY (ts, "Object_UUID")
);
--grant all on vmreports.compute to nifi;
drop table if exists vmreports.storage;
create table vmreports.storage (
"Name" varchar(255) NULL,
"VM_UUID" varchar(255) NULL,
"VDC" varchar(255) NULL,
"Organization_UUID" uuid NULL,
"Organization" varchar(255) NULL,
"WZ" varchar(255) NULL,
"Cluster" varchar(255) NULL,
"Disk" varchar(255) NOT NULL, --part of the key
"Storage_Policy" varchar(255) NULL,
"Disk_Allocated" float NULL,
"Disk_Used" float NULL,
"VM_is_ON" float NULL,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_storage PRIMARY KEY (ts, "Object_UUID", "Disk")
);
--Disk мог содержать NULL, но должен быть частью ключа, как оказалось
--grant all on vmreports.storage to nifi;
--UPDATE vmreports.storage SET "Disk"='' WHERE "Disk" IS NULL;
--ALTER TABLE vmreports.storage ALTER COLUMN "Disk" SET NOT NULL;
--ALTER TABLE vmreports.storage DROP CONSTRAINT PK_storage;
--ALTER TABLE vmreports.storage ADD CONSTRAINT PK_storage PRIMARY KEY (ts, "Object_UUID", "Disk");
UpdateRecord[id=3d3d3faa-fabd-1de9-d3d8-8f24953a9832] Failed to process FlowFile[filename=9cd148f7-bd09-43da-9ccd-82c8607ee98d]; will route to failure: java.lang.NullPointerException: Cannot invoke "org.apache.nifi.serialization.record.RecordField.getDataType()" because the return value of "org.apache.nifi.record.path.FieldValue.getField()" is null
drop table if exists vmreports.vm_gpu;
create table vmreports.vm_gpu (
"Name" varchar(255) NULL,
"ClientID" varchar(255) NULL, --WZ
"VCD" varchar(255) NULL, --vcloud
"Org" varchar(255) NULL,
"OrgVDC" varchar(255) NULL,
"vCenter" varchar(255) NULL,
"GPU" jsonb NULL,
"OrgStatus" varchar(255) NULL,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_vm_gpu PRIMARY KEY (ts, "Object_UUID")
);
--grant all on vmreports.vm_gpu to nifi;
DROP TABLE IF EXISTS vmreports.vm_power_state;
CREATE TABLE IF NOT EXISTS vmreports.vm_power_state
(
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
value float,
CONSTRAINT PK_vm_power_state PRIMARY KEY ("Object_UUID", ts)
);
--grant all on vmreports.vm_power_state to nifi;
+204
View File
@@ -0,0 +1,204 @@
--
-- PostgreSQL database dump
--
--
-- TOC entry 10 (class 2615 OID 9816969)
-- Name: vmreports; Type: SCHEMA; Schema: -; Owner: dbo
--
CREATE SCHEMA IF NOT EXISTS vmreports;
ALTER SCHEMA vmreports OWNER TO dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi,appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT ALL ON TABLES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi, appserver;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- TOC entry 721 (class 1259 OID 9816992)
-- Name: compute; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.compute (
"Name" character varying(255),
"UUID_VM" character varying(255),
"VDC" character varying(255),
"Organization_UUID" uuid,
"Organization" character varying(255),
"WZ" character varying(255),
"Cluster" character varying(255),
"Host" character varying(255),
"Host_Model" character varying(255),
"CPU_Model" character varying(255),
"Host_CPU_Cores" double precision,
"Host_CPU_Sockets" double precision,
"Host_CPU_Speed" double precision,
"Host_RAM" double precision,
"Host_CPU_Used" double precision,
"Host_CPU_Used_Avg" double precision,
"VDC_CPU_Reserv" double precision,
"VDC_CPU_Speed" double precision,
"VM_vCPU_count" double precision,
"VM_CPU_Used" double precision,
"VM_CPU_Used_AVG" double precision,
"VM_CPU_Reserv_GHz" double precision,
"VM_CPU_Reserv_perc" double precision,
"VM_RAM_Allocated" double precision,
"VM_RAM_Used" double precision,
"VM_is_ON" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.compute OWNER TO dbo;
--
-- TOC entry 722 (class 1259 OID 9817004)
-- Name: storage; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.storage (
"Name" character varying(255),
"VM_UUID" character varying(255),
"VDC" character varying(255),
"Organization_UUID" uuid,
"Organization" character varying(255),
"WZ" character varying(255),
"Cluster" character varying(255),
"Disk" character varying(255),
"Storage_Policy" character varying(255),
"Disk_Allocated" double precision,
"Disk_Used" double precision,
"VM_is_ON" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.storage OWNER TO dbo;
--
-- TOC entry 720 (class 1259 OID 9816984)
-- Name: host; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.host (
"Name" character varying(255),
"Model" character varying(255),
"Cluster" character varying(255),
"Maintenance" character varying(255),
"CPU_Cores" double precision,
"CPU_Sockets" double precision,
"CPU_Speed" double precision,
"RAM_Total" double precision,
"RAM_Used" double precision,
"CPU_Used" double precision,
"CPU_Used_AVG" double precision,
"CPU_Contention_95" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
"Cluster_v2" character varying(255),
"HT_Active" boolean,
"CPUtoVM" double precision,
"RAMtoVM" double precision
);
ALTER TABLE vmreports.host OWNER TO dbo;
--
-- TOC entry 733 (class 1259 OID 11093092)
-- Name: vm_gpu; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.vm_gpu (
"Name" character varying(255),
"ClientID" character varying(255),
"VCD" character varying(255),
"Org" character varying(255),
"OrgVDC" character varying(255),
"vCenter" character varying(255),
"GPU" jsonb,
"OrgStatus" character varying(255),
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.vm_gpu OWNER TO dbo;
--
-- TOC entry 732 (class 1259 OID 11093087)
-- Name: vm_power_state; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.vm_power_state (
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
value double precision
);
ALTER TABLE vmreports.vm_power_state OWNER TO dbo;
--
-- TOC entry 4761 (class 2606 OID 9816998)
-- Name: compute pk_compute; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.compute
ADD CONSTRAINT pk_compute PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4759 (class 2606 OID 9816990)
-- Name: host pk_host; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.host
ADD CONSTRAINT pk_host PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4763 (class 2606 OID 9817010)
-- Name: storage pk_storage; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.storage
ADD CONSTRAINT pk_storage PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4767 (class 2606 OID 11093098)
-- Name: vm_gpu pk_vm_gpu; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.vm_gpu
ADD CONSTRAINT pk_vm_gpu PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4765 (class 2606 OID 11093091)
-- Name: vm_power_state pk_vm_power_state; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.vm_power_state
ADD CONSTRAINT pk_vm_power_state PRIMARY KEY ("Object_UUID", ts);
--
-- TOC entry 5168 (class 0 OID 0)
-- Dependencies: 10
-- Name: SCHEMA vmreports; Type: ACL; Schema: -; Owner: dbo
--
GRANT ALL ON SCHEMA vmreports TO nifi;
+101
View File
@@ -0,0 +1,101 @@
ALTER TABLE IF EXISTS vmreports.storage
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.storage FROM analyst;
REVOKE ALL ON TABLE vmreports.storage FROM analytics;
REVOKE ALL ON TABLE vmreports.storage FROM appserver;
GRANT SELECT ON TABLE vmreports.storage TO analyst;
GRANT SELECT ON TABLE vmreports.storage TO analytics;
GRANT SELECT ON TABLE vmreports.storage TO appserver;
GRANT ALL ON TABLE vmreports.storage TO dbo;
GRANT ALL ON TABLE vmreports.storage TO designer;
GRANT ALL ON TABLE vmreports.storage TO nifi;
---------------
ALTER TABLE IF EXISTS vmreports.compute
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.compute FROM analyst;
REVOKE ALL ON TABLE vmreports.compute FROM analytics;
REVOKE ALL ON TABLE vmreports.compute FROM appserver;
GRANT SELECT ON TABLE vmreports.compute TO analyst;
GRANT SELECT ON TABLE vmreports.compute TO analytics;
GRANT SELECT ON TABLE vmreports.compute TO appserver;
GRANT ALL ON TABLE vmreports.compute TO dbo;
GRANT ALL ON TABLE vmreports.compute TO designer;
GRANT ALL ON TABLE vmreports.compute TO nifi;
--------------------
ALTER TABLE IF EXISTS vmreports.host
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.host FROM analyst;
REVOKE ALL ON TABLE vmreports.host FROM analytics;
REVOKE ALL ON TABLE vmreports.host FROM appserver;
GRANT SELECT ON TABLE vmreports.host TO analyst;
GRANT SELECT ON TABLE vmreports.host TO analytics;
GRANT SELECT ON TABLE vmreports.host TO appserver;
GRANT ALL ON TABLE vmreports.host TO dbo;
GRANT ALL ON TABLE vmreports.host TO designer;
GRANT ALL ON TABLE vmreports.host TO nifi;
------------------------------------
ALTER TABLE IF EXISTS vmreports.vm_gpu
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.vm_gpu FROM analyst;
REVOKE ALL ON TABLE vmreports.vm_gpu FROM analytics;
REVOKE ALL ON TABLE vmreports.vm_gpu FROM appserver;
GRANT SELECT ON TABLE vmreports.vm_gpu TO analyst;
GRANT SELECT ON TABLE vmreports.vm_gpu TO analytics;
GRANT SELECT ON TABLE vmreports.vm_gpu TO appserver;
GRANT ALL ON TABLE vmreports.vm_gpu TO dbo;
GRANT ALL ON TABLE vmreports.vm_gpu TO designer;
GRANT ALL ON TABLE vmreports.vm_gpu TO nifi;
-----------------
ALTER TABLE IF EXISTS vmreports.vm_power_state
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.vm_power_state FROM analyst;
REVOKE ALL ON TABLE vmreports.vm_power_state FROM analytics;
REVOKE ALL ON TABLE vmreports.vm_power_state FROM appserver;
GRANT SELECT ON TABLE vmreports.vm_power_state TO analyst;
GRANT SELECT ON TABLE vmreports.vm_power_state TO analytics;
GRANT SELECT ON TABLE vmreports.vm_power_state TO appserver;
GRANT ALL ON TABLE vmreports.vm_power_state TO dbo;
GRANT ALL ON TABLE vmreports.vm_power_state TO designer;
GRANT ALL ON TABLE vmreports.vm_power_state TO nifi;
+65
View File
@@ -0,0 +1,65 @@
SET ROLE dbo;
DROP SCHEMA IF EXISTS vmreports CASCADE;
CREATE SCHEMA IF NOT EXISTS vmreports AUTHORIZATION dbo;
GRANT USAGE ON SCHEMA vmreports TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO appserver;
GRANT USAGE ON SCHEMA vmreports TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi,analytics,analyst;
/*
конструкция осложняется тем, что
нужен один пользователь-владелец и второй читатель
Это вызывает множество телодвижений при создании.
Необходимость указывать пароль при локальном коннекте не очень радует
(ведь пользователь локальный, прописан на этом же сервере)
*/
--DROP FOREIGN TABLE IF EXISTS host;
--CREATE FOREIGN TABLE vmreports.host () SERVER billing OPTIONS (schema_name 'vmreports', table_name 'host'); -- вместо этого лучше импортировать схему
DROP SERVER IF EXISTS billing CASCADE;
CREATE SERVER billing
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'billing', updatable 'false');
-- для импорта схемы мы создаем маппинг владельцу
DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER billing;
CREATE USER MAPPING FOR PUBLIC SERVER billing OPTIONS (user 'appserver', password '***');
IMPORT FOREIGN SCHEMA vmreports FROM SERVER billing INTO vmreports;
--GRANT SELECT ON vmreports.host to appserver;
--DROP USER MAPPING IF EXISTS FOR dbo SERVER billing; -- мы импортировали схему, маппинг больше не нужен.
-- Но нам все равно пришлось указать пароль дважды
--DROP USER MAPPING IF EXISTS FOR appserver SERVER billing;
--CREATE USER MAPPING FOR appserver SERVER billing OPTIONS (user 'nifi', password '***');
--проверка
--set role appserver; select * from vmreports.host;
-- это если интересно, не разрешен ли локальный коннект
--SELECT * FROM pg_hba_file_rules;
/*
Довольно тонкий нюанс, легко ускользающий
ALTER DEFAULT PRIVILEGES FOR ROLE target_creator_role
IN SCHEMA schema_name
GRANT privilege_type ON TABLES TO recipient_role;
Recommended Strategy for "Any User"Because there is no "FOR ALL ROLES" option, the best practice is to have all users create objects as a shared group role.Create a group role (e.g., schema_owner_role).Grant that role to all relevant users.Instruct users to run SET ROLE schema_owner_role; before creating objects.Run ALTER DEFAULT PRIVILEGES FOR ROLE schema_owner_role ... once to cover all objects created under that role.
*/
/*
Best Practices for Role Owners:Dedicated Schema Owner: Create a dedicated, non-login role for managing database schema objects (DDL) and make it the owner of the database/schemas.Separation of Duties: Separate the application user (DML - SELECT, INSERT, UPDATE) from the owner role (DDL - CREATE, ALTER, DROP).Use ALTER DEFAULT PRIVILEGES: Set up default privileges for new objects to ensure that user roles automatically gain access to new tables or functions created by the owner role.Leverage Role Hierarchy: Create group roles (e.g., app_reader, app_writer) and grant these to individual user roles for easier permission management.Avoid Public Schema: Create specific schemas and revoke CREATE privileges on the public schema to prevent accidental object creation.Secure SUPERUSER Usage: Only use the default postgres superuser role for initial setup and critical administrative tasks. Never use it for applications.Example Implementation:sql-- Create a schema owner
CREATE ROLE ddl_user NOLOGIN;
-- Create an app user
CREATE ROLE dml_user LOGIN PASSWORD 'secure_password';
-- Assign ownership
ALTER DATABASE mydb OWNER TO ddl_user;
ALTER SCHEMA public OWNER TO ddl_user;
-- Grant future access
ALTER DEFAULT PRIVILEGES FOR ROLE ddl_user IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO dml_user;
*/
+37
View File
@@ -0,0 +1,37 @@
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
GRANT USAGE ON FOREIGN DATA WRAPPER postgres_fdw TO dbo;
SET ROLE dbo;
DROP SCHEMA IF EXISTS vmreports CASCADE;
CREATE SCHEMA IF NOT EXISTS vmreports AUTHORIZATION dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO nifi,analytics,analyst; -- это работает и для внешних таблиц
--ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi,analytics,analyst;
/*
конструкция осложняется тем, что
нужен один пользователь-владелец и второй читатель
Это вызывает множество телодвижений при создании.
Необходимость указывать пароль при локальном коннекте не очень радует
(ведь пользователь локальный, прописан на этом же сервере)
*/
DROP SERVER IF EXISTS billing CASCADE;
CREATE SERVER billing
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'billing', updatable 'false');
DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER billing;
CREATE USER MAPPING FOR PUBLIC SERVER billing OPTIONS (user 'nifi', password '***');
IMPORT FOREIGN SCHEMA vmreports FROM SERVER billing INTO vmreports;
--select * from vmreports.host;
/*
план миграции
Забекапить схему vmreports
Восстановить схему vmreports в базу биллинг
Поменять целевую базу импорта НиФи (для прода - втащить новый обмен из репозитория)
Выполнить скрипт в базе DWH - он удалит физические таблицы и создаст внешние таблицы на базу billing
*/
+113
View File
@@ -0,0 +1,113 @@
-- выполнить в базе dwh для создания внешних таблиц схемы vmreports
-- перед выполнением забекапить схему vmreports для восстановления в БД billing
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
GRANT USAGE ON FOREIGN DATA WRAPPER postgres_fdw TO dbo;
SET ROLE dbo;
--DROP SCHEMA IF EXISTS vmreports CASCADE;
CREATE SCHEMA IF NOT EXISTS vmreports AUTHORIZATION dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO nifi,analytics,analyst; -- это работает и для внешних таблиц
--ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi,analytics,analyst;
/*
конструкция осложняется тем, что
нужен один пользователь-владелец и второй читатель
Это вызывает множество телодвижений при создании.
Необходимость указывать пароль при локальном коннекте не очень радует
(ведь пользователь локальный, прописан на этом же сервере)
А при смене его пароля все сломается, и поиск ошибки будет неочевиден
*/
DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'vmreports'
LOOP
EXECUTE format('ALTER TABLE vmreports.%I RENAME TO %I',
rec.tablename,
'del_' || rec.tablename);
END LOOP;
END $$;
DROP SERVER IF EXISTS billing CASCADE;
CREATE SERVER billing
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'billing', updatable 'false');
DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER billing;
CREATE USER MAPPING FOR PUBLIC SERVER billing OPTIONS (user 'nifi', password '***');
IMPORT FOREIGN SCHEMA vmreports FROM SERVER billing INTO vmreports;
--select * from vmreports.host;
/*
план миграции - черновик
Забекапить схему vmreports
Восстановить схему vmreports из бэкапа в базу биллинг
Поменять целевую базу импорта НиФи (для прода - втащить новый обмен из репозитория)
Выполнить скрипт в базе DWH - он удалит (переименует) физические таблицы и создаст внешние таблицы на базу billing
*/
/*
Исходно данные host, compute, storage, (gpu)
загружались в dwh.vmreports,
для анализа capacity
В текущей конфигурации,
чтобы избежать дублирования
таблиц, данных и обменов NiFi
Направляем загрузку данных из VROPS в схему billing.vmreports
и в базе dwh (схема vmreports) создаем Foreign Tables на billing.vmreports.
-----------------------------------------------
Миграция на проде:
убедиться, что новый обмен VROPS->billing.vmreports (billing/vmReports) загружен в NiFi Registry
найти пароль nifi (под ним коннект к localhost)
остановить загрузку из VROPS на nifi-01 (billing/vmReports),
сделать бэкап схемы dwh.vmreports (займет минут 10 ... заняло 215 сек),
Восстановить схему vmreports в БД billing - запустить восстановление (Clean Before Restore) (РИСК: восстановление может занять много времени и завершиться с ошибками, обычно так бывает)
select count (*) from vmreports.storage
Выполнить этот скрипт (заменив пароль nifi на актуальный перед выполнением)
переименовать таблицы из схемы vmreports для удаления (есть в скрипте выше)
создать Foreign Tables (коннект проверится при импорте схемы)
Проверить, что права назначились коррректно
Обновить обмен billing/vmReports на продуктиве, запустить
-----------------------------------------------------------------
Данные из billing.vmreports также реплицируются
(процессорами группы LK)
в буферную базу rpt
*/
/*
17:04:49 MSKERROR0dff1f93-153d-3a90-ed64-55d23960b88c
EvaluateJsonPath[id=0dff1f93-153d-3a90-ed64-55d23960b88c] FlowFile FlowFile[filename=06c68bec-d824-4114-913b-bda34cb4fb1d] did not have valid JSON content.
-- со второй попытки отработало
17:05:15 MSKERROR239db407-76f2-3660-7fad-c4bedff6a4f8
InvokeHTTP[id=239db407-76f2-3660-7fad-c4bedff6a4f8] Request Processing failed: FlowFile[filename=3474c340-f360-4a1e-a27e-283be41fc264]: java.net.SocketTimeoutException: timeout
- Caused by: java.net.SocketException: Socket closed
https://payg.adl.nubes.ru/payg.cfm с первого раза таймаут, со второго посчиталось
*/
/*
теперь ищу в нифи примеры с кроном
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Failed to put Records to database for FlowFile[filename=11f8176f-2a98-4199-932e-22e2fc2073b1]. Routing to failure.: org.apache.nifi.serialization.MalformedRecordException: Record does not have a value for the UpdateKey column 'obj_uuid'
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Record does not have a value for the UpdateKey column 'obj_uuid'
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Failed to put Records to database for FlowFile[filename=5039a3b6-2244-4451-81fc-deb8074f3cc7]. Routing to failure.: org.apache.nifi.serialization.MalformedRecordException: Record does not have a value for the UpdateKey column 'obj_uuid'
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Record does not have a value for the UpdateKey column 'obj_uuid'
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Failed to put Records to database for FlowFile[filename=8c6e9efb-198d-4303-897a-53fcfdf4d0f6]. Routing to failure.: org.apache.nifi.serialization.MalformedRecordException: Record does not have a value for the UpdateKey column 'obj_uuid'
*/
+47
View File
@@ -0,0 +1,47 @@
SET ROLE dbo;
DROP SCHEMA IF EXISTS vmreports CASCADE;
CREATE SCHEMA IF NOT EXISTS vmreports AUTHORIZATION dbo;
--GRANT ALL ON SCHEMA vmreports TO postgres;
BEGIN
GRANT USAGE ON SCHEMA vmreports TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO appserver;
EXCEPTION END;
BEGIN
GRANT USAGE ON SCHEMA vmreports TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi,analytics,analyst;
EXCEPTION WHEN others THEN END;
/*
конструкция осложняется тем, что
нужен один пользователь-владелец и второй читатель
Это вызывает множество телодвижений при создании.
Необходимость указывать пароль при локальном коннекте не очень радует
(ведь пользователь локальный, прописан на этом же сервере)
*/
--DROP FOREIGN TABLE IF EXISTS host;
--CREATE FOREIGN TABLE vmreports.host () SERVER billing OPTIONS (schema_name 'vmreports', table_name 'host'); -- вместо этого лучше импортировать схему
DROP SERVER IF EXISTS billing CASCADE;
CREATE SERVER billing
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'billing');
-- для импорта схемы мы создаем маппинг владельцу
DROP USER MAPPING IF EXISTS FOR dbo SERVER billing;
CREATE USER MAPPING FOR dbo SERVER billing OPTIONS (user 'appserver', password '***');
IMPORT FOREIGN SCHEMA vmreports FROM SERVER billing INTO vmreports;
GRANT SELECT ON vmreports.host to appserver;
DROP USER MAPPING IF EXISTS FOR dbo SERVER billing; -- мы импортировали схему, маппинг больше не нужен.
-- Но нам все равно пришлось указать пароль дважды
DROP USER MAPPING IF EXISTS FOR appserver SERVER billing;
CREATE USER MAPPING FOR appserver SERVER billing OPTIONS (user 'appserver', password '***');
--проверка
--set role appserver; select * from vmreports.host;
-- это если интересно, не разрешен ли локальный коннект
--SELECT * FROM pg_hba_file_rules;
+52
View File
@@ -0,0 +1,52 @@
SET ROLE dbo;
DROP TABLE IF EXISTS vmreports.vsan_storage CASCADE;
CREATE TABLE vmreports.vsan_storage (
"Name" varchar(255) NULL,
"ClusterNew" varchar(255) NULL,
"Total" float NULL,
"Usable" float NULL,
"Used" float NULL,
"EffectiveFree" float NULL,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_vsan_storage PRIMARY KEY (ts, "Object_UUID")
);
--ALTER TABLE vmreports.vsan_storage OWNER TO dbo;
GRANT ALL ON vmreports.vsan_storage TO nifi;
--select * from vmreports.vsan_storage;
/*
{
"type": "record",
"name": "vmreports.vsan_storage",
"namespace": "org.apache.nifi",
"fields": [
{"name":"Object_UUID", "type":{"type": "string", "logicalType": "uuid"}},
{"name":"ts","type":{"type": "long", "logicalType": "local-timestamp-millis"}},
{"name":"Name", "type":["string" ,"null"]},
{"name":"ClusterNew", "type":["string" ,"null"]},
{"name":"Total", "type":["float" ,"null"]},
{"name":"Usable", "type":["float" ,"null"]},
{"name":"Used", "type":["float" ,"null"]},
{"name":"EffectiveFree", "type":["float" ,"null"]}
]
}
*/
/*
{
"*": {
"objId":"[&1].Name",
"1":"[&1].ClusterNew",
"2":"[&1].Total",
"3":"[&1].Usable",
"4":"[&1].Used",
"5":"[&1].EffectiveFree",
"objUUID":"[&1].Object_UUID"
}
}
*/
+5
View File
@@ -0,0 +1,5 @@
$ git remote -v
origin https://gitea.services.ngcloud.ru/smishchuk/payg-report.git (fetch)
origin https://gitea.services.ngcloud.ru/smishchuk/payg-report.git (push)
$ git push https://gitea.services.ngcloud.ru/smishchuk/payg-report-dev.git master
+633
View File
@@ -0,0 +1,633 @@
{
"companyId": "1",
"contracts": [
{
"contractId": "01983721-6049-72dd-a682-a5af1bd72420",
"contractInsideId": "02319_1",
"contractOutsideId": "02319_1",
"contractStartDate": "20250801",
"isContractTerminated": false,
"CalculationPeriod": "20260701",
"additionalAgreements": [
{
"additionalAgreementId": "019b6ec1-1984-78fc-9e48-0984aa1f3ca7",
"additionalAgreementIndex": 3,
"additionalAgreementName": "Доп соглашение 3, к сделке №3768 Сделка на изменения договора",
"additionalAgreementStartDate": "",
"additionalAgreementBaseBy": "019a5ef8-eebe-76b3-acb9-b5c37748f4ef",
"isActual": true,
"CalculationPeriod": "20260701",
"services": [
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.ceph.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.8300000000000001,
"discount": 0,
"atomicCode": "paas.s3.ceph.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.ceph.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5249999999999999,
"discount": 0,
"atomicCode": "paas.s3.ceph.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.ceph.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.3538,
"discount": 0,
"atomicCode": "paas.s3.ceph.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.ceph.put",
"component": "Запросы PUT, POST, PATCH, LIST",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 4.5750000000000002,
"discount": 0,
"atomicCode": "paas.s3.ceph.put"
}
],
"staticId": "e57bb0bd-8946-4603-95e8-c9c331378bce",
"serviceId": "2e1681d7-542e-489c-9169-de562bdca83a",
"serviceHash": "e57bb0bd-8946-4603-95e8-c9c331378bce3",
"LineCRMID": "019b6ec6-a779-7902-abeb-d2f7e746d64e",
"serviceCode": "paas.s3.ceph",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 0.24125452,
"quantity": 1,
"dateStartProvision": "20260101",
"dateEndProvision": "",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2026-01-01T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH, LIST - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 22,
"vatRate": 0.22,
"vatPerc": 22,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "2e1681d7-542e-489c-9169-de562bdca83a",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.cphc.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.026833333333,
"discount": 0,
"atomicCode": "paas.s3.cphc.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.cphc.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5249999999999999,
"discount": 0,
"atomicCode": "paas.s3.cphc.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.cphc.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.97599999999999998,
"discount": 0,
"atomicCode": "paas.s3.cphc.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.cphc.put",
"component": "Запросы PUT, POST, PATCH, LIST",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 11.834,
"discount": 0,
"atomicCode": "paas.s3.cphc.put"
}
],
"staticId": "2031f3ac-e921-4816-b91f-9b5b1cf5bf94",
"serviceId": "5271dc39-2ec5-4f20-b91e-fdffb4f363b9",
"serviceHash": "2031f3ac-e921-4816-b91f-9b5b1cf5bf943",
"LineCRMID": "019b6ec6-a82c-7902-93a3-48ba4e25d71d",
"serviceCode": "paas.s3.cphc",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20260101",
"dateEndProvision": "",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2026-01-01T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH, LIST - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 22,
"vatRate": 0.22,
"vatPerc": 22,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "5271dc39-2ec5-4f20-b91e-fdffb4f363b9",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 175,
"componentCode": "paas.s3.cphc.volq",
"component": "Дисковое пространство: Квота",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 2.7999999999999998,
"discount": 0,
"atomicCode": "paas.s3.cphc.volq"
}
],
"staticId": "f216f0ac-6c61-42aa-a454-56a93c707d11",
"serviceId": "776a3490-cb3a-4721-b521-7bf97073e104",
"serviceHash": "f216f0ac-6c61-42aa-a454-56a93c707d112",
"LineCRMID": "019b6ec6-a86d-7902-9ba2-defe46f2a036",
"serviceCode": "paas.s3.cphc",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 63.928570999999998,
"quantity": 1000,
"dateStartProvision": "20250826",
"dateEndProvision": "20250930",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-26T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-09-30T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное (Квота), в составе:\n Дисковое пространство: Квота - 1 ГБайт\n",
"serviceType": 2,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "776a3490-cb3a-4721-b521-7bf97073e104",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 175,
"componentCode": "paas.s3.cphc.volq",
"component": "Дисковое пространство: Квота",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 2.7999999999999998,
"discount": 0,
"atomicCode": "paas.s3.cphc.volq"
}
],
"staticId": "78707549-a675-46e0-bc5b-790ef4d21b6d",
"serviceId": "2e9369bc-7f3e-4c31-bb72-3d19e979964e",
"serviceHash": "78707549-a675-46e0-bc5b-790ef4d21b6d2",
"LineCRMID": "019b6ec6-a8ab-7902-af73-1034bf013a16",
"serviceCode": "paas.s3.cphc",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 63.928570999999998,
"quantity": 40000,
"dateStartProvision": "20250826",
"dateEndProvision": "20250930",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-26T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-09-30T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное (Квота), в составе:\n Дисковое пространство: Квота - 1 ГБайт\n",
"serviceType": 2,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "2e9369bc-7f3e-4c31-bb72-3d19e979964e",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.cphc.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.01,
"discount": 0,
"atomicCode": "paas.s3.cphc.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.cphc.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.cphc.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.cphc.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.95999999999999996,
"discount": 0,
"atomicCode": "paas.s3.cphc.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.cphc.put",
"component": "Запросы PUT, POST, PATCH",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 11.640000000000001,
"discount": 0,
"atomicCode": "paas.s3.cphc.put"
}
],
"staticId": "20240911T125603868",
"serviceId": "a564eda1-b716-42ca-9ae7-efc49b112a58",
"serviceHash": "20240911T1256038683",
"LineCRMID": "019b6ec6-a949-7902-b473-8741f2a18efa",
"serviceCode": "paas.s3.cphc",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20250826",
"dateEndProvision": "20250930",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-26T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-09-30T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "a564eda1-b716-42ca-9ae7-efc49b112a58",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.ceph.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.8,
"discount": 0,
"atomicCode": "paas.s3.ceph.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.ceph.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.ceph.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.ceph.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.34999999999999998,
"discount": 0,
"atomicCode": "paas.s3.ceph.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.ceph.put",
"component": "Запросы PUT, POST, PATCH",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 4.5,
"discount": 0,
"atomicCode": "paas.s3.ceph.put"
}
],
"staticId": "20241015T124353835",
"serviceId": "5371b7cb-6f6c-4c7d-85b4-609eb52f994a",
"serviceHash": "20241015T1243538353",
"LineCRMID": "019b6ec6-aa03-7902-9c40-06f6d5cf6e13",
"serviceCode": "paas.s3.ceph",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20250801",
"dateEndProvision": "20250930",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-01T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-09-30T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "5371b7cb-6f6c-4c7d-85b4-609eb52f994a",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 69,
"componentCode": "paas.s3.ceph.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.ceph.trf"
},
{
"paramId": 70,
"componentCode": "paas.s3.ceph.put",
"component": "Запросы PUT, POST, PATCH, LIST",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 4.5,
"discount": 0,
"atomicCode": "paas.s3.ceph.put"
},
{
"paramId": 118,
"componentCode": "paas.s3.ceph.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.8,
"discount": 0,
"atomicCode": "paas.s3.ceph.vol"
},
{
"paramId": 68,
"componentCode": "paas.s3.ceph.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.34999999999999998,
"discount": 0,
"atomicCode": "paas.s3.ceph.get"
}
],
"staticId": "e57bb0bd-8946-4603-95e8-c9c331378bce",
"serviceId": "09fd617a-9ffc-4b98-ad66-3fedac52c7b9",
"serviceHash": "e57bb0bd-8946-4603-95e8-c9c331378bce3",
"LineCRMID": "019a732b-22a2-7545-91ce-819207c779a0",
"catalogName": "Объектное хранилище",
"endCustomerName": "",
"endCustomerINN": "",
"serviceCode": "paas.s3.ceph",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20251001",
"CalculationPeriod": "20260701",
"dateEndProvision": "20251231",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-10-01T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-12-31T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH, в составе:\n Исходящий трафик - 1 ГБайт\nЗапросы PUT, POST, PATCH, LIST - 1 10тыс.шт.\nДисковое пространство: Объем хранения - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "09fd617a-9ffc-4b98-ad66-3fedac52c7b9",
"dealIndex": 3274
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.cphc.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.01,
"discount": 0,
"atomicCode": "paas.s3.cphc.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.cphc.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.cphc.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.cphc.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.95999999999999996,
"discount": 0,
"atomicCode": "paas.s3.cphc.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.cphc.put",
"component": "Запросы PUT, POST, PATCH, LIST",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 11.640000000000001,
"discount": 0,
"atomicCode": "paas.s3.cphc.put"
}
],
"staticId": "2031f3ac-e921-4816-b91f-9b5b1cf5bf94",
"serviceId": "539d210f-2e5a-45e2-b148-b0b0834c138c",
"serviceHash": "2031f3ac-e921-4816-b91f-9b5b1cf5bf943",
"LineCRMID": "019a732b-22d4-7545-83af-18b7f8867eaa",
"catalogName": "Объектное хранилище",
"endCustomerName": "",
"endCustomerINN": "",
"serviceCode": "paas.s3.cphc",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20251001",
"CalculationPeriod": "20260701",
"dateEndProvision": "20251231",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-10-01T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-12-31T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH, LIST - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "539d210f-2e5a-45e2-b148-b0b0834c138c",
"dealIndex": 3274
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.cphc.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 40000,
"priceWVat": 1.01,
"discount": 0,
"atomicCode": "paas.s3.cphc.vol"
},
{
"paramId": 68,
"componentCode": "paas.s3.cphc.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.95999999999999996,
"discount": 0,
"atomicCode": "paas.s3.cphc.get"
},
{
"paramId": 69,
"componentCode": "paas.s3.cphc.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.cphc.trf"
},
{
"paramId": 70,
"componentCode": "paas.s3.cphc.put",
"component": "Запросы PUT, POST, PATCH",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 11.640000000000001,
"discount": 0,
"atomicCode": "paas.s3.cphc.put"
}
],
"staticId": "20240911T125603868",
"serviceId": "67dffa20-a69e-449a-b02c-edf78d9ff192",
"serviceHash": "20240911T1256038683",
"LineCRMID": "01983726-3501-7496-b7f3-6f58f4d7f209",
"catalogName": "Объектное хранилище",
"endCustomerName": "",
"endCustomerINN": "",
"serviceCode": "paas.s3.cphc",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20250801",
"CalculationPeriod": "20260701",
"dateEndProvision": "20250825",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-01T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-25T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное, в составе:\n Дисковое пространство: Объем хранения - 40000 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nИсходящий трафик - 1 ГБайт\nЗапросы PUT, POST, PATCH - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "67dffa20-a69e-449a-b02c-edf78d9ff192",
"dealIndex": 1450
}
]
}
]
}
]
}
+151 -70
View File
@@ -1,25 +1,33 @@
<cfset msStartAt=getTickCount()/>
<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)#/>
<cfset hours=dateDiff('h',dt_start,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))/>
<cfset tenant_wz_index=val(mid(request.auth.wz,3,5))/><!--- cut off WZ --->
<cffunction name="snake2camel"
returntype="string"
output="false"
hint="convert snake style name to camel style name">
<cfargument name="snake" type="string" required="true" />
<cfreturn #reReplace(ARGUMENTS.snake,"_([a-z])","\u\1","ALL")#/>
</cffunction>
<cffunction name="camel2snake"
returntype="string"
output="false"
hint="convert camel style name to snake style name">
<cfargument name="camel" type="string" required="true" />
<cfreturn #reReplace(ARGUMENTS.camel,"([A-Z])","_\l\1","ALL")#/>
</cffunction>
<cfscript>
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>
<cffunction name="iso8601dtFormat"
returntype="string"
@@ -30,65 +38,138 @@
</cffunction>
<cffunction name="query2json"
output="false"
hint="print query in json format as array of structures">
<cfargument name="qry" type="query" required="true"/><!--- column names should not contain commas --->
<cfargument name="convertSnakeToCamel" type="boolean" default=true/>
<cfif listFind("WZ01112",request.auth.wz)>
<cfset temp.out=[
"status"="exception"
,"message"="Для провайдера отчетность по потреблению не отображается."
,"tenantWzIndex"="#tenant_wz_index#"
,"wz"=#request.auth.wz#
,"login"=#request.auth.login#
,"appVersion"=#request.APP_VERSION#
,"queryDurationMs"=#(getTickCount() - msStartAt)#
]/>
<cfset var columnsIn=#arguments.qry.columnList()#/>
<cfset var columnsOut=""/>
<cfelseif listFind("WZ01012,WZ01031,WZ01153,WZ01165,WZ01166,WZ01348,WZ01359,WZ01406,WZ01445,WZ01553,WZ02059,WZ02121,WZ02319,WZ02557,WZ02706,WZ03232,WZ03467,WZ03513,WZ03768,WZ03811,WZ03812,WZ03817,WZ03888,WZ03933",request.auth.wz)>
<cfset temp.out=[
"status"="exception"
,"message"="Отображение актуальных данных по потреблению временно недоступно. Обратитесь к сервис-менеджеру. Извините за доставленные неудобства."
,"tenantWzIndex"="#tenant_wz_index#"
,"wz"=#request.auth.wz#
,"login"=#request.auth.login#
,"appVersion"=#request.APP_VERSION#
,"queryDurationMs"=#(getTickCount() - msStartAt)#
]/>
<cfif arguments.convertSnakeToCamel>
<cfloop list=#columnsIn# item="col">
<cfset columnsOut=listAppend(columnsOut,snake2camel(col))/>
</cfloop>
<cfset var outArray=arrayNew(1)/>
//copy fields to structure, renaming fields, this preserves data type
<cfloop query=#arguments.qry#>
<cfset var structRow=structNew()/>
<cfloop index="i" from="1" to=#listLen(columnsOut)#>
<cfset structInsert(structRow,"#listGetAt(#columnsOut#,i)#",arguments.qry["#listGetAt(#columnsIn#,i)#"])/>
</cfloop>
<cfset arrayAppend(outArray,structRow)/>
</cfloop>
<cfreturn serializeJSON(outArray)/>
<cfelse>
<cfreturn serializeJSON(arguments.qry,"struct")/>
</cfif>
</cffunction>
<cfelse>
<cfmodule template="mod/compute_payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
tenant_wz_index=#tenant_wz_index#
full_join=false
output="report"
debug=#isDefined("DEBUG")#
/>
<cfmodule template="mod/payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
tenant_wz_index=#tenant_wz_index#
output="report"
debug=#isDefined("DEBUG")#
/>
<cfset out={
"appVersion"=#request.APP_VERSION#,
"hours"=#hours#,
"dtStart="=#iso8601dtFormat(dt_start)#,
"dtFinish"=#iso8601dtFormat(dt_finish)#,
"vcdComputingLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#",
"vcdStorageLoadedAt"="#iso8601dtFormat(report.qStorageAge.dt_load)#",
"s3StorageLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#",
"s3OpsTrfLoadedAt"="#iso8601dtFormat(report.qS3OpsTrfAge.dt_load)#",
"records"=#report.qCharge.recordCount#,
"queryDurationMs"=#(getTickCount() - msStartAt)#,
"wz"=#request.auth.wz#,
"login"=#request.auth.login#,
"report"=#report.qCharge.reduce((result, row) => {
result.append(row);
return result;
}, [])#
}/>
<cfset temp.out=[
"status"="success"
,"dtStart"=#iso8601dtFormat(dt_start)#
,"dtFinish"=#iso8601dtFormat(dt_finish)#
,"hours"=#hours#
,"tenantWzIndex"="#tenant_wz_index#"
,"wz"=#request.auth.wz#
,"login"=#request.auth.login#
,"appVersion"=#request.APP_VERSION#
,"vcdComputingLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#"
,"vcdStorageLoadedAt"="#iso8601dtFormat(report.qStorageAge.dt_load)#"
,"s3StorageLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#"
,"s3OpsTrfLoadedAt"="#iso8601dtFormat(report.qS3OpsTrfAge.dt_load)#"
,"records"=#report.qCharge.recordCount#
,"queryDurationMs"=#(getTickCount() - msStartAt)#
,"periods"=[]
]/><!--- using [] instead of {} for struct literals to obtain linked (ordered) structures: keys order preserved --->
<!--- beware nulls --->
<cfoutput query="report.qCharge" group="year">
<cfoutput group="month">
<cfset temp.period=[
"year"=#year#
,"month"=#month#
,"services"=[]
]/>
<cfset arrayAppend(temp.out.periods,temp.period)/>
<cfoutput group="svc">
<cfset var accum_cost_svc = 0/>
<!--- <cfset var accum_metric_component = 0/> --->
<cfset "temp.service"=["svc"="#svc#"
,"serviceCode"="#service_code#"
,"serviceCustomName"="#serviceCustomName#"
<!--- ,"note"="#(serviceHash EQ '')?'не найдено в договоре':''#" --->
,"hash"="#serviceHash#"
<!--- ,"metric"=#accum_metric_component# --->
,"components"=[]
]/>
<cfoutput group="code">
<cfoutput group="serviceStart">
<!--- <cfset accum_metric_component += isNumeric(metric) ? metric : 0/> --->
<cfset accum_metric_dimension_1 = 0/>
<cfset "temp.component"=["code"="#code#"
,"codeMetered"="#m_code#"
,"component"="#component#"
,"unit"="#unit#"
,"freeTier"=#isNumeric(free_tier) ? free_tier : 0#
,"price"=#isNumeric(discounted_price) ? gaussianRound(discounted_price,2) : javaCast("null",0)#
,"details"=[]
]/>
<cfoutput group="dimension_1">
<cfset accum_metric_dimension_1 += isNumeric(metric) ? metric : 0/>
<cfset temp.dimension=["dimension1name"="#dimension_1_name#"
,"dimension1value"="#dimension_1#"
,"dtBilledFrom"="#dateFormat(dt_billed_from,'YYYY-MM-DD')#"
,"dtBilledTo"="#dateFormat(dt_billed_to,'YYYY-MM-DD')#"
,"metric"=#(metric NEQ 0 AND isNumeric(metric))?round(metric,3):javaCast("null",0)#
,"unit"="#unit#"
<!--- ,"price"="#gaussianRound(discounted_price,2)#" --->
]/>
<cfset arrayAppend(temp.component.details, temp.dimension)/>
</cfoutput><!--- dimension_1 --->
<!--- считаем, что free_tier NOT NULL (вместо NULL 0) --->
<cfset var freeTier = isNumeric(free_tier) ? free_tier : 0/>
<cfset var payable_metric_dimension_1 = isNumeric(accum_metric_dimension_1) ? max(ceiling(accum_metric_dimension_1) - freeTier, 0) : 0/>
<!--- кажется, payable_metric_dimension_1 гарантированно isNumeric --->
<cfset var cost_dimension_1 = isNumeric(discounted_price) ? precisionEvaluate("gaussianRound(payable_metric_dimension_1*gaussianRound(discounted_price, 2), 2)") : 0/>
<cfset accum_cost_svc = precisionEvaluate("accum_cost_svc + cost_dimension_1")/>
<cfset "temp.component.metric"=#(accum_metric_dimension_1 NEQ 0 AND isNumeric(accum_metric_dimension_1))? round(accum_metric_dimension_1,3):javaCast("null",0)#/>
<cfset "temp.component.metricPayable"=#payable_metric_dimension_1#/>
<cfset "temp.component.cost"=#cost_dimension_1#/>
<cfset arrayAppend(temp.service.components, temp.component)/>
</cfoutput><!--- serviceStart (component version in specification, that is svc version joined down to the component) --->
<cfset "temp.service.cost"=#accum_cost_svc#/>
</cfoutput><!--- code (component) --->
<cfset arrayAppend(temp.period.services, temp.service)/>
</cfoutput><!--- svc --->
</cfoutput><!--- month --->
</cfoutput><!--- year --->
<cfset "temp.out.durationMs"=(getTickCount()-request.startTickCount)/>
</cfif>
<cfcontent
type="application/json"
/><cfoutput>#serializeJSON(out)#</cfoutput>
/><cfoutput>#serializeJSON(temp.out)#</cfoutput>
+1 -1
View File
@@ -67,7 +67,7 @@
</cfloop>
<cfset var outArray=arrayNew(1)/>
//copy fields to structure, renaming fields, this preserves data type
<!--- //copy fields to structure, renaming fields, this preserves data type --->
<cfloop query=#arguments.qry#>
<cfset var structRow=structNew()/>
<cfloop index="i" from="1" to=#listLen(columnsOut)#>
+103
View File
@@ -0,0 +1,103 @@
component {
/*0.04 precision fix DECIMAL(128,32) maybe too memory consumable*/
/*0.03 precision fix*/
/*0.02 timestamp support*/
/**
* HSQL 1.8
* Принимает Lucee Query, создает в HSQL временную таблицу и льет в нее данные
* @name Имя таблицы, которое ты хочешь использовать в SQL
* @data Сам запрос (результат queryExecute или cfhttp)
* @dsn Имя datasource для HSQL (по умолчанию "inmem")
*/
public string function register(required string name, required query data, string dsn = "inmem") {
var cols = arguments.data.columnList;
var meta = getMetaData(arguments.data);
//writedump(meta);
var colDefs = [];
var colTypes = {};
var params = [];
// 1. Генерируем "CREATE TEMP TABLE name (col1 TYPE, col2 TYPE...)"
for (var i = 1; i <= arrayLen(meta); i++) {
var colName = meta[i].name;
// Маппинг типов Lucee -> типы H2
var dbType = mapType(meta[i].typeName);
arrayAppend(colDefs, "#colName# #dbType#");
colTypes[colName] = meta[i].typeName;
}
//writedump(colDefs);
//var checkSql = "SELECT * FROM INFORMATION_SCHEMA.SYSTEM_TABLES WHERE TABLE_NAME = '#ucase(arguments.name)#';"
//var res = queryExecute(checkSql, {}, { datasource: arguments.dsn });
//if (res.recordCount EQ 0) {
var createSql = "DROP TABLE IF EXISTS #arguments.name#;
CREATE TEMPORARY TABLE #arguments.name#(#arrayToList(colDefs, ', ')#) ON COMMIT PRESERVE ROWS";
queryExecute(createSql, {}, { datasource: arguments.dsn });
//}
//writedump(meta); writedump(createSql);abort;
// 2. Цикл вставки
var insertSql = "INSERT INTO #arguments.name# (#cols#) VALUES (#repeatString("?,", listLen(cols)-1)#?)";
for (var row in arguments.data) {
var rowData = [];
for (var col in cols) {
arrayAppend(rowData, formatValue(row[col], colTypes[col]));
}
queryExecute(insertSql, rowData, { datasource: arguments.dsn });
}
return arguments.name;
}
/**
* Удалить временные таблицы (опционально, база сама почистит при закрытии коннекта)
*/
public void function drop(required string names, string dsn = "inmem") {
var list = arguments.names;
if (!isArray(list)) list = [list];
for (var name in list) {
queryExecute("DROP TABLE IF EXISTS #name#", {}, { datasource: arguments.dsn });
}
}
// --- Вспомогательная функция маппинга типов ---
private string function mapType(required string luceeType) {
switch (arguments.luceeType) {
case "VARCHAR": case "CHAR": case "LONGVARCHAR": return "LONGVARCHAR";
case "INTEGER": case "SMALLINT": case "TINYINT": return "INT";
case "BIGINT": return "BIGINT";
case "DOUBLE": case "FLOAT": case "REAL": return "DOUBLE";
case "BOOLEAN": case "BIT": return "BIT";
case "DECIMAL": case "NUMERIC": return "DECIMAL(128,32)";
case "TIMESTAMP": case "DATE": case "TIME": return "TIMESTAMP";
default: return "LONGVARCHAR"; // И тут тоже
}
}
private function formatValue(required any val, required string targetType) {
var isTextType = listFindNoCase("VARCHAR,CHAR,LONGVARCHAR", arguments.targetType);
// 1. Обработка пустых значений
if (isNull(arguments.val) || (isSimpleValue(arguments.val) && len(trim(arguments.val)) == 0)) {
// Если это текстовое поле — сохраняем пустую строку как есть
if (isTextType) {
return "";
}
// Для чисел, дат и булев — отдаем настоящий SQL NULL
return javaCast("null", "");
}
// 2. Форматирование непустых дат
if (listFindNoCase("DATE,TIMESTAMP,TIME", arguments.targetType)) {
return dateFormat(arguments.val, "yyyy-mm-dd") & " " & timeFormat(arguments.val, "HH:mm:ss");
}
// 3. Числа и непустой текст отдаем как есть
return arguments.val;
}
}
+568
View File
@@ -0,0 +1,568 @@
<cfimport prefix="m" taglib="../lib"/>
<cfimport prefix="d" taglib="../lib/data"/>
<cfparam name="ATTRIBUTES.dt_finish" type="date" default=#createDateTime(year(Now()),month(Now()),1,0,0,0)#/>
<cfparam name="ATTRIBUTES.dt_start" type="date" default=#dateAdd('m',-1,ATTRIBUTES.dt_finish)#/>
<cfparam name="ATTRIBUTES.output" type="string" default="report"/>
<cfparam name="ATTRIBUTES.debug" type="boolean" default=false/>
<cfparam name="ATTRIBUTES.tenant_wz_index" type="string"/>
<cfparam name="ATTRIBUTES.restrict_to_own_instances" type="boolean" default=true/>
<cfparam name="ATTRIBUTES.full_join" type="boolean" default=true/>
<cfset hours=dateDiff('h', ATTRIBUTES.dt_start, dateAdd("d",1,ATTRIBUTES.dt_finish))/> <!--- *** нужно для каждого фрагмента --->
<cfif hours LE 0>
<cfthrow message="Некорректный период" detail="Период должен быть больше 0"/>
</cfif>
<!---
*** ВНИМАНИЕ! Вычитать минимальный бесплатный лимит (free tier) нужно после агрегации!
Исходная последовательность расчетов сначала делает агрегацию по месяцу, при расчете по дням нужно менять порядок
--->
<!---
Скрытое противоречие, которое требует разрешения: если у нас несколько периодов тарификации в одном месяце (по разной цене), ИЗ КАКОГО вычитать минимальный лимит?
Еще сложность, если лимита хватает на несколько периодов тарификации, как его распределять по периодам. Если тарификация строго по месяцам, этой проблемы нет.
Это можно сделать процедурно, но сложность расчета все возрастает
--->
<!--- *** Проверить, что у нас не биллится закрытый период (закрытый следующим) - возможно, с учетом line_key
- насколько вижу, прошлый период закрывается, причем концы отрезки закрытые (конец включается). Считаем, что этой проблемы нет
--->
<!--- QoQ does not support join syntax, and of course outer joins, at least of Lucee 5.4--->
<!--- а внешние джойны хорошо подсвечивают ошибки в данных, особенно помогает full --->
<cfmodule template="crm_contract.cfm" wz="WZ#numberFormat(ATTRIBUTES.tenant_wz_index,'00000')#" out="qSpecification"/>
<cfif (ATTRIBUTES.debug)>
qSpecification (from CRM)
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qSpecification#/>
<cfflush/>
</cfif>
<cfquery name="qComputingAge">
select max(capacity_resource.timestamp) as dt_load from ngcloud_ru.capacity_resource
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qComputingAge#/>
<cfflush/>
</cfif>
<cfquery name="qStorageAge" datasource="rpt">
select max(capacity_storage.timestamp) as dt_load from ngcloud_ru.capacity_storage
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qStorageAge#/>
<cfflush/>
</cfif>
<cfquery name="qS3VolAge">
select max(timestamp_addition) as dt_load from s3billing.bucket_stat
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3VolAge#/>
<cfflush/>
</cfif>
<cfquery name="qS3OpsTrfAge">
select max(time) as dt_load from s3billing.usage_bucket_by_user
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3OpsTrfAge#/>
<cfflush/>
</cfif>
<cfset qFreeTier=queryNew("code,free_tier","CF_SQL_VARCHAR,CF_SQL_NUMERIC",[['paas.s3.ceph.vol-m',1],['paas.s3.ceph.get-m',10],['paas.s3.ceph.put-m',1],['paas.s3.ceph.trf-m',100]])/>
<!--- -- все-таки пришлось прибить гвоздями - в каталоге нет даже бесплатного лимита, не то что скидочной политики
-- придется добавить в каталог
-- бесплатный лимит есть только для S3 HOT (в месяц в единицах измерения) --->
<!--- ******************************** здесь нужно организовать пересечение версионной спецификации с анализируемым диапазоном дат --->
<!--- Проверка: включаем дату смены цены в диапазон и смотрим --->
<!--- Важно: если в периоде было 2+ цены, то строки должны быть замножены, а цена за период перестает иметь смысл (не показывать же среднее арифметическое, это неправильно) --->
<!--- Left Outer Join with Free Tier --->
<cfquery name="qSpec" dbtype="query">
select s.catalogName as svc, s.component, s.serviceCode as service_code, s.fullCode as sku_code, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price
,s.componentDiscount as component_discount, s.serviceDiscount as service_discount, s.serviceQuantity as service_quantity, s.componentQuantity as component_quantity
,coalesce(s.serviceQuantity*(100-s.serviceDiscount)/100*s.priceWVat*(100-s.componentDiscount)/100,0) as discounted_price
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, f.free_tier
from qSpecification s, qFreeTier f
WHERE s.fullCode=f.code
AND s.serviceType = 3
UNION
select s.catalogName as svc, s.component, s.serviceCode as service_code, s.fullCode as sku_code, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price
,s.componentDiscount as component_discount, s.serviceDiscount as service_discount, s.serviceQuantity as service_quantity, s.componentQuantity as component_quantity
,coalesce(s.serviceQuantity*(100-s.serviceDiscount)/100*s.priceWVat*(100-s.componentDiscount)/100,0) as discounted_price
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, 0
from qSpecification s
WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsqltype="cf_sql_varchar" list="true">)
AND s.serviceType = 3
</cfquery>
<!--- <cfloop query="qSpec">
<cfset qSpec.sku_code = (len(qSpec.code) GT 2) ? qSpec.code.left(qSpec.code.len() - 2) : qSpec.code/>
</cfloop> --->
<cfif (ATTRIBUTES.debug)>
qSpec (Left Outer Join with Free Tier)
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qSpec#/>
<cfflush/>
</cfif>
<!--- -- 1. Create a specialized expression index matching your date filters
CREATE INDEX idx_capacity_storage_perf_test ON ngcloud_ru.capacity_storage (
vdc_id,
(timestamp::date),
storage_profile_types_id
)
INCLUDE (limit, used);
-- 2. Optional: Standardize tenant scanning since it currently falls back to Seq Scan
CREATE INDEX idx_tenant_wzcode ON ngcloud_ru.tenant (wzcode); --->
<!--- *** Нужно еще реализовать в модели данных (каталоге) политики бесплатного минимума (free tier), сейчас прибито гвоздями --->
<cfquery name="qComputing">
select upper(clientid) as wz, vdc.name as vdc_name,
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,
capacity_resource.timestamp::date as dt,
vdc.name as dimension_1,
null as dimension_2
from ngcloud_ru.capacity_resource
join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
where tenant.clientid = <cfqueryparam cfsqltype="cf_sql_varchar" value="WZ#numberFormat(ATTRIBUTES.tenant_wz_index,'00000')#"/>
AND capacity_resource.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND capacity_resource.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dateAdd("d",1,attributes.dt_finish)#/>
group by tenant.clientid, vdc.name, capacity_resource.timestamp::date
order by tenant.clientid, vdc.name, capacity_resource.timestamp::date;
</cfquery>
<cfif (ATTRIBUTES.debug)>
qComputing
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qComputing#/>
<cfflush/>
</cfif>
<!--- если захочется сгенерить литерал таблицы соответствия
with a as (
select * from ngcloud_ru.storage_profile_types
)
select '('||id||', '''|| name||''', '''||
case
when name ilike '%fastssd%' then 'fstssd'
when name ilike '%ssd%' then 'ssd'
when name ilike '%sata%' then 'sata'
else '' end
||''''||')'
from a
--->
<!--- переделать storage_profile_types с расшифровкой ssd, sata, можно прямо до каталожного кода --->
<!--- очень долго исполняется --->
<cfquery name="qStorage">
select upper(clientid) as wz, vdc.name as vdc_name,
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, -- *** это не тип, а профайл
sum("limit")/1024./4./<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/> as gb_m_limit,
sum(used)/1024./4./<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/> as gb_m_used,
capacity_storage.timestamp::date as dt,
vdc.name as dimension_1,
storage_profile_types.name as dimension_2
from ngcloud_ru.capacity_storage
join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
join ngcloud_ru.storage_profile_types on capacity_storage.storage_profile_types_id=storage_profile_types.id
where tenant.clientid = <cfqueryparam cfsqltype="cf_sql_varchar" value="WZ#numberFormat(ATTRIBUTES.tenant_wz_index,'00000')#"/>
AND capacity_storage.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND capacity_storage.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dateAdd("d",1,attributes.dt_finish)#/>
group by tenant.clientid, vdc.name, storage_profile_types.name, capacity_storage.timestamp::date
order by tenant.clientid, vdc.name, storage_profile_types.name, capacity_storage.timestamp::date
;
</cfquery>
<!--- create index IX_capacity_storage_sort on ngcloud_ru.capacity_storage(vdc_id,storage_profile_types_id,timestamp); --->
<cfif (ATTRIBUTES.debug)>
qStorage
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qStorage#/>
<cfflush/>
</cfif>
<!---
"placement" "size_gb" "get_10k" "put_10k" "bytes_sent_gb"
HOT_FREE_LIMIT 1 10 1 100
--->
<!--- надо дисконтировать не ГБ-часы а ГБ-месяцы --->
<cfquery name="qS3Vol">
select /* *** ниже дублирование кода агрегации - round ceil etc, обратить внимание при правке */
/* *** переделать на avg - сначала агрегировать по бакетам*/
(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_stat.timestamp_addition::date as dt
,bucket_info.name /*|| ' - ' || 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 dimension_1
,null as dimension_2
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.owner = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
AND bucket_stat.timestamp_addition >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND bucket_stat.timestamp_addition < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dateAdd("d",1,attributes.dt_finish)#/>
group by bucket_stat.owner, bucket_stat.placement_id, bucket_info.name, bucket_stat.timestamp_addition::date
order by bucket_stat.owner, bucket_stat.placement_id, bucket_info.name, bucket_stat.timestamp_addition::date;
</cfquery>
<cfif (ATTRIBUTES.debug)>
qS3Vol
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3Vol#/>
<cfflush/>
</cfif>
<cfquery name="qS3OpsTrf">
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
,'WZ'||lpad(user_info.name, 5, '0') as wz
,usage_bucket_by_user.time::date as dt
,bucket_info.name <!--- || ' - ' || 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 dimension_1
,null as dimension_2
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 user_info.name = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
AND usage_bucket_by_user.time >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND usage_bucket_by_user.time < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#dateAdd("d",1,attributes.dt_finish)#/>
group by user_info.name, bucket_info.placement_id, bucket_info.name, usage_bucket_by_user.time::date
order by user_info.name, bucket_info.placement_id, bucket_info.name, usage_bucket_by_user.time::date;
</cfquery>
<!--- здесь метрики уже сгруппированы по дням. Умножать на цену еще рано, надо сильно позже --->
<cfif (ATTRIBUTES.debug)>
qS3OpsTrf
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3OpsTrf#/>
<cfflush/>
</cfif>
<!--- собрали все метрики в один резалтсет --->
<!--- Здесь мы знаем единицы измерения, причем надежнее, чем в спецификации --->
<cfquery name="qUnifiedMetric" dbType="query">
select dt, wz, code + '.vcpu' as code, core_h as raw_metric, core_h as metric,
dimension_1, 'vdc' as dimension_1_name, dimension_2, NULL as dimension_2_name, 'ядро-ч' as unit
from qComputing
union all
select dt, wz, code + '.ram' as code, gb_h as raw_metric, gb_h as metric,
dimension_1, 'vdc' as dimension_1_name, dimension_2, NULL as dimension_2_name, 'ГБайт-ч' as unit
from qComputing
union all
select dt, wz, code, gb_m_used as raw_metric, gb_m_used as metric,
dimension_1, 'vdc' as dimension_1_name, dimension_2, 'storage policy' as dimension_2_name, 'ГБайт-м' as unit
from qStorage
union all
select dt, wz, code + '.vol', vol_b as raw_metric, vol_gb as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name, 'ГБайт-м' as unit
from qS3Vol
union all
select dt, wz, code + '.get', get as raw_metric, get_10k as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name, '10тыс.шт.' as unit
from qS3OpsTrf
union all
select dt, wz, code + '.put', put as raw_metric, put_10k as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name, '10тыс.шт.' as unit
from qS3OpsTrf
union all
select dt, wz, code + '.trf', bytes_sent as raw_metric, bytes_sent_gb as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name, 'ГБайт' as unit
from qS3OpsTrf
order by dt, code, dimension_1, dimension_2 <!--- /*dimension_*_name нужно только в качестве заголовка *** потом оптимизировать */ --->
</cfquery>
<!--- clean extra spaces to work around QoQ bug --->
<cfloop query="qUnifiedMetric">
<cfset qUnifiedMetric.dimension_1_name=trim(qUnifiedMetric.dimension_1_name)/>
<cfset qUnifiedMetric.dimension_2_name=trim(qUnifiedMetric.dimension_2_name)/>
<!--- *** возможно, и с другими полями придется сделать то же самое --->
</cfloop>
<!--- This is a known, long-standing quirk in ColdFusion's Query of Queries (QoQ) engine.When you perform a UNION ALL across queries where a character column has a dynamic or varying string length (like your concatenated m.dimension_1_name + 'x'), QoQ identifies the longest string in that column across all result sets. It then treats the column like a fixed-length CHAR database field and pads all shorter values with trailing spaces to match that maximum length. --->
<!--- лучше никогда не связываться с QoQ, но если все исполнять в исходной бд, это скорее мегаселект с большим числом CTE --->
<!--- надо было сразу делать в БД. Причем сводить в одну, чтобы избежать нескольких источников данных... можно было бы и так добиться, но все равно сводим в буферную базу --->
<cfif (ATTRIBUTES.debug)>
qUnifiedMetric
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qUnifiedMetric#/>
<cfflush/>
</cfif>
<!--- умножаем на цену, вычисляем дисконт (довольно поздно) и форматируем
*** здесь нужно учесть версионность. Самым простым кажется декомпозировать по дням и снова собрать
*** только как нам это сделать без нормального SQL движка
Может быть, процедурная обработка не будет выглядеть ужасно, и не хуже SQL
В конце концов, можно просто В ЦИКЛЕ обсчитать диапазон по дням, выполнив запрос 30 раз, или сколько надо
Если будет тупить - оптимизируем
--->
<!--- Для декомпозиции на более мелкие объекты мы можем как полумеру - профильтровать по конкретному бакету или ВМ-ВДЦ
Что не нравится - для ВМ у нас может быть целое дерево нижележащих объектов вместо линейного списка бакетов --->
<!--- Вычитание Free Tier можно начинать с начала месяца - для этого можно сгруппировать по месяцам --->
<!--- Кажется, здесь еще рано агрегировать, если мы хотим OUTER JOIN --->
<!--- агрегировать вообще не надо в этом компоненте, а стоимость вычислять после агрегации --->
<cfset db = new MemoryDbHelper()/>
<cfset db.register("qUnifiedMetric", qUnifiedMetric, "inmem")/>
<cfset db.register("qSpec", qSpec, "inmem")/>
<cfquery name="qChargeDaily" datasource="inmem">
SELECT
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
<d:field title="Код компонента">s.code as s_code</d:field>
<d:field title="Код метрики">m.code as m_code</d:field>
<d:field title="Код услуги">s.service_code</d:field>
<d:field title="код SKU">s.sku_code</d:field>
<d:field title="Услуга">s.svc</d:field>
<d:field title="Компонент">s.component</d:field>
<d:field title="Ед.изм.">m.unit</d:field>
<!--- <d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">
s.price*(100-s.discount)/100 as discounted_price
</d:field> --->
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">s.discounted_price</d:field>
<d:field title="Цена с НДС" cfSqlType="CF_SQL_NUMERIC">s.price</d:field>
<d:field cfSqlType="CF_SQL_NUMERIC">s.component_quantity</d:field>
<d:field title="Скидка%" cfSqlType="CF_SQL_NUMERIC">s.component_discount</d:field>
<d:field cfSqlType="CF_SQL_NUMERIC">s.service_quantity</d:field>
<d:field cfSqlType="CF_SQL_NUMERIC">s.service_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="serviceStart" cfSqlType="CF_SQL_TIMESTAMP">s.serviceStart</d:field>
<d:field title="serviceEnd" cfSqlType="CF_SQL_TIMESTAMP">s.serviceEnd</d:field>
<d:field title="serviceHash">s.serviceHash</d:field>
<d:field title="serviceCustomName">s.serviceCustomName</d:field>
<d:field title="dt" cfSqlType="CF_SQL_TIMESTAMP">m.dt</d:field>
<d:field title="free tier" cfSqlType="CF_SQL_NUMERIC">s.free_tier</d:field>
<d:field>m.dimension_1</d:field>
<d:field>m.dimension_1_name</d:field>
<d:field>m.dimension_2</d:field>
<d:field>m.dimension_2_name</d:field>
</d:field_set>
FROM qSpec s
<cfif attributes.full_join>
FULL OUTER JOIN
<cfelse>
LEFT OUTER JOIN
</cfif>
qUnifiedMetric m ON (
s.sku_code=m.code
AND s.serviceStart <= m.dt
AND (m.dt <= s.serviceEnd OR s.serviceEnd IS NULL)
)
WHERE 1=1
<cfif #attributes.restrict_to_own_instances#>
AND m.wz=<cfqueryparam cfsqltype="cf_sql_varchar" value=#request.auth.wz#/>
</cfif>
<!--- <!--- таким замысловатым способом мы реализуем FULL JOIN --->
UNION ALL
SELECT NULL, m.code, NULL, NULL, NULL, NULL, NULL, 0,
0, 0, 0, 0, 0, m.raw_metric, m.metric, NULL, NULL, NULL, NULL, m.dt, 0, m.dimension_1, m.dimension_1_name, m.dimension_2, m.dimension_2_name
FROM qUnifiedMetric as m
WHERE m.code NOT IN (<cfqueryparam value="#ValueList(qSpec.sku_code)#" cfsqltype="cf_sql_varchar" list="true">)
UNION ALL
SELECT s.code, NULL, s.service_code, s.sku_code, s.svc, s.component, s.unit, s.discounted_price,
s.price, s.component_quantity, s.component_discount, s.service_quantity, s.service_discount, NULL, NULL, s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, NULL, 0, NULL, NULL, NULL, NULL
FROM qSpec as s
WHERE s.code NOT IN (<cfqueryparam value="#listRemoveDuplicates(ValueList(qUnifiedMetric.code))#" cfsqltype="cf_sql_varchar" list="true">) --->
</cfquery>
<!--- <cfdump var=#qChargeDaily# abort=true/> --->
<cfif (ATTRIBUTES.debug)>
qChargeDaily
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qChargeDaily#/>
<cfflush/>
</cfif>
<!--- Из-за детализации придется вынести вычитание free_tier на более поздний этап --->
<!--- Здесь агрегировано за месяц, но детализировано по измерениям --->
<cfquery name="qChargeMonthly" dbType="query">
SELECT
<d:field_set titleMapOut="chargeTitleMap" lengthOut="chargeFieldCount">
<d:field title="Артикул по договору">s_code</d:field>
<d:field title="Артикул по системам">m_code</d:field>
<d:field title="Артикул">coalesce(sku_code, m_code) as code</d:field>
<d:field title="Код услуги">service_code</d:field>
<d:field title="SKU code">sku_code</d:field>
<d:field title="Услуга по каталогу">svc</d:field>
<d:field title="Пользовательское наименование">serviceCustomName</d:field>
<d:field title="Компонент">component</d:field>
<d:field title="Ед.изм.">unit</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">discounted_price</d:field>
<d:field cfSqlType="CF_SQL_NUMERIC">price</d:field>
<d:field cfSqlType="CF_SQL_NUMERIC">component_quantity</d:field>
<d:field cfSqlType="CF_SQL_NUMERIC">component_discount</d:field>
<d:field cfSqlType="CF_SQL_NUMERIC">0 as discounted_price2</d:field>
<d:field cfSqlType="CF_SQL_NUMERIC">service_quantity</d:field>
<d:field cfSqlType="CF_SQL_NUMERIC">service_discount</d:field>
<d:field title="Сырая метрика" cfSqlType="CF_SQL_NUMERIC">sum(raw_metric) as raw_metric</d:field>
<d:field title="Приведенный объем" cfSqlType="CF_SQL_NUMERIC">sum(metric) as metric</d:field>
<d:field title="serviceStart" cfSqlType="CF_SQL_TIMESTAMP">serviceStart</d:field>
<d:field title="serviceEnd" cfSqlType="CF_SQL_TIMESTAMP">serviceEnd</d:field>
<d:field title="dt_billed_from" cfSqlType="CF_SQL_TIMESTAMP">min(dt) as dt_billed_from</d:field>
<d:field title="dt_billed_to" cfSqlType="CF_SQL_TIMESTAMP">max(dt) as dt_billed_to</d:field>
<d:field title="serviceHash">serviceHash</d:field>
<d:field title="free tier" cfSqlType="CF_SQL_NUMERIC">free_tier</d:field>
<d:field>dimension_1</d:field>
<d:field>dimension_1_name</d:field>
<d:field>dimension_2</d:field>
<d:field>dimension_2_name</d:field>
<d:field title="Год" cfSqlType="CF_SQL_INTEGER">year(dt) as year</d:field>
<d:field title="Месяц" cfSqlType="CF_SQL_INTEGER">month(dt) as month</d:field>
</d:field_set>
from qChargeDaily
group by s_code, m_code, svc, service_code, sku_code, serviceCustomName, component, unit, discounted_price, price, component_quantity, component_discount, service_quantity, service_discount, serviceStart, ServiceEnd, serviceHash, free_tier, year(dt), month(dt),
dimension_1_name, dimension_1, dimension_2_name, dimension_2
order by year(dt), month(dt), sku_code, m_code, serviceStart, dimension_1, dimension_2
</cfquery>
<!--- в связи с бедностью языка QoQ (нет CASE) грубо патчим резалтсет --->
<!--- <cfloop query="qChargeMonthly">
<cfset qChargeMonthly.code = (len(code) GT 0) ? code : m_code/><!--- имя в левой части должно быть квалифицировано запросом (резалтсетом) --->
<cftry>
<cfset qChargeMonthly.discounted_price2 = precisionEvaluate("service_quantity*(100-service_discount)/100*price*(100-component_discount)/100")/><!--- имя в левой части должно быть квалифицировано запросом (резалтсетом) --->
<cfcatch type="any">
<cfdump var=#cfcatch#/>
<cfdump var=#queryGetRow(qChargeMonthly, qChargeMonthly.currentRow)# abort=true/>
</cfcatch>
</cftry>
</cfloop>
--->
<cfif (ATTRIBUTES.debug)>
qChargeMonthly
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qChargeMonthly#/>
<cfflush/>
</cfif>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<!--- <cfflush/> --->
<cfset var report={}/>
<cfset report.qComputingAge = qComputingAge/>
<cfset report.qStorageAge = qStorageAge/>
<cfset report.qS3VolAge = qS3VolAge/>
<cfset report.qS3OpsTrfAge = qS3OpsTrfAge/>
<cfset report.qSpecification = qSpecification/>
<cfset report.qCharge = qChargeMonthly/>
<cfset report.qFreeTier = qFreeTier/><!--- мы не применяем скидку на уровне детализации, а только после агрегации. На самом деле правильно было бы агрегировать натуральные метрики, а умножать на цену уже агрегаты --->
<cfset report.chargeTitleMap = chargeTitleMap/><!--- нужен для экспорта в XL --->
<!--- <cfset report.chargeFieldCount = chargeFieldCount/> --->
<cfset "CALLER.#ATTRIBUTES.output#" = report/>
<cfexit method="exittag"/>
+27 -11
View File
@@ -2,11 +2,11 @@
<cfimport prefix="d" taglib="../lib/data"/>
<cfparam name="ATTRIBUTES.wz"/>
<cfparam name="ATTRIBUTES.crm_cache_base" default="https://elma-cache.adl.nubes.ru/contract_ls.cfm"/>
<!--- <cfparam name="ATTRIBUTES.crm_cache_base" default="https://elma-cache.adl.nubes.ru/contract_ls.cfm"/> --->
<cfparam name="ATTRIBUTES.out" default="qComponent"/>
<cfhttp method="get"
url="#ATTRIBUTES.crm_cache_base#?wz=#ATTRIBUTES.wz#"
url="#request.crm_cache_base#?wz=#ATTRIBUTES.wz#"
timeout=10
charset="utf-8"
result="res"
@@ -14,9 +14,22 @@
</cfhttp>
<cfset local={}/>
<cfset company=deserializeJson(res.fileContent)/>
<!--- <cfdump var=#company#/> --->
<!--- --->
<cfset safeJsonString = REReplace(res.fileContent, '(:\s*)(-?\d+\.\d{16,})', '$1"$2"', "ALL")/><!--- очень длинные числа завернуть в кавычки --->
<!--- <cfdump var=#safeJsonString#/> --->
<cftry>
<cfset company=deserializeJson(safeJsonString)/>
<cfcatch type="ANY">
Cannot deserialize CRM response
result:
<cfdump var=#res#/>
safeJsonString:
<cfdump var=#safeJsonString#/>
</cfcatch>
</cftry>
<!---<cfset company=deserializeJson(res.fileContent)/> --->
<!---<cfdump var=#company#/> --->
<!--- *** это не слишком надежно, некорректно представленный NULL (обыкновенно представляемый пустой строкой) может все сломать. Да и корректный --->
<!--- мартышкин труд собирать этот запрос из JSON , может, лучше было динамически --->
<cfset qComponent = queryNew("
@@ -42,12 +55,14 @@
,componentCode
,component
,measureShort
,quantity
,componentQuantity
,priceWVat
,discount
,componentDiscount
,atomicCode
,fullCode
")/>
",
"varChar,varChar,varChar,varChar,integer,varChar,integer,varChar,varChar,varChar,timestamp,timestamp,decimal,decimal,varChar,integer,varChar,varChar,varChar,decimal,decimal,decimal,varChar,varChar")/>
<!--- если не указывать типы, он пытается их определить сам, потом проблемы с union и арифметикой --->
<!--- this.nullSupport = true in Application.cfc ... no way --->
<cfloop array=#company.contracts# item="local.contract"><!--- имя массива писать в ##, иначе думает, что это строковый литерал --->
@@ -78,11 +93,12 @@
componentCode=local.component.componentCode,
component=local.component.component,
measureShort=local.component.measureShort,
quantity=local.component.quantity,
componentQuantity=local.component.quantity,
priceWVat=local.component.priceWVat,
discount=local.component.discount,
componentDiscount=local.component.discount,
atomicCode=local.component.atomicCode,
fullCode="#local.component.componentCode#-#listGetAt('i,f,m',local.service.serviceType)#"
//fullCode="#local.component.componentCode#-#listGetAt('i,f,m',local.service.serviceType)#"
fullCode="#reReplace(local.component.componentCode, '-.$', '')#" //truncate -m garbage
}])/>
</cfloop>
</cfloop>
-428
View File
@@ -1,428 +0,0 @@
<cfimport prefix="m" taglib="../lib"/>
<cfimport prefix="d" taglib="../lib/data"/>
<cfparam name="ATTRIBUTES.dt_finish" type="date" default=#createDateTime(year(Now()),month(Now()),1,0,0,0)#/>
<cfparam name="ATTRIBUTES.dt_start" type="date" default=#dateAdd('m',-1,ATTRIBUTES.dt_finish)#/>
<cfparam name="ATTRIBUTES.output" type="string" default="report"/>
<cfparam name="ATTRIBUTES.debug" type="boolean" default=false/>
<cfparam name="ATTRIBUTES.tenant_wz_index" type="string"/>
<cfset hours=dateDiff('h', ATTRIBUTES.dt_start, ATTRIBUTES.dt_finish)/> <!--- *** нужно для каждого фрагмента --->
<!---
*** ВНИМАНИЕ! Вычитать минимальный лимит нужно после агрегации!
Исходная последовательность расчетов сначала делает агрегацию по месяцу, при расчете по дням нужно менять порядок
--->
<!---
Скрытое противоречие, которое требует разрешения: если у нас несколько периодов тарификации в одном месяце (по разной цене), ИЗ КАКОГО вычитать минимальный лимит?
Еще сложность, если лимита хватает на несколько периодов тарификации, как его распределять по периодам. Если тарификация строго по месяцам, этой проблемы нет.
Это можно сделать процедурно, но сложность расчета все возрастает
--->
<!--- *** Проверить, что у нас не биллится закрытый период (закрытый следующим) - возможно, с учетом line_key
- насколько вижу, прошлый период закрывается, причем концы отрезки закрытые (конец включается). Считаем, что этой проблемы нет
--->
<!--- QoQ does not support join syntax, and of course outer joins, at least of Lucee 5.4--->
<!--- а внешние джойны хорошо подсвечивают ошибки, особенно помогает full --->
<cfmodule template="crm_contract.cfm" wz="WZ01348" out="qSpecification"/><!--- WZ01200 --->
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qSpecification#/>
<cfflush/>
</cfif>
<cfquery name="qComputingAge">
select max(capacity_resource.timestamp) as dt_load from ngcloud_ru.capacity_resource
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qComputingAge#/>
<cfflush/>
</cfif>
<cfquery name="qStorageAge" datasource="rpt">
select max(capacity_storage.timestamp) as dt_load from ngcloud_ru.capacity_storage
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qStorageAge#/>
<cfflush/>
</cfif>
<cfquery name="qS3VolAge">
select max(timestamp_addition) as dt_load from s3billing.bucket_stat
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3VolAge#/>
<cfflush/>
</cfif>
<cfquery name="qS3OpsTrfAge">
select max(time) as dt_load from s3billing.usage_bucket_by_user
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3OpsTrfAge#/>
<cfflush/>
</cfif>
<cfset qFreeTier=queryNew("code,free_tier","CF_SQL_VARCHAR,CF_SQL_NUMERIC",[['paas.s3.ceph.vol-m',1],['paas.s3.ceph.get-m',10],['paas.s3.ceph.put-m',1],['paas.s3.ceph.trf-m',100]])/>
<!--- -- все-таки пришлось прибить гвоздями - в каталоге нет даже бесплатного лимита, не то что скидочной политики
-- придется добавить в каталог
-- бесплатный лимит есть только для S3 HOT (в месяц в единицах измерения) --->
<!--- здесь нужно организовать пересечение версионной спецификации с анализируемым диапазоном дат --->
<cfquery name="qSpec" dbtype="query">
select s.catalogName as svc, s.component, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, f.free_tier
from qSpecification s, qFreeTier f
WHERE s.fullCode=f.code
UNION
select s.catalogName as svc, s.component, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, 0
from qSpecification s
WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsqltype="cf_sql_varchar" list="true">)
</cfquery>
<!--- <cfdump var=#qSpec# abort=true/> --->
<!--- <cfloop from=#attributes.dt_start# to=#attributes.dt_finish# step="#CreateTimeSpan( 1, 0, 0, 0 )#" index="local.dt">
<cfdump var=#dateFormat(local.dt,"YYYY-MM-DD")#/>--->
<!--- *** Нужно еще реализовать в модели данных политики бесплатного минимума, сейчас пробито гвоздями --->
<cfquery name="qComputing">
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
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 '%-v1cl6%' then 'iaas.ngc.i28'
else '['||vdc.name||']' end as code,
sum(cpu_used/cpu_speed)/4. as core_h,
sum(mem_used)/1024/4. as gb_h,
capacity_resource.timestamp::date as dt
from ngcloud_ru.capacity_resource
join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#ATTRIBUTES.tenant_wz_index#/>
AND capacity_resource.timestamp::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND capacity_resource.timestamp::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by tenant.wzcode, vdc.name, capacity_resource.timestamp::date
order by tenant.wzcode, vdc.name, capacity_resource.timestamp::date;
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qComputing#/>
<cfflush/>
</cfif>
<!--- переделать storage_profile_types с расшифровкой ssd, sata, можно прямо до каталожного кода --->
<cfquery name="qStorage">
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
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 '%-v1cl6%' THEN 'iaas.ngc.i28'
ELSE '['||vdc.name||']' END
|| CASE
WHEN storage_profile_types.name ILIKE '%-SSD%' THEN '.ssd-m'
WHEN storage_profile_types.name ILIKE '%-SAS' THEN '.sas-m'
WHEN storage_profile_types.name ILIKE '%-SATA' THEN '.sata-m'
ELSE '.['|| storage_profile_types.name ||']' END
as code,
storage_profile_types.name, -- *** это не тип, а профайл
sum("limit")/1024./4 as gb_h_limit, -- 4 потому, что сбор раз в 15 минут
sum(used)/1024./4 as gb_h_used,
capacity_storage.timestamp::date as dt
from ngcloud_ru.capacity_storage
join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
join ngcloud_ru.storage_profile_types on capacity_storage.storage_profile_types_id=storage_profile_types.id
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#ATTRIBUTES.tenant_wz_index#/>
AND capacity_storage.timestamp::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND capacity_storage.timestamp::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by tenant.wzcode, vdc.name, storage_profile_types.name, capacity_storage.timestamp::date
order by tenant.wzcode, vdc.name, storage_profile_types.name, capacity_storage.timestamp::date;
</cfquery>
<!---
"placement" "size_gb" "get_10k" "put_10k" "bytes_sent_gb"
HOT_FREE_LIMIT 1 10 1 100
--->
<!--- надо дисконтировать не ГБ-часы а ГБ-месяцы --->
<cfquery name="qS3Vol">
select /* *** ниже дублирование кода агрегации - round ceil etc, обратить внимание при правке */
(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>) as vol_B
,ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>/1024/1024/1024) as vol_GB
,greatest(ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>
/1024/1024/1024)-
case when bucket_stat.placement_id = 1 then 1 else 0 end, 0) as vol_GB_free_tier_subtracted /*считается не в точности так, как в функции*/
,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_stat.timestamp_addition::date as dt
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.owner = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
AND bucket_stat.timestamp_addition::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND bucket_stat.timestamp_addition::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by bucket_stat.owner, bucket_stat.placement_id, bucket_stat.timestamp_addition::date
order by bucket_stat.owner, bucket_stat.placement_id, bucket_stat.timestamp_addition::date;
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3Vol#/>
<cfflush/>
</cfif>
<cfquery name="qS3OpsTrf">
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
,ceil(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
,greatest(ceil(sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
)/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 10 ELSE 0 END, 0
) as get_10k_free_tier_subtracted
,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
,ceil(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
,greatest(ceil(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) - CASE WHEN bucket_info.placement_id = 1 THEN 1 ELSE 0 END, 0
) as put_10k_free_tier_subtracted
,(sum(bytes_sent)) as bytes_sent
,ceil(sum(bytes_sent)/1024/1024/1024) as bytes_sent_GB
,greatest(ceil(sum(bytes_sent)/1024/1024/1024) - CASE WHEN bucket_info.placement_id = 1 THEN 100 ELSE 0 END, 0
) as bytes_sent_GB_free_tier_subtracted
,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
,'WZ'||lpad(user_info.name, 5, '0') as wz
,usage_bucket_by_user.time::date as dt
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 user_info.name = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
AND usage_bucket_by_user.time::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND usage_bucket_by_user.time::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by user_info.name, bucket_info.placement_id, usage_bucket_by_user.time::date
order by user_info.name, bucket_info.placement_id, usage_bucket_by_user.time::date;
</cfquery>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3OpsTrf#/>
<cfflush/>
</cfif>
<!--- собрали все метрики в один резалтсет --->
<cfquery name="qUnifiedMetric" dbType="query">
select dt, wz, code || '.vcpu-m' as code, core_h as raw_metric, core_h as metric from qComputing
union all
select dt, wz, code || '.ram-m' as code, gb_h as raw_metric, gb_h as metric from qComputing
union all
select dt, wz, code, gb_h_used as raw_metric, gb_h_used as metric from qStorage
union all
select dt, wz, code || '.vol-m', vol_b as raw_metric, vol_gb as metric from qS3Vol
union all
select dt, wz, code || '.get-m', get as raw_metric, get_10k as metric from qS3OpsTrf
union all
select dt, wz, code || '.put-m', put as raw_metric, put_10k as metric from qS3OpsTrf
union all
select dt, wz, code || '.trf-m', bytes_sent as raw_metric, bytes_sent_gb as metric from qS3OpsTrf
order by dt, code
</cfquery>
<!--- <cfdump var=#qUnifiedMetric#/> --->
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qUnifiedMetric#/>
<cfflush/>
</cfif>
<!--- умножаем на цену, вычисляем дисконт (довольно поздно) и форматируем
*** здесь нужно учесть версионность. Самым простым кажется декомпозировать по дням и снова собрать
*** только как нам это сделать без нормального SQL движка
Может быть, процедурная обработка не будет выглядеть ужасно, и не хуже SQL
В конце концов, можно просто В ЦИКЛЕ обсчитать диапазон по дням, выполнив запрос 30 раз, или сколько надо
Если будет тупить - оптимизируем
--->
<!--- Для декомпозиции на более мелкие объекты мы можем как полумеру - профильтровать по конкретному бакету или ВМ-ВДЦ
Что не нравится - для ВМ у нас может быть целое дерево нижележащих объектов вместо линейного списка бакетов --->
<!--- Вычитание Free Tier можно начинать с начала месяца - для этого можно сгруппировать по месяцам --->
<!--- Кажется, здесь еще рано агрегировать, если мы хотим OUTER JOIN --->
<cfquery name="qChargeDaily" dbType="query">
SELECT
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
<d:field title="Артикул">s.code</d:field>
<d:field title="Артикул">m.code as m_code</d:field>
<d:field title="Услуга">s.svc</d:field>
<d:field title="Компонент">s.component</d:field>
<d:field title="Ед.изм.">s.unit</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">s.price*(100-s.discount)/100 as discounted_price</d:field>
<d:field title="Цена с НДС" cfSqlType="CF_SQL_NUMERIC">s.price</d:field>
<d:field title="Скидка%" cfSqlType="CF_SQL_NUMERIC">s.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="serviceStart" cfSqlType="CF_SQL_TIMESTAMP">s.serviceStart</d:field>
<d:field title="serviceEnd" cfSqlType="CF_SQL_TIMESTAMP">s.serviceEnd</d:field>
<d:field title="serviceHash">s.serviceHash</d:field>
<d:field title="serviceCustomName">s.serviceCustomName</d:field>
<d:field title="dt" cfSqlType="CF_SQL_TIMESTAMP">m.dt</d:field>
<d:field title="free tier" cfSqlType="CF_SQL_NUMERIC">s.free_tier</d:field>
</d:field_set>
FROM qSpec as s, qUnifiedMetric as m
WHERE m.wz=<cfqueryparam cfsqltype="cf_sql_varchar" value=#request.auth.wz#/>
AND s.code=m.code
AND s.serviceStart <= m.dt
AND (m.dt <= s.serviceEnd OR s.serviceEnd IS NULL)
<!--- таким замысловатым способом мы реализуем FULL JOIN --->
UNION
SELECT NULL, m.code, NULL, NULL, NULL, NULL, NULL, NULL, m.raw_metric, m.metric, NULL, NULL, NULL, NULL, m.dt, 0
FROM qUnifiedMetric as m where m.code NOT IN (<cfqueryparam value="#ValueList(qSpec.code)#" cfsqltype="cf_sql_varchar" list="true">)
UNION
SELECT s.code, NULL, s.svc, s.component, s.unit, s.price*(100-s.discount)/100, s.price, s.discount, NULL, NULL, s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, NULL, 0
FROM qSpec as s where s.code NOT IN (<cfqueryparam value="#listRemoveDuplicates(ValueList(qUnifiedMetric.code))#" cfsqltype="cf_sql_varchar" list="true">)
order by m.dt, s.code
</cfquery>
<cfquery name="qChargeMonthly" dbType="query">
SELECT
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
<d:field title="Артикул по договору">code</d:field>
<d:field title="Артикул по системам">m_code</d:field>
<d:field title="Услуга по каталогу">svc</d:field><!--- *** добавить пользовательское --->
<d:field title="Компонент">component</d:field>
<d:field title="Ед.изм.">unit</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">discounted_price</d:field>
<d:field title="Сырая метрика" cfSqlType="CF_SQL_NUMERIC">sum(raw_metric) as raw_metric</d:field>
<d:field title="Приведенный объем" cfSqlType="CF_SQL_NUMERIC">sum(metric) as metric</d:field>
<d:field title="Объем к оплате" cfSqlType="CF_SQL_NUMERIC">sum(metric) - free_tier as chargeable_metric</d:field>
<d:field title="Сумма к оплате" cfSqlType="CF_SQL_NUMERIC">(sum(metric) - free_tier)*discounted_price as charge</d:field>
<d:field title="serviceStart" cfSqlType="CF_SQL_TIMESTAMP">serviceStart</d:field>
<d:field title="serviceEnd" cfSqlType="CF_SQL_TIMESTAMP">serviceEnd</d:field>
<d:field title="serviceHash">serviceHash</d:field>
<d:field title="serviceCustomName">serviceCustomName</d:field>
<d:field title="free tier" cfSqlType="CF_SQL_NUMERIC">free_tier</d:field>
<d:field title="Год" cfSqlType="CF_SQL_INTEGER">year(dt) as year</d:field>
<d:field title="Месяц" cfSqlType="CF_SQL_INTEGER">month(dt) as month</d:field>
</d:field_set>
from qChargeDaily
group by code, m_code, svc, serviceCustomName, component, unit, discounted_price, serviceStart, ServiceEnd, serviceHash, free_tier, year(dt), month(dt)
order by year(dt), month(dt), code, m_code
</cfquery>
<!--- в связи с бедностью языка QoQ (нет CASE) грубо патчим резалтсет --->
<cfloop query="qChargeMonthly">
<cfset qChargeMonthly.chargeable_metric = (chargeable_metric GT 0) ? chargeable_metric : 0/> <!--- имя в левой части должно быть квалифицировано запросом (резалтсетом) --->
<cfset qChargeMonthly.charge = (charge GT 0) ? charge : 0/>
</cfloop>
<!--- тут могут быть подводные камни с копированием и переиспользованием объектов --->
<!--- <cfif isQuery(qTotal)>
<cfquery name="qTotalNew" dbType="query">
select * from qTotal
union all
select * from qCharge
</cfquery>
<cfset qTotal = qTotalNew/>
<cfelse>
<cfset qTotal = qCharge/>
</cfif>
<cfdump var=#qTotal#/> --->
<!--- <cfdump var=#qChargeMonthly#/> --->
<!--- *** проверить краевые даты --->
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qChargeMonthly#/>
<cfflush/>
</cfif>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfflush/>
<cfset var report={}/>
<cfset report.qComputingAge = qComputingAge/>
<cfset report.qStorageAge = qStorageAge/>
<cfset report.qS3VolAge = qS3VolAge/>
<cfset report.qS3OpsTrfAge = qS3OpsTrfAge/>
<cfset report.qCharge = qChargeMonthly/>
<cfset report.chargeTitleMap = titleMap/>
<cfset report.chargeFieldCount = fieldCount/>
<cfset "CALLER.#ATTRIBUTES.output#" = report/>
<cfexit method="exittag"/>
+234 -63
View File
@@ -7,34 +7,48 @@
<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)#/>
<cfset hours=dateDiff('h',dt_start,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 tenant_wz_index=val(mid(request.auth.wz,3,5))/><!--- cut off WZ --->
<cfset hours=dateDiff('h', dt_start, dateAdd('d', 1, dt_finish))/>
<cfset restrict_to_own_instances = true>
<cfparam name="WZ" default=#request.auth.wz#/>
<cfif request.auth.wz EQ 'WZ01112'><!--- грубо --->
<cfset restrict_to_own_instances = false>
<cfelse>
<cfset WZ=request.auth.wz/><!--- без баловства --->
</cfif>
<cfset tenant_wz_index=val(mid(WZ,3,5))/><!--- cut off WZ --->
<cfif isDefined("output_xls")><cfmodule template="mod/payg.cfm"
<cfif isDefined("output_xls")><cfmodule template="mod/compute_payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
tenant_wz_index=#tenant_wz_index#
full_join=true
output="report"
debug=#isDefined("DEBUG")#
/><layout:xml qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.xml"/>
restrict_to_own_instances=#restrict_to_own_instances#
/>
<layout:xml qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.xml"/>
<cfabort/>
</cfif><cfif isDefined("output_json")><cfmodule template="mod/payg.cfm"
</cfif><cfif isDefined("output_json")><cfmodule template="mod/compute_payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
tenant_wz_index=#tenant_wz_index#
full_join=true
output="report"
debug=#isDefined("DEBUG")#
restrict_to_own_instances=#restrict_to_own_instances#
/><!--- <cfdump var=#report#/><cfabort/> ---><layout:json qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.json"/>
<cfabort/>
</cfif><!---
---><layout:page section="header" pageInfo=#pageInfo#>
<layout:attribute name="title">
<cfoutput><b>Отчет по PAYG</b> #request.auth.wz#</cfoutput>
<cfoutput><b>Отчет по PAYG</b> #WZ#</cfoutput>
</layout:attribute>
<layout:attribute name="controls">
<!---skip filter link, filter is not implemented--->
@@ -42,58 +56,91 @@
</layout:attribute>
</layout:page>
<!--- Внимание! округление вверх при расчете метрик get, put S3. Расчетные суммы могут содержать погрешности округления, поэтому окончательный расчет стоимости (количество * цену) должен выполняться в бухгалтерской программе.
<!--- Внимание! округление вверх при расчете метрик get, put S3. Расчетные суммы могут содержать погрешности округления, поэтому окончательный расчет стоимости (количество * цену) выполняeтся в бухгалтерской программе.
<b>Если у клиента больше 1 действующего допника с услугами PAYG, то данные в отчете могут быть замножены, в этом случае считать вручную</b> --->
<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')#"/>
<input type="submit" style="cursor:pointer;"/>
<input type="submit" name="DEBUG" value="DEBUG" style="cursor:pointer;"/><!--- ********* --->
<form method="post" action="#request.thisPage#" style="display:inline"><!--- не надо делать action="" - параметр, затесавшийся в URL будет затирать параметр формы, и заметить это трудно --->
Период с <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')#"/>
<cfif request.auth.wz EQ 'WZ01112'><!--- очень грубо --->
WZ <input type="text" name="wz" value="#WZ#"/>
</cfif>
<input type="submit" style="cursor:pointer;"/>
<input type="submit" name="DEBUG" value="DEBUG" style="cursor:pointer;"/><!--- ********* --->
</form>
<cfset month_start = createDate(year(now()), month(now()), 1)/>
<a href="?dt_start=#dateFormat(dateAdd('m', -1, month_start),'YYYY-MM-DD')#&dt_finish=#dateFormat(dateAdd('d', -1, month_start),'YYYY-MM-DD')#&wz=#WZ#">
прошлый месяц
</a>
<a href="?dt_start=#dateFormat(createDate(year(now()), month(now()), 1),'YYYY-MM-DD')#&dt_finish=#dateFormat(createDate(year(now()), month(now()), daysInMonth(now())),'YYYY-MM-DD')#&wz=#WZ#">
текущий месяц
</a>
<br/>
<!--- <cfflush/> --->
<cfmodule template="mod/payg.cfm"
<cfmodule template="mod/compute_payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
tenant_wz_index=#tenant_wz_index#
output="report"
debug=#isDefined("DEBUG")#
debug=#structKeyExists(Form,"DEBUG")#
restrict_to_own_instances=#restrict_to_own_instances#
/>
Часов в периоде: <b>#hours#</b> Строк в отчете: <b>#report.qCharge.recordCount#</b><br/>
Актуальность данных:
VCD Computing: <b>#dateFormat(report.qComputingAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qComputingAge.dt_load,'HH:MM:SS')#</b>
VCD Storage: <b>#dateFormat(report.qStorageAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qStorageAge.dt_load,'HH:MM:SS')#</b>
S3 хранение: <b>#dateFormat(report.qS3VolAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3VolAge.dt_load,'HH:MM:SS')#</b>
S3 операции и трафик: <b>#dateFormat(report.qS3OpsTrfAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3OpsTrfAge.dt_load,'HH:MM:SS')#</b>
Цены GPL<br/>
VCD Computing: <b<cfif datediff("h",report.qComputingAge.dt_load, Now()) GT 24> class="err"</cfif>>
#dateFormat(report.qComputingAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qComputingAge.dt_load,'HH:mm:ss.lXX')#
</b>
VCD Storage: <b<cfif datediff("h",report.qStorageAge.dt_load, Now()) GT 24> class="err"</cfif>>
#dateFormat(report.qStorageAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qStorageAge.dt_load,'HH:mm:ss.lXX')#</b>
S3 хранение: <b<cfif datediff("h",report.qS3VolAge.dt_load, Now()) GT 24> class="err"</cfif>>
#dateFormat(report.qS3VolAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3VolAge.dt_load,'HH:mm:ss.lXX')#
</b>
S3 операции и трафик: <b<cfif datediff("h",report.qS3OpsTrfAge.dt_load, Now()) GT 24> class="err"</cfif>>
#dateFormat(report.qS3OpsTrfAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3OpsTrfAge.dt_load,'HH:mm:ss.lXX')#
</b>
<br/>
Цены по данным договора из CRM (через elma-cache). Учитываются метрики по S3, vCloud (кроме GPU). Внимание! количество за период *** округляется вверх, но делается это после всех агрегаций.
<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="1%"></th>
<th width="7%" colspan="2">Артикул</th>
<th width="3%">С</td>
<th width="3%">По</td>
<th width="3%">Кол-во (метрика)</td>
<th width="3%">Free Tier</td>
<th width="7%">Оплачиваемый объем</th>
<th width="3%">Ед.изм.</th>
<th width="3%">Цена &#8381; с НДС</th>
<th width="3%">Стоимость &#8381; с НДС</th>
<th width="3%">&nbsp;&nbsp;&nbsp;</th>
<th width="10%">Услуга</th>
<th width="10%">Компонент</th>
<th width="3%">Ед.изм.</th>
<td width="3%">Цена &#8381; до ск. с НДС</td>
<td width="3%">Цена &#8381; co ск. с НДС без окр.</td>
<!--- <td width="3%">Цена &#8381; co ск. с НДС без окр.v2</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="10%">Клиентс. назв.</td> --->
<!--- <td width="3%">GPL с НДС</td>
@@ -112,63 +159,187 @@ S3 операции и трафик: <b>#dateFormat(report.qS3OpsTrfAge.dt_load,
</thead>
<!--- *** Обнаружена засада: возможно, группировка (сортировка) отображаемого вывода нужна по размерности детализации, а не по коду
Но: агрегация скорее нужна по коду
Видимо, придется на фронте перетасовывать?
--->
<!--- Еще засада: если я делегирую агрегацию на фронт, мне нужно и вычет free_tier делегировать на фронт (помня еще, что он на месяц, а не на произвольный период - наверно, можно его вычитать всегда, а больше месяца не агрегировать... или умножать на число полных месяцев) --->
<!--- видимо, Free Tier и единицу измерения нужно брать и для строк, которые не найдены в договоре
(еше можно было бы туда для полноты добавлять GPL цену, но для этого нужен каталог)
Добавить реквизиты договора? --->
<cfset accum_cost_total = 0/>
<cfoutput query="report.qCharge" group="year">
<cfoutput group="month">
<tr><td colspan="99"><b>#year#-#numberFormat(month,"00")#</b></td></tr>
<tr><td colspan="99"><cfif month GT 0><b>#year#-#numberFormat(month,"00")#</b></cfif></td></tr>
<cfoutput group="svc">
<cfset var acc = 0/>
<cfset var accum_cost_svc = 0/>
<cfset var accum_metric_svc = 0/>
<tr>
<td bgcolor="##ccc"></td>
<td bgcolor="##ccc" colspan="9">
<b>#svc#</b>
<td bgcolor="##ccc" colspan="99">
#svc# <b>#service_code#</b>
<cfif len(serviceCustomName)>(#serviceCustomName#)</cfif>
<cfif serviceHash EQ "">(не найдено в договоре)</cfif>
</td>
</tr>
<cfoutput>
<cfset acc += isNumeric(charge) ? charge : 0/>
<tr>
<td>#currentRow#</a></td>
<td>#code# <cfif code EQ "">(#m_code#)</cfif></td>
<td class="r">#metric#</td>
<td class="r" style="font-size:120%; padding:0 1em;">#chargeable_metric#</td>
<td class="r">#discounted_price#</td>
<td class="r">#numberFormat(charge,".00")#</td>
<td><!--- #acc# ---></td>
<td>#svc#</td>
<td>#component#</td>
<td class="c">#unit#</td>
<!--- <td>#user_description#</td> --->
<!--- <td class="r">#price#</td>
<td class="r">#discount#</td> --->
</tr>
</cfoutput>
<cfoutput group="code">
<cfoutput group="serviceStart"> <!--- может быть, для ясности надо в каждой строке писать точный период --->
<cfset accum_metric_svc += isNumeric(metric) ? metric : 0/><!--- агрегация по метрике возможна только на уровне компонента --->
<tr>
<td></td>
<td colspan="10">#code# <cfif code EQ "">(#m_code#)</cfif></td>
<td>#svc#</td>
<td colspan="99">#component# </td>
</tr>
<cfset accum_metric_dimension_1 = 0/>
<cfoutput group="dimension_1">
<cfset accum_metric_dimension_1 += isNumeric(metric) ? metric : 0/>
<tr class="i">
<td>#currentRow#</td>
<td class="r">#dimension_1_name#</td>
<td>#dimension_1#</td>
<td class="c">#dateFormat(dt_billed_from,"YYYY-MM-DD")#</td>
<td class="c">#dateFormat(dt_billed_to,"YYYY-MM-DD")#</td>
<td class="r"><cfif metric NEQ 0 AND isNumeric(metric)>#round(metric,3)#</cfif></td>
<td><!--- free tier ---></td>
<td class="r" style="font-size:120%; padding:0 1em;"></td>
<td class="c">#unit#</td>
<td class="r">#gaussianRoundSafe(discounted_price,2)#</td>
<td class="r"><!--- здесь не вычисляется стоимость, потому что Free Tier вычитается после агрегации (скидочные политики реализовывать будет непросто) ---></td>
<td></td>
<td></td>
<!--- сырые исходные данные без округлений --->
<td class="r gray">#price#</td>
<td class="r gray">#discounted_price#</td>
<!--- <td class="r gray">#discounted_price2#</td> --->
<td class="r gray">#service_quantity#</td>
<td class="r gray">#service_discount#</td>
<td class="r gray">#component_quantity#</td>
<td class="r gray">#component_discount#</td>
</tr>
</cfoutput>
<cfset final_metric_dimension_1 = ceiling(accum_metric_dimension_1)/>
<cfset cost_dimension_1 = (isNumeric(discounted_price)) ? precisionEvaluate("gaussianRound(max(final_metric_dimension_1 - free_tier, 0)*gaussianRound(discounted_price, 2), 2)"):0/>
<tr class="b">
<td></td>
<td colspan="4" class="r">Итого #code# <cfif code EQ "">(#m_code#)</cfif>:</td>
<td class="r"><cfif final_metric_dimension_1 NEQ 0 AND isNumeric(final_metric_dimension_1)>#round(final_metric_dimension_1,3)#</cfif></td>
<td class="r">#free_tier#</td>
<td class="r" style="font-size:120%; padding:0 1em;"><cfif final_metric_dimension_1 NEQ 0 AND isNumeric(final_metric_dimension_1) AND isNumeric(free_tier)>#max(final_metric_dimension_1 - free_tier, 0)#</cfif></td>
<td class="c">#unit#</td>
<td class="r">#gaussianRoundSafe(discounted_price,2)#</td><!--- если цена менялась в периоде, неправильно показыввать последнюю - но мы не тестировали с изменением в периоде --->
<!--- непонятно, как вычитать Free Tier для неполных месяцев. Если с 15 по 15 - вычитать 2 раза, вычитать (каким-то сложным образом) 1 раз или ни разу --->
<!--- видимо, нужно сформировать тест кейсы на неполные месяцы. Где взять биллинговые данные для этого ВПР --->
<td class="r">#cost_dimension_1#</td>
<cfset accum_cost_svc = precisionEvaluate("accum_cost_svc + cost_dimension_1")/><!--- простое сложение дает ошибку округления и лишние знаки, но и precisionEvaluate не выглядит слишком надежным --->
<td>#svc# <!--- #service_code# ---></td>
<td colspan="99">#component#</td>
</tr>
</cfoutput><!--- serviceStart --->
</cfoutput><!--- code --->
<cfset accum_cost_total += isNumeric(accum_cost_svc) ? accum_cost_svc : 0/>
<tr>
<td bgcolor="##eee" style="border-bottom:2px solid gray;"></td>
<td bgcolor="##eee" colspan="4" class="r" style="border-bottom:2px solid gray;"><b>Итого</b> &nbsp;&nbsp;&nbsp; #svc#:</td>
<td bgcolor="##eee" colspan="1" class="r b" style="border-bottom:2px solid gray;">#NumFmt(acc,2)#</td>
<td colspan="12" style="border-bottom:2px solid gray;"></td>
<td bgcolor="##eee" colspan="9" class="r" style="border-bottom:2px solid gray;">
<b>Итого </b> &nbsp;&nbsp;&nbsp;
<cfif len(service_code)>
#svc# <b>#service_code#</b>
<cfelse>
(#m_code#)
</cfif>
<cfif month GT 0>за #year#-#numberFormat(month,"00")#</cfif>:
</td>
<td bgcolor="##eee" class="r b" style="border-bottom:2px solid gray;">#accum_cost_svc#</td>
<td bgcolor="##eee" colspan="99" style="border-bottom:2px solid gray;"></td>
</tr >
</cfoutput>
<tr><td colspan="99">&nbsp;</td></tr>
</cfoutput>
</cfoutput>
<cfoutput><tr class="b"><td colspan="99">Total: #accum_cost_total# </td></tr></cfoutput>
</table>
<!--- <cfdump var=#qCharge#/> --->
<!--- <cfdump var=#report.qSpecification#/> --->
<h3>Спецификация</h3>
<table class="worktable">
<tr>
<th width="5%">Договор</th>
<th width="15%">Доп. соглашение</th>
<th width="3%">Тип оплаты</th>
<th width="20%">Пользовательское наименование</th>
<th width="3%">Кол-во по строке</th>
<th width="3%">Скидка по строке</th>
<th width="5%">НОУ</th>
<th width="5%">КОУ</th>
<th width="10%">Компонент</th>
<th width="5%">Артикул</th>
<th width="5%">Кол-во</th>
<th width="5%">Ед.изм.</th>
<th width="5%">Цена с НДС</th>
<th width="5%">Скидка %</th>
<th width="5%">Цена со скидкой с НДС</th>
</tr>
<cfoutput query="report.qSpecification">
<tr>
<td>#contractOutsideId#</td>
<td>#additionalAgreementName#</td>
<td class="c"<cfif serviceType EQ 3> style="background:lightgreen"</cfif>>#serviceType#</td>
<td>#serviceCustomName#</td>
<td class="c">#serviceQuantity#</td>
<td class="r">#serviceDiscount#</td>
<td class="c">#dateFormat(serviceStart, "YYYY-MM-DD")#</td>
<td class="c">#dateFormat(serviceEnd, "YYYY-MM-DD")#</td>
<td>#component#</td>
<td>#componentCode#</td>
<td class="r">#componentQuantity#</td>
<td class="c">#measureShort#</td>
<td class="r">#gaussianRoundSafe(priceWVat,2)# | #priceWVat#</td>
<td class="r">#componentDiscount#</td>
<td class="r">#gaussianRoundSafe((100-serviceDiscount)/100*(100-componentDiscount)/100*priceWVat,7)#</td>
</tr>
</cfoutput>
</table>
<!---<cfdump var=#report.qCharge#/> --->
<!--- <cfdump var=#this.datasources[request.ds].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'); --->
<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>