作为中国现金流量表剖析的后续篇,今朝我们谈一个隐秘函数。在几周之前,当用户问我:“SuiteScript能调用标准财务报表的运行结果么?”。我的答案是“No!”,斩钉截铁。但是直到我们从代码堆里发现了这个API - nlapiRunReport。终于又让我们对NetSuite的理解又进了一层,再一次印证了我的认知局限,学艺不精、惭愧不已。
Google、GitHub了一圈,只有一篇有价值的文章,亦来自C站。
NetSuite 记录通过脚本从报表中读取数据的过程_吴铁柱儿的博客-CSDN博客
chatGPT的答案其实也是来自网络,只说了是隐藏,并且最好别用。因为会被进化掉,只是概率较小。
基于上面提供的细节,在咨询了NetSuite开发大神后,我们渐渐扒出了这个API的全貌。
nlapiRunReport 是一个隐藏的API,遵从SuiteScript 1.0规范,其用途是对financial Report的代码调用并获取其运行结果中的数据。在2.0被进化掉了,但是由于在某些官方本土化Bundle中仍然使用,所以只能逐步Phase Out,个人判断永远不会。因为,作为一个平台产品是不能不考虑既有的用户自己做的那些客制功能的,那是客户的资产了,如果弃用这个API,影响太大。想想各类End Of Available的那些基础API,你就知道退出成本有多高了。
API定义:nlapiRunReport(reportId, reportSettings)
- 参数reportId是财务报表的Internal ID;
- 参数reportSettings是财务报表的搜索参数;
除了这个主体API外,还有几个附属函数。基本上可以顾名思义。
nlobjReportSettings(paramsObj.periodFrom, paramsObj.periodTo)
getColumnHierarchy()
getVisibleChildren()
getRowHierarchy()
getSummaryLine()
由于没有任何的官方文档可以参考,我们通过分析现有代码,做了Demo出来,实证有效。搞开发的同学参考之。
以下是Restlet脚本,Get Function = cn.rlv1.fin.standard.reports.run
由于环境有差异,请将Line 40处的参数调整为自己的。
var params = {
subsidiary: '3', // Sub 3
fiscalCalendar:'1',//Calendar 1
periodFrom: '258', // Feb 2023
periodTo: '259', // Feb 2023
};
Enjoy your coding!
/**
* NetSuite知识会分享 2023-3-25
*
* nlapiRunReport 是一个隐藏的API,其用途是对financial Report的代码调用并获取其运行结果中的数据。
*
* 本脚本是个范例,指示如何使用此API。
*
* 场景为:调用General Ledger Report(id=293)报表的Bank Type的科目汇总金额。报表的参数包括:Sub=3,起止期间都是 258。
*
* Params
* subsidiary - mandatory for One-World account, will throw error if it is One-World account and subsidiary is not given
* fiscalCalendar - mandatory if it is One-World account and Multi-Calendar feature has been enabled
* periodFrom - mandatory
* periodTo - mandatory
* location - optional
* department - optional
* classification - optional
*
* Return
* String representative of below json object.
* {
startBalanceCurrent: 0,
endBalanceCurrent: 0,
}
*/
if (!cn) {
var cn = {};
}
cn.rlv1 = cn.rlv1 || {};
cn.rlv1.fin = cn.rlv1.fin || {};
cn.rlv1.fin.standard = cn.rlv1.fin.standard || {};
cn.rlv1.fin.standard.reports = cn.rlv1.fin.standard.reports || {};
cn.rlv1.fin.standard.reports = function() {
var isOW;
var reportByPeriod;
var generalLedgerColumnName;
var unrealizedGainAndLossColumnName;
this.doGet = function(params) {
var params = {
subsidiary: '3', // Sub 3
fiscalCalendar:'1',//Calendar 1
periodFrom: '258', // Feb 2023
periodTo: '259', // Feb 2023
};
isOW = isOW();
var paramsObj = validateParams(params);
// check language, etc
var langPref = getUserLanguage();
nlapiLogExecution('DEBUG', 'doGet', 'langPref=' + langPref);
if (langPref === 'zh_CN') {
generalLedgerColumnName = '\u4f59\u989d';
} else {
generalLedgerColumnName = 'Balance'; //Balance是general ledger report的特别列,其他报告不一定有
}
// Reference getPeriod comments for the structure of periodObj
var periodObj = getPeriod(paramsObj.periodFrom, paramsObj.periodTo, paramsObj.fiscalCalendar);
var rtn = {
startBalanceCurrent: 0,
endBalanceCurrent: 0,
};
// Beginning balance of cash and cash equivalents - Current Period
if (periodObj.periodFrom.lastPeriod.id) {
rtn.startBalanceCurrent = runGeneralLedgerReport({
'subsidiary': paramsObj.subsidiary,
'periodFrom': periodObj.periodFrom.lastPeriod.id,
'periodTo': periodObj.periodFrom.lastPeriod.id
});
}
// Ending balance of cash and cash equivalents - Current Period
rtn.endBalanceCurrent = runGeneralLedgerReport({
'subsidiary': paramsObj.subsidiary,
'periodFrom': paramsObj.periodTo,
'periodTo': paramsObj.periodTo
});
nlapiLogExecution('AUDIT', 'doGet', 'rtn=' + JSON.stringify(rtn));
return JSON.stringify(rtn);
}
/**
* Validate passed-in parameters. Will throw exception if meets below condition:
* passed-in parameters is undefined, null or ''
* One-World account and subsidiary is undefined, null or ''
* One-World account with Multi-Calendar feature been enabled and fiscalCalendar is undefined, null or ''
* periodFrom is undefined, null or ''
* periodTo is undefined, null or ''
* @return object of pass in parameters if there are no exception
*/
function validateParams(params) {
if (!params) {
throw nlapiCreateError('RunStandardReportError', 'Run report params are undefined', true);
}
nlapiLogExecution('AUDIT', 'validateParams', 'params=' + JSON.stringify(params));
if (!isObject(params)) {
var paramsObj = JSON.parse(params);
} else {
paramsObj = params;
}
// subsidiary should not be undefined/null/'' for OW
if (isOW && !paramsObj.subsidiary) {
throw nlapiCreateError('RunStandardReportError', 'Subsidiary is mandatory for OW account', true);
}
if (isOW && isMultiCalendarEnabled() && !paramsObj.fiscalCalendar) {
throw nlapiCreateError('RunStandardReportError', 'fiscalCalendar is mandatory for OW account with Multi-Calendar enabled', true);
}
// periodFrom should not be undefined/null/''
if (!paramsObj.periodFrom) {
throw nlapiCreateError('RunStandardReportError', 'period from is mandatory', true);
}
if (!paramsObj.periodTo) {
throw nlapiCreateError('RunStandardReportError', 'period to is mandatory', true);
}
return paramsObj;
}
function runGeneralLedgerReport(paramsObj) {
nlapiLogExecution('AUDIT', 'runGeneralLedgerReport', 'paramsObj=' + JSON.stringify(paramsObj));
return runStandardReport(293, composeGeneralLedgerReportSettings(paramsObj), generalLedgerColumnName)
}
function runStandardReport(reportId, reportSettings, targetColumnName) {
try {
setupUserPref();
var pivotTable = nlapiRunReport(reportId, reportSettings);
if (!pivotTable) {
return 0;
}
//取目标列的ID,用于后面的取值
var colHier = pivotTable.getColumnHierarchy();
var colChildren = colHier.getVisibleChildren();
var targetCol = null;
for ( var colIdx in colChildren) {
nlapiLogExecution('DEBUG', 'runStandardReport', 'target col label: ' + colChildren[colIdx].getLabel());
if (colChildren[colIdx].getLabel() === targetColumnName) {
targetCol = colChildren[colIdx];
break;
}
}
if (targetCol === null) {
throw nlapiCreateError('RunStandardReportError', 'Column ' + targetColumnName + ' does not exists', true);
}
recoverUserPref();
//取目标列的汇总行的金额
var summaryRow = pivotTable.getRowHierarchy().getSummaryLine();
return summaryRow !== null ? summaryRow.getValue(targetCol) : 0;
} catch (ex) {
nlapiLogExecution('ERROR', 'runStandardReport Error', ex);
recoverUserPref();
throw ex;
}
}
function composeGeneralLedgerReportSettings(paramsObj) {
var reportSettings = composeReportSettings(paramsObj);
// Only show Bank types
// Active Account by Type > Account Type
reportSettings.addCriteria('aatype,account,saccttype,x,x', 'Bank');
if (paramsObj.location) {
reportSettings.addCriteria('regtx,tranline,klocation,x,alltranlineloc', paramsObj.location);
}
if (paramsObj.department) {
reportSettings.addCriteria('regtx,tranline,kdepartment,x,alltranline29', paramsObj.department);
}
if (paramsObj.classification) {
reportSettings.addCriteria('regtx,tranline,kclass,x,alltranline6', paramsObj.classification);
}
return reportSettings;
}
function composeReportSettings(paramsObj) {
var settings = new nlobjReportSettings(paramsObj.periodFrom, paramsObj.periodTo);
if (paramsObj.subsidiary) {
settings.setSubsidiary(paramsObj.subsidiary);
}
return settings;
}
function setupUserPref() {
var userPref = nlapiLoadConfiguration('userpreferences');
this.reportByPeriod = userPref.getFieldValue('reportbyperiod');
nlapiLogExecution('AUDIT', 'setupUserPref', 'reportByPeriod = ' + this.reportByPeriod);
if (this.reportByPeriod !== 'FINANCIALS') {
userPref.setFieldValue('reportbyperiod', 'FINANCIALS');
nlapiSubmitConfiguration(userPref);
}
}
function recoverUserPref() {
if (this.reportByPeriod !== 'FINANCIALS') {
var userPref = nlapiLoadConfiguration('userpreferences');
userPref.setFieldValue('reportbyperiod', this.reportByPeriod);
nlapiSubmitConfiguration(userPref);
}
}
function getPeriod(periodFrom, periodTo, fiscalCalendar) {
var startDateInfo = getStartDateInfo(periodFrom, periodTo);
var filterExpression = [
[
"isadjust",
"is",
"F"
],
"AND",
[
"isinactive",
"is",
"F"
],
"AND",
[
"isquarter",
"is",
"F"
],
"AND",
[
"isyear",
"is",
"F"
],
"AND",
[
[
"startdate",
"on",
startDateInfo.periodFromObj.lastPeriod
],
"OR",
[
"startdate",
"on",
startDateInfo.periodFromObj.samePeriodLastFiscalYear
],
"OR",
[
"startdate",
"on",
startDateInfo.periodFromObj.lastPeriodOneFiscalYearAgo
],
"OR",
[
"startdate",
"on",
startDateInfo.periodToObj.lastPeriod
],
"OR",
[
"startdate",
"on",
startDateInfo.periodToObj.samePeriodLastFiscalYear
],
"OR",
[
"startdate",
"on",
startDateInfo.periodToObj.lastPeriodOneFiscalYearAgo
]
]
];
if (fiscalCalendar) {
filterExpression.push('AND');
filterExpression.push([
"fiscalcalendar",
"is",
fiscalCalendar
]);
}
var periodNameCol = new nlobjSearchColumn('periodname');
var startDateCol = new nlobjSearchColumn('startdate');
var columns = [
periodNameCol,
startDateCol
];
var searchresults = nlapiSearchRecord('accountingperiod', null, filterExpression, columns);
nlapiLogExecution('DEBUG', 'getPeriod', 'searchresults = ' + JSON.stringify(searchresults));
var rtn = {
'periodFrom': {
'lastPeriod': {
'startdate': startDateInfo.periodFromObj.lastPeriod
},
'samePeriodLastFiscalYear': {
'startdate': startDateInfo.periodFromObj.samePeriodLastFiscalYear
},
'lastPeriodOneFiscalYearAgo': {
'startdate': startDateInfo.periodFromObj.lastPeriodOneFiscalYearAgo
}
},
'periodTo': {
'lastPeriod': {
'startdate': startDateInfo.periodToObj.lastPeriod
},
'samePeriodLastFiscalYear': {
'startdate': startDateInfo.periodToObj.samePeriodLastFiscalYear
},
'lastPeriodOneFiscalYearAgo': {
'startdate': startDateInfo.periodToObj.lastPeriodOneFiscalYearAgo
}
}
};
for (var i = 0; searchresults != null && i < searchresults.length; i++) {
var searchresult = searchresults[i];
var id = searchresult.getId();
var periodName = searchresult.getValue(periodNameCol);
var startDate = searchresult.getValue(startDateCol);
if (startDate === startDateInfo.periodFromObj.lastPeriod) {
rtn.periodFrom.lastPeriod.id = id;
rtn.periodFrom.lastPeriod.name = periodName
} else if (startDate === startDateInfo.periodFromObj.samePeriodLastFiscalYear) {
rtn.periodFrom.samePeriodLastFiscalYear.id = id;
rtn.periodFrom.samePeriodLastFiscalYear.name = periodName;
} else if (startDate === startDateInfo.periodFromObj.lastPeriodOneFiscalYearAgo) {
rtn.periodFrom.lastPeriodOneFiscalYearAgo.id = id;
rtn.periodFrom.lastPeriodOneFiscalYearAgo.name = periodName;
}
// periodFrom and periodTo might be the same
if (startDate === startDateInfo.periodToObj.lastPeriod) {
rtn.periodTo.lastPeriod.id = id;
rtn.periodTo.lastPeriod.name = periodName;
} else if (startDate === startDateInfo.periodToObj.samePeriodLastFiscalYear) {
rtn.periodTo.samePeriodLastFiscalYear.id = id;
rtn.periodTo.samePeriodLastFiscalYear.name = periodName;
} else if (startDate === startDateInfo.periodToObj.lastPeriodOneFiscalYearAgo) {
rtn.periodTo.lastPeriodOneFiscalYearAgo.id = id;
rtn.periodTo.lastPeriodOneFiscalYearAgo.name = periodName;
}
}
nlapiLogExecution('AUDIT', 'getPeriod', 'periodObj = ' + JSON.stringify(rtn));
return rtn;
}
function getStartDateInfo(periodFrom, periodTo) {
var filterExpression = [
[
"internalid",
"anyof",
periodFrom,
periodTo
]
];
var periodNameCol = new nlobjSearchColumn('periodname');
var lastPeriodCol = new nlobjSearchColumn('formuladate').setFormula("ADD_MONTHS({startdate}, -1)");
var samePeriodLastFiscalYearCol = new nlobjSearchColumn('formuladate').setFormula("ADD_MONTHS({startdate}, -12)");
var lastPeriodOneFiscalYearAgoCol = new nlobjSearchColumn('formuladate').setFormula("ADD_MONTHS({startdate}, -13)");
var columns = [
periodNameCol,
lastPeriodCol,
samePeriodLastFiscalYearCol,
lastPeriodOneFiscalYearAgoCol
];
var searchresults = nlapiSearchRecord('accountingperiod', null, filterExpression, columns);
for (var i = 0; searchresults != null && i < searchresults.length; i++) {
var searchresult = searchresults[i];
var id = searchresult.getId();
if (id === periodFrom) {
var fromPeriodObj = {
'id': periodFrom,
'name': searchresult.getValue(periodNameCol),
'lastPeriod': searchresult.getValue(lastPeriodCol),
'samePeriodLastFiscalYear': searchresult.getValue(samePeriodLastFiscalYearCol),
'lastPeriodOneFiscalYearAgo': searchresult.getValue(lastPeriodOneFiscalYearAgoCol)
};
}
// Return two records even when from and to are the same
if (id === periodTo) {
var toPeriodObj = {
'id': periodFrom,
'name': searchresult.getValue(periodNameCol),
'lastPeriod': searchresult.getValue(lastPeriodCol),
'samePeriodLastFiscalYear': searchresult.getValue(samePeriodLastFiscalYearCol),
'lastPeriodOneFiscalYearAgo': searchresult.getValue(lastPeriodOneFiscalYearAgoCol)
};
}
}
nlapiLogExecution('DEBUG', 'getStartDateInfo', 'fromPeriodObj=' + JSON.stringify(fromPeriodObj));
nlapiLogExecution('DEBUG', 'getStartDateInfo', 'toPeriodObj=' + JSON.stringify(toPeriodObj));
return {
'periodFromObj': fromPeriodObj,
'periodToObj': toPeriodObj
};
}
/**
* Get earliest period that startdate is within startdateFrom and startdateTo.
*/
function getEarliestPeriod(fiscalCalendar, startdateFrom, startdateTo) {
nlapiLogExecution('DEBUG', 'getEarliestPeriod', 'fiscalCalendar=' + fiscalCalendar + ', startdateFrom=' + startdateFrom + ', startdateTo=' + startdateTo);
var filters = getBasePeriodFilter(fiscalCalander);
filters.push("AND").push([
"startdate",
"within",
startdateFrom,
startdateTo
]);
nlapiLogExecution('DEBUG', 'getEarliestPeriod', 'filters=' + JSON.stringify(filters));
var periodNameCol = new nlobjSearchColumn('periodname');
var startDateCol = new nlobjSearchColumn('startdate');
startDateCol.setSort(); // ascending order
var columns = [
periodNameCol,
startDateCol
];
var searchresults = nlapiSearchRecord('accountingperiod', null, filters, columns);
if (!searchresults) {
throw nlapiCreateError('RunStandardReportError', 'Should have at least one period between ' + startdateFrom + " and " + startdateTo, true);
}
nlapiLogExecution('DEBUG', 'getEarliestPeriod', 'return=' + searchresults[0].getValue(periodNameCol));
return searchresults[0].id;
}
function getBasePeriodFilter(fiscalCalendar) {
var baseFilter = [
[
"isadjust",
"is",
"F"
],
"AND",
[
"isinactive",
"is",
"F"
],
"AND",
[
"isquarter",
"is",
"F"
],
"AND",
[
"isyear",
"is",
"F"
]
];
if (fiscalCalander) {
baseFilter.push("AND").push([
"fiscalcalendar",
"anyof",
fiscalCalendar
]);
}
return baseFilter;
}
function getUserLanguage() {
return nlapiLoadConfiguration('userpreferences').getFieldValue('language');
}
/**
* @return true if it is One-World account.
*/
function isOW() {
return nlapiGetContext().getFeature('SUBSIDIARIES');
}
/**
* @return true if Multi-Calendar feature has been enabled.
*/
function isMultiCalendarEnabled() {
return nlapiGetContext().getFeature('MULTIPLECALENDARS');
}
/**
* @return true if Multi-Currency feature has been enabled.
*/
function isMultiCurrencyEnabled() {
return nlapiGetContext().getFeature('MULTICURRENCY');
}
function isObject(bechecked) {
return typeof bechecked === 'object' && bechecked.constructor === Object || Object.prototype.toString.apply(bechecked) === '[object Object]';
}
};
cn.rlv1.fin.standard.reports.run = function(params) {
var standardReports = new cn.rlv1.fin.standard.reports();
return standardReports.doGet(params);
}
如果有任何关于NetSuite的问题,欢迎来谈。我的邮箱:[email protected]