updated advanced pack to 3.12.0:

Reports:

    Non-aggregated columns in Grid report export.
    Normalized table mode for 2-dimensional Grid reports.
    Ability to create internal reports via the UI.
    Ability to show/hide and resize columns in the list report result view.
This commit is contained in:
2026-02-07 16:09:20 +01:00
parent 26db904407
commit f95246f99f
384 changed files with 6184 additions and 3643 deletions

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -25,6 +25,7 @@ use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error\Body;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Select\Where\Item as WhereItem;
use Espo\Core\Utils\Json;
@@ -74,6 +75,7 @@ class PostRunGridPreview implements Action
/**
* @throws BadRequest
* @throws Forbidden
*/
private function fetchData(Request $request): stdClass
{
@@ -83,6 +85,15 @@ class PostRunGridPreview implements Action
throw new BadRequest("No data.");
}
$internalClassName = $data->internalClassName ?? null;
$isInternal = $data->isInternal ?? false;
if (($internalClassName !== null || $isInternal) && !$this->user->isAdmin()) {
throw Forbidden::createWithBody('onlyAdminCanPreviewInternalReports',
Body::create()->withMessageTranslation('onlyAdminCanPreviewInternalReports', Report::ENTITY_TYPE)
);
}
return $data;
}

View File

@@ -0,0 +1,80 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export;
use Espo\Modules\Advanced\Tools\Report\GridType\Helper;
use Espo\Modules\Advanced\Tools\Report\GridType\Result as GridResult;
use RuntimeException;
use stdClass;
class CellValueHelper
{
public function __construct(
private Helper $gridHelper,
) {}
/**
* Only for Grid-2.
*/
public function getCellDisplayValueFromResult(
int $groupIndex,
string $groupValue,
string $column,
GridResult $reportResult,
): mixed {
$groupName = $reportResult->getGroupByList()[$groupIndex];
$dataMap = $reportResult->getNonSummaryData()->$groupName ?? null;
if (!$dataMap instanceof stdClass) {
throw new RuntimeException("No non-summary data for the group '$groupName'.");
}
$value = '';
if ($this->gridHelper->isColumnNumeric($column, $reportResult)) {
$value = 0;
}
if (
property_exists($dataMap, $groupValue) &&
property_exists($dataMap->$groupValue, $column)
) {
$value = $dataMap->$groupValue->$column;
}
if (
!$this->gridHelper->isColumnNumeric($column, $reportResult) &&
!is_null($value)
) {
if (property_exists($reportResult->getCellValueMaps(), $column)) {
if (property_exists($reportResult->getCellValueMaps()->$column, $value)) {
$value = $reportResult->getCellValueMaps()->$column->$value;
}
}
}
if (is_null($value)) {
$value = '';
}
return $value;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,310 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Language;
use Espo\Modules\Advanced\Entities\Report;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\CellFunction;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\CellType;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\DataCell;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\DataRow;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\DateFunction;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\RowType;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\SheetData;
use Espo\Modules\Advanced\Tools\Report\GridType\Result;
use RuntimeException;
use stdClass;
class Grid1DataBuilder
{
private const PRECISION = 2;
public function __construct(
private Language $language,
) {}
public function build(Result $reportResult): SheetData
{
$hasSubListColumns = $reportResult->getSubListColumnList() !== [];
$groupCount = count($reportResult->getGroupByList());
$groupName = $reportResult->getGroupByList()[0] ?? '__STUB__';
$rows = [];
$cells = [
new DataCell(
value: $reportResult->getGroupNameMap()[$groupName] ?? null,
type: CellType::HeadGroup,
),
];
foreach ($reportResult->getColumnList() as $column) {
$cells[] = new DataCell(
value: $reportResult->getColumnNameMap()[$column] ?? null,
type: $this->isNonSummary($reportResult, $column) ? CellType::HeadNonSummary : CellType::HeadSummary,
);
}
$rows[] = new DataRow(
cells: $cells,
type: RowType::Header,
);
foreach ($reportResult->getGrouping()[0] ?? [] as $group) {
$itemRows = $this->buildGroupRows($reportResult, $groupName, $group);
$rows = [...$rows, ...$itemRows];
}
if ($groupCount) {
$cells = [];
$cells[] = new DataCell(
value: $this->language->translateLabel('Total', 'labels', Report::ENTITY_TYPE),
type: CellType::Label,
);
foreach ($reportResult->getColumnList() as $column) {
if (
!in_array($column, $reportResult->getNumericColumnList()) ||
!in_array($column, $reportResult->getAggregatedColumnList())
) {
$cells[] = new DataCell(
value: null,
type: CellType::Empty,
);
continue;
}
$decimalPlaces = $reportResult->getColumnDecimalPlacesMap()->$column ?? null;
$fieldType = $reportResult->getColumnTypeMap()[$column] ?? null;
$function = Grid2NormalizedDataBuilder::getCellFunction($column);
if ($fieldType === FieldType::INT && $function === CellFunction::Avg) {
$decimalPlaces = self::PRECISION;
}
$value = $reportResult->getSums()->$column ?? 0;
$cells[] = new DataCell(
value: $value,
type: CellType::Total,
fieldType: $fieldType,
function: !$hasSubListColumns ? $function : null,
decimalPlaces: $decimalPlaces,
);
}
$rows[] = new DataRow(
cells: $cells,
type: RowType::Total,
);
}
$hasTotalFunctions = !$hasSubListColumns && $groupCount;
return new SheetData(
rows: $rows,
firstSummaryRowNumber: $hasTotalFunctions ? 1 : null,
lastSummaryRowNumber: $hasTotalFunctions ? count($rows) - 2 : null,
);
}
private function isNonSummary(Result $reportResult, string $column): bool
{
return in_array($column, $reportResult->getNonSummaryColumnList());
}
/**
* @return DataRow[]
*/
private function buildGroupRows(Result $reportResult, string $groupName, mixed $group): array
{
$hasSubListColumns = $reportResult->getSubListColumnList() !== [];
$rows = [];
if (!$hasSubListColumns) {
return [
$this->buildGroupTotalRow($reportResult, $groupName, $group, true, true)
];
}
$rows[] = $this->buildGroupTotalRow($reportResult, $groupName, $group, false);
foreach ($this->buildSubRows($reportResult, $group) as $row) {
$rows[] = $row;
}
$rows[] = $this->buildGroupTotalRow($reportResult, $groupName, $group, true);
return $rows;
}
private function buildGroupTotalRow(
Result $reportResult,
string $groupName,
mixed $group,
bool $onlyNumeric,
bool $full = false,
): DataRow {
$hasSubListColumns = $reportResult->getSubListColumnList() !== [];
$cells = [];
if (!$onlyNumeric || $full) {
$cells[] = new DataCell(
value: $this->prepareGroupValue($reportResult, $groupName, $group),
type: CellType::NonSummary,
dateFunction: $this->getDateFunction($groupName),
);
} else {
$cells[] = new DataCell(
value: $this->language->translateLabel('Group Total', 'labels', 'Report'),
type: CellType::Label,
);
}
foreach ($reportResult->getColumnList() as $column) {
$isNumericValue = in_array($column, $reportResult->getNumericColumnList());
if (
$hasSubListColumns && !$onlyNumeric && $isNumericValue ||
$hasSubListColumns && $onlyNumeric && !$isNumericValue ||
$isNumericValue && !in_array($column, $reportResult->getAggregatedColumnList())
) {
$cells[] = new DataCell(
value: null,
type: CellType::Empty,
);
continue;
}
if ($isNumericValue) {
$value = $reportResult->getReportData()->$group->$column ?? 0;
$cells[] = new DataCell(
value: $value,
type: CellType::Summary,
fieldType: $reportResult->getColumnTypeMap()[$column] ?? null,
decimalPlaces: $reportResult->getColumnDecimalPlacesMap()->$column ?? null,
);
} else {
$value = $reportResult->getReportData()->$group->$column ?? null;
$cells[] = new DataCell(
value: $value,
type: CellType::Summary,
fieldType: $reportResult->getColumnTypeMap()[$column] ?? null,
);
}
}
return new DataRow(
cells: $cells,
type: $full || $onlyNumeric ? RowType::DataRow : RowType::HeadDataRow,
);
}
private function prepareGroupValue(Result $reportResult, string $groupName, mixed $group): mixed
{
if ($group) {
$label = $reportResult->getGroupValueMap()[$groupName][$group] ?? $group;
} else {
$label = $this->language->translateLabel('-Empty-', 'labels', 'Report');
}
return $label;
}
private function getDateFunction(string $column): ?DateFunction
{
return Grid2NormalizedDataBuilder::getDateFunction($column);
}
/**
* @return DataRow[]
*/
private function buildSubRows(Result $reportResult, mixed $group): array
{
$rows = [];
foreach ($reportResult->getSubListData()->$group ?? [] as $item) {
if (!$item instanceof stdClass) {
throw new RuntimeException("Bad sub-list item.");
}
$rows[] = $this->buildSubRowItem($reportResult, $group, $item);
}
return $rows;
}
private function buildSubRowItem(Result $reportResult, mixed $group, stdClass $item): DataRow
{
$cells = [];
$cells[] = new DataCell(
value: null,
type: CellType::Empty,
);
foreach ($reportResult->getColumnList() as $column) {
if (!in_array($column, $reportResult->getSubListColumnList())) {
$cells[] = new DataCell(
value: null,
type: CellType::Empty,
);
continue;
}
$isNumericValue = in_array($column, $reportResult->getNumericColumnList());
if ($isNumericValue) {
$value = $item->$column ?? 0;
$cells[] = new DataCell(
value: $value,
type: CellType::NonSummary,
fieldType: $reportResult->getColumnTypeMap()[$column] ?? null,
decimalPlaces: $reportResult->getColumnDecimalPlacesMap()->$column ?? null,
isNumeric: true,
);
} else {
$value = $item->$column ?? null;
$cells[] = new DataCell(
value: $value,
type: CellType::NonSummary,
fieldType: $reportResult->getColumnTypeMap()[$column] ?? null,
isNumeric: false,
);
}
}
return new DataRow(
cells: $cells,
type: RowType::SubDataRow,
);
}
}

View File

@@ -0,0 +1,267 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Language;
use Espo\Modules\Advanced\Entities\Report;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\CellFunction;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\CellType;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\DataCell;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\DataRow;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\DateFunction;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\SheetData;
use Espo\Modules\Advanced\Tools\Report\Export\Xlsx\RowType;
use Espo\Modules\Advanced\Tools\Report\GridType\Result;
use Espo\Modules\Advanced\Tools\Report\GridType\Result as GridResult;
class Grid2NormalizedDataBuilder
{
private const PRECISION = 2;
public function __construct(
private Language $language,
private CellValueHelper $cellValueHelper,
) {}
public function build(Result $reportResult): SheetData
{
$groupName1 = $reportResult->getGroupByList()[0];
$groupName2 = $reportResult->getGroupByList()[1];
$rows = [];
$cells = [
new DataCell(
value: $reportResult->getGroupNameMap()[$groupName1] ?? null,
type: CellType::HeadGroup,
),
new DataCell(
value: $reportResult->getGroupNameMap()[$groupName2] ?? null,
type: CellType::HeadGroup,
),
];
foreach ($reportResult->getNonSummaryColumnList() as $column) {
$cells[] = new DataCell(
value: $reportResult->getColumnNameMap()[$column] ?? null,
type: CellType::HeadNonSummary,
);
}
foreach ($reportResult->getSummaryColumnList() as $column) {
$cells[] = new DataCell(
value: $reportResult->getColumnNameMap()[$column] ?? null,
type: CellType::HeadSummary,
);
}
$rows[] = new DataRow(
cells: $cells,
type: RowType::Header,
);
foreach ($reportResult->getGrouping()[0] ?? [] as $group) {
foreach ($reportResult->getGrouping()[1] ?? [] as $secondGroup) {
$cells = [];
$cells[] = new DataCell(
value: $this->prepareGroupValue($reportResult, $groupName1, $group),
type: CellType::NonSummary,
dateFunction: self::getDateFunction($groupName1),
);
$cells[] = new DataCell(
value: $this->prepareGroupValue($reportResult, $groupName2, $secondGroup),
type: CellType::NonSummary,
dateFunction: self::getDateFunction($groupName2),
);
foreach ($reportResult->getNonSummaryColumnList() as $column) {
$columnGroup = $reportResult->getNonSummaryColumnGroupMap()->$column ?? null;
$columnGroupValue = $columnGroup === $reportResult->getGroupByList()[0] ?
$group : $secondGroup;
$cells[] = new DataCell(
value: $this->getCellDisplayValueFromResult(
groupIndex: $columnGroup === $reportResult->getGroupByList()[0] ? 0 : 1,
groupValue: $columnGroupValue,
column: $column,
reportResult: $reportResult,
),
type: CellType::NonSummary,
fieldType: $reportResult->getColumnTypeMap()[$column] ?? null,
decimalPlaces: $reportResult->getColumnDecimalPlacesMap()->$column ?? null,
);
}
$hasNonEmpty = false;
foreach ($reportResult->getSummaryColumnList() as $column) {
$value = $reportResult->getReportData()->$group->$secondGroup->$column ?? null;
if ($value !== null) {
$hasNonEmpty = true;
}
$decimalPlaces = $reportResult->getColumnDecimalPlacesMap()->$column ?? null;
$fieldType = $reportResult->getColumnTypeMap()[$column] ?? null;
$cells[] = new DataCell(
value: $value,
type: CellType::Summary,
fieldType: $fieldType,
decimalPlaces: $decimalPlaces,
);
}
if (!$hasNonEmpty) {
continue;
}
$rows[] = new DataRow(
cells: $cells,
type: RowType::DataRow,
);
}
}
$cells = [];
$cells[] = new DataCell(
value: $this->language->translateLabel('Total', 'labels', Report::ENTITY_TYPE),
type: CellType::Label,
);
$cells[] = new DataCell(
value: null,
type: CellType::Empty,
);
foreach ($reportResult->getNonSummaryColumnList() as $ignored) {
$cells[] = new DataCell(
value: null,
type: CellType::Empty,
);
}
foreach ($reportResult->getSummaryColumnList() as $column) {
$value = $reportResult->getSums()->$column ?? 0;
$decimalPlaces = $reportResult->getColumnDecimalPlacesMap()->$column ?? null;
$fieldType = $reportResult->getColumnTypeMap()[$column] ?? null;
$function = self::getCellFunction($column);
if ($fieldType === FieldType::INT && $function === CellFunction::Avg) {
$decimalPlaces = self::PRECISION;
}
$cells[] = new DataCell(
value: $value,
type: CellType::Total,
fieldType: $fieldType,
function: $function,
decimalPlaces: $decimalPlaces,
);
}
$rows[] = new DataRow(
cells: $cells,
type: RowType::Total,
);
return new SheetData(
rows: $rows,
firstSummaryRowNumber: 1,
lastSummaryRowNumber: count($rows) - 2,
);
}
private function getCellDisplayValueFromResult(
int $groupIndex,
string $groupValue,
string $column,
GridResult $reportResult,
): mixed {
return $this->cellValueHelper->getCellDisplayValueFromResult(
groupIndex: $groupIndex,
groupValue: $groupValue,
column: $column,
reportResult: $reportResult,
);
}
private function prepareGroupValue(Result $reportResult, string $groupName, mixed $group): string
{
if (!$group) {
return $this->language->translateLabel('-Empty-', 'labels', 'Report');
}
if (isset($reportResult->getGroupValueMap()[$groupName][$group])) {
return $reportResult->getGroupValueMap()[$groupName][$group];
}
return (string) $group;
}
public static function getCellFunction(string $column): ?CellFunction
{
[$function] = explode(':', $column);
if ($function === 'COUNT') {
return CellFunction::Sum;
}
if ($function === 'SUM') {
return CellFunction::Sum;
}
if ($function === 'AVG') {
return CellFunction::Avg;
}
if ($function === 'MIN') {
return CellFunction::Min;
}
if ($function === 'MAX') {
return CellFunction::Max;
}
return null;
}
public static function getDateFunction(string $column): ?DateFunction
{
if (!str_contains($column, ':')) {
return null;
}
[$f,] = explode(':', $column);
return match ($f) {
'MONTH' => DateFunction::Month,
'YEAR' => DateFunction::Year,
'DAY' => DateFunction::Day,
default => null,
};
}
}

View File

@@ -0,0 +1,696 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export;
use Espo\Core\AclManager;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Field\LinkParent;
use Espo\Core\InjectableFactory;
use Espo\Core\Select\Where\Item as WhereItem;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Language;
use Espo\Core\Utils\Metadata;
use Espo\Entities\Attachment;
use Espo\Entities\Template;
use Espo\Entities\User;
use Espo\Modules\Advanced\Entities\Report;
use Espo\Modules\Advanced\Tools\Report\GridType\Data as GridTypeData;
use Espo\Modules\Advanced\Tools\Report\GridType\Helper as GridHelper;
use Espo\Modules\Advanced\Tools\Report\GridType\Result as GridResult;
use Espo\Modules\Advanced\Tools\Report\GridType\Util as GridUtil;
use Espo\Modules\Advanced\Tools\Report\Service;
use Espo\Modules\Crm\Entities\Opportunity;
use Espo\ORM\EntityManager;
use Espo\Tools\Pdf\Data;
use Espo\Tools\Pdf\Service as PdfService;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use RuntimeException;
class GridExportService
{
private const STUB_KEY = '__STUB__';
public function __construct(
private EntityManager $entityManager,
private AclManager $aclManager,
private Metadata $metadata,
private Config $config,
private Language $language,
private Service $service,
private GridHelper $gridHelper,
private GridUtil $gridUtil,
private InjectableFactory $injectableFactory,
private PdfService $pdfService,
private Grid2NormalizedDataBuilder $grid2NormalizedDataBuilder,
private Grid1DataBuilder $grid1DataBuilder,
private CellValueHelper $cellValueHelper,
) {}
/**
* @throws Forbidden
* @throws NotFound
* @throws Error
* @throws BadRequest
*/
public function exportXlsx(string $id, ?WhereItem $where, ?User $user = null): string
{
$report = $this->entityManager->getRDBRepositoryByClass(Report::class)->getById($id);
if (!$report) {
throw new NotFound();
}
$this->checkAccess($report, $user);
$contents = $this->buildXlsxContents($id, $where, $user);
$mimeType = $this->metadata->get(['app', 'export', 'formatDefs', 'xlsx', 'mimeType']);
$fileName = $this->prepareXlsxFileName($report);
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getNew();
$attachment
->setName($fileName)
->setRole(Attachment::ROLE_EXPORT_FILE)
->setType($mimeType)
->setContents($contents)
->setRelated(LinkParent::createFromEntity($report));
$this->entityManager->saveEntity($attachment);
return $attachment->getId();
}
/**
* @throws Error
* @throws Forbidden
* @throws NotFound
* @throws BadRequest
*/
public function buildXlsxContents(string $id, ?WhereItem $where, ?User $user = null): string
{
$report = $this->entityManager->getRDBRepositoryByClass(Report::class)->getById($id);
if (!$report) {
throw new NotFound();
}
$entityType = $report->getTargetEntityType();
$groupCount = count($report->getGroupBy());
$columnList = $report->getColumns();
$groupByList = $report->getGroupBy();
$reportResult = null;
if (
$report->getType() === Report::TYPE_JOINT_GRID ||
!$report->getGroupBy()
) {
$reportResult = $this->service->runGrid($id, $where, $user);
$columnList = $reportResult->getColumnList();
$groupByList = $reportResult->getGroupByList();
$groupCount = count($groupByList);
}
$reportResult ??= $this->service->runGrid($id, $where, $user);
$result = [];
$sheetData = null;
if ($groupCount === 2 && $reportResult->getTableMode() !== GridTypeData::TABLE_MODE_NORMALIZED) {
foreach ($reportResult->getSummaryColumnList() as $column) {
$result[] = $this->getGrid2ResultForExport($reportResult, $column);
}
} else if ($groupCount === 2 && $reportResult->getTableMode() === GridTypeData::TABLE_MODE_NORMALIZED) {
$sheetData = $this->grid2NormalizedDataBuilder->build($reportResult);
} else if ($groupCount === 1 && $reportResult->getSubListColumnList()) {
$sheetData = $this->grid1DataBuilder->build($reportResult);
} else {
$result[] = $this->getGrid1ResultForExport($reportResult);
}
$columnTypes = [];
foreach ($columnList as $item) {
$columnData = $this->gridHelper->getDataFromColumnName($entityType, $item, $reportResult);
$type = $this->metadata
->get(['entityDefs', $columnData->entityType, 'fields', $columnData->field, 'type']);
if (
$entityType === Opportunity::ENTITY_TYPE &&
$columnData->field === 'amountWeightedConverted'
) {
$type = 'currencyConverted';
}
if ($columnData->function === 'COUNT') {
$type = 'int';
}
$columnTypes[$item] = $type;
}
$columnLabels = [];
if ($groupCount === 2) {
$columnNameMap = $reportResult->getColumnNameMap();
foreach ($columnList as $column) {
$columnLabels[$column] = $columnNameMap[$column] ?? null;
}
}
$exportParams = [
'exportName' => $report->getName(),
'columnList' => $columnList,
'columnTypes' => $columnTypes,
'chartType' => $reportResult->getChartType() ?? $report->getChartType(),
'groupByList' => $groupByList,
'columnLabels' => $columnLabels,
'reportResult' => $reportResult,
'groupLabel' => '',
'currency' => $reportResult->getCurrency(),
];
if ($groupCount) {
$group = $groupByList[$groupCount - 1];
$exportParams['groupLabel'] = $reportResult->getGroupNameMap()[$group] ??
$this->gridUtil->translateGroupName($entityType, $group);
}
$export = $this->injectableFactory->create(ExportXlsx::class);
if ($sheetData) {
try {
return $export->processWithSheedData($reportResult, $sheetData, $report);
} catch (PhpSpreadsheetException $e) {
throw new RuntimeException($e->getMessage(), previous: $e);
}
}
try {
return $export->process($entityType, $exportParams, $result);
} catch (PhpSpreadsheetException $e) {
throw new RuntimeException($e->getMessage(), previous: $e);
}
}
/**
* @throws Forbidden
* @throws NotFound
* @throws Error
* @throws BadRequest
*/
public function exportCsv(
string $id,
?WhereItem $where,
?string $column = null,
?User $user = null,
): string {
$report = $this->entityManager->getRDBRepositoryByClass(Report::class)->getById($id);
if (!$report) {
throw new NotFound();
}
$this->checkAccess($report, $user);
$contents = $this->getGridReportCsv($id, $where, $column, $user);
$mimeType = $this->metadata->get(['app', 'export', 'formatDefs', 'csv', 'mimeType']);
$fileName = $this->prepareCsvFileName($report);
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getNew();
$attachment
->setName($fileName)
->setRole(Attachment::ROLE_EXPORT_FILE)
->setType($mimeType)
->setContents($contents)
->setRelated(LinkParent::createFromEntity($report));
$this->entityManager->saveEntity($attachment);
return $attachment->getId();
}
/**
* @throws Forbidden
* @throws Error
* @throws NotFound
* @throws BadRequest
*/
private function getGridReportCsv(
string $id,
?WhereItem $where,
?string $column = null,
?User $user = null,
): string {
$result = $this->getGridReportResultForExport(
id: $id,
where: $where,
currentColumn: $column,
user: $user,
);
$delimiter = $this->config->get('exportDelimiter', ';');
$fp = fopen('php://temp', 'w');
if ($fp === false) {
throw new RuntimeException("Could not open temp.");
}
foreach ($result as $row) {
fputcsv($fp, $row, $delimiter);
}
rewind($fp);
$csv = stream_get_contents($fp);
fclose($fp);
if ($csv === false) {
throw new RuntimeException("Could not get from stream.");
}
return $csv;
}
/**
* @return array<int, mixed>[]
* @throws Error
* @throws Forbidden
* @throws NotFound
* @throws BadRequest
*/
private function getGridReportResultForExport(
string $id,
?WhereItem $where,
?string $currentColumn = null,
?User $user = null,
): array {
$reportResult = $this->service->runGrid($id, $where, $user);
$depth = count($reportResult->getGroupByList());
if ($depth === 2 && $reportResult->getTableMode() === GridTypeData::TABLE_MODE_NORMALIZED) {
$sheetData = $this->grid2NormalizedDataBuilder->build($reportResult);
return $this->sheetDataToRaw($sheetData);
}
if ($depth === 1 && $reportResult->getSubListColumnList()) {
$sheetData = $this->grid1DataBuilder->build($reportResult);
return $this->sheetDataToRaw($sheetData);
}
if ($depth === 2) {
return $this->getGrid2ResultForExport($reportResult, $currentColumn);
}
if ($depth === 1 || $depth === 0) {
return $this->getGrid1ResultForExport($reportResult);
}
throw new RuntimeException();
}
public function getCellDisplayValueFromResult(
int $groupIndex,
string $groupValue,
string $column,
GridResult $reportResult,
): mixed {
return $this->cellValueHelper->getCellDisplayValueFromResult(
groupIndex: $groupIndex,
groupValue: $groupValue,
column: $column,
reportResult: $reportResult,
);
}
/**
* @throws Forbidden
* @throws Error
* @throws NotFound
*/
public function exportPdf(
string $id,
?WhereItem $where,
string $templateId,
?User $user = null,
): string {
$report = $this->entityManager->getRDBRepositoryByClass(Report::class)->getById($id);
$template = $this->entityManager->getRDBRepositoryByClass(Template::class)->getById($templateId);
if (!$report || !$template) {
throw new NotFound();
}
if ($user) {
if (!$this->aclManager->checkEntityRead($user, $report)) {
throw new Forbidden("No access to report.");
}
if (!$this->aclManager->checkEntityRead($user, $template)) {
throw new Forbidden("No access to template.");
}
}
$additionalData = [
'user' => $user,
'reportWhere' => $where,
];
$contents = $this->pdfService
->generate(
Report::ENTITY_TYPE,
$report->getId(),
$template->getId(),
null,
Data::create()->withAdditionalTemplateData((object) $additionalData)
)
->getString();
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getNew();
$attachment
->setRole(Attachment::ROLE_EXPORT_FILE)
->setType('application/pdf')
->setContents($contents)
->setRelated(LinkParent::createFromEntity($report));
$this->entityManager->saveEntity($attachment);
return $attachment->getId();
}
/**
* @return array<int, mixed>[]
*/
private function getGrid2ResultForExport(GridResult $reportResult, ?string $currentColumn): array
{
$result = [];
$reportData = $reportResult->getReportData();
$groupName1 = $reportResult->getGroupByList()[0];
$groupName2 = $reportResult->getGroupByList()[1];
$group1NonSummaryColumnList = [];
$group2NonSummaryColumnList = [];
if ($reportResult->getGroup1NonSummaryColumnList() !== null) {
$group1NonSummaryColumnList = $reportResult->getGroup1NonSummaryColumnList();
}
if ($reportResult->getGroup2NonSummaryColumnList() !== null) {
$group2NonSummaryColumnList = $reportResult->getGroup2NonSummaryColumnList();
}
$row = [];
$row[] = '';
foreach ($group2NonSummaryColumnList as $column) {
$text = $reportResult->getColumnNameMap()[$column];
$row[] = $text;
}
foreach ($reportResult->getGrouping()[0] ?? [] as $gr1) {
$label = $gr1;
if (empty($label)) {
$label = $this->language->translate('-Empty-', 'labels', 'Report');
} else if (!empty($reportResult->getGroupValueMap()[$groupName1][$gr1])) {
$label = $reportResult->getGroupValueMap()[$groupName1][$gr1];
}
$row[] = $label;
}
$result[] = $row;
foreach ($reportResult->getGrouping()[1] ?? [] as $gr2) {
$row = [];
$label = $gr2;
if (empty($label)) {
$label = $this->language->translate('-Empty-', 'labels', 'Report');
} else if (!empty($reportResult->getGroupValueMap()[$groupName2][$gr2])) {
$label = $reportResult->getGroupValueMap()[$groupName2][$gr2];
}
$row[] = $label;
foreach ($group2NonSummaryColumnList as $column) {
$row[] = $this->getCellDisplayValueFromResult(1, $gr2, $column, $reportResult);
}
foreach ($reportResult->getGrouping()[0] ?? [] as $gr1) {
$value = 0;
if (!empty($reportData->$gr1) && !empty($reportData->$gr1->$gr2)) {
if (!empty($reportData->$gr1->$gr2->$currentColumn)) {
$value = $reportData->$gr1->$gr2->$currentColumn;
}
}
$row[] = $value;
}
$result[] = $row;
}
$row = [];
$row[] = $this->language->translate('Total', 'labels', 'Report');
foreach ($group2NonSummaryColumnList as $ignored) {
$row[] = '';
}
foreach ($reportResult->getGrouping()[0] ?? [] as $gr1) {
$sum = 0;
if (!empty($reportResult->getGroup1Sums()->$gr1)) {
if (!empty($reportResult->getGroup1Sums()->$gr1->$currentColumn)) {
$sum = $reportResult->getGroup1Sums()->$gr1->$currentColumn;
}
}
$row[] = $sum;
}
$result[] = $row;
if (count($group1NonSummaryColumnList)) {
$result[] = [];
}
foreach ($group1NonSummaryColumnList as $column) {
$row = [];
$text = $reportResult->getColumnNameMap()[$column];
$row[] = $text;
foreach ($group2NonSummaryColumnList as $ignored) {
$row[] = '';
}
foreach ($reportResult->getGrouping()[0] ?? [] as $gr1) {
$row[] = $this->getCellDisplayValueFromResult(0, $gr1, $column, $reportResult);
}
$result[] = $row;
}
return $result;
}
/**
* @return array<int, mixed>[]
*/
private function getGrid1ResultForExport(GridResult $reportResult): array
{
$result = [];
$depth = count($reportResult->getGroupByList());
$reportData = $reportResult->getReportData();
$aggregatedColumnList = $reportResult->getAggregatedColumnList();
if ($depth === 1) {
$groupName = $reportResult->getGroupByList()[0];
} else {
$groupName = self::STUB_KEY;
}
$row = [];
$row[] = '';
foreach ($aggregatedColumnList as $column) {
$label = $column;
if (!empty($reportResult->getColumnNameMap()[$column])) {
$label = $reportResult->getColumnNameMap()[$column];
}
$row[] = $label;
}
$result[] = $row;
foreach ($reportResult->getGrouping()[0] ?? [] as $gr) {
$row = [];
$label = $gr;
if (empty($label)) {
$label = $this->language->translate('-Empty-', 'labels', 'Report');
} else if (
!empty($reportResult->getGroupValueMap()[$groupName]) &&
array_key_exists($gr, $reportResult->getGroupValueMap()[$groupName])
) {
$label = $reportResult->getGroupValueMap()[$groupName][$gr];
}
$row[] = $label;
foreach ($aggregatedColumnList as $column) {
if (in_array($column, $reportResult->getNumericColumnList())) {
$value = 0;
if (!empty($reportData->$gr)) {
if (!empty($reportData->$gr->$column)) {
$value = $reportData->$gr->$column;
}
}
} else {
$value = '';
if (property_exists($reportData, $gr) && property_exists($reportData->$gr, $column)) {
$value = $reportData->$gr->$column;
if (
!is_null($value) &&
property_exists($reportResult->getCellValueMaps(), $column) &&
property_exists($reportResult->getCellValueMaps()->$column, $value)
) {
$value = $reportResult->getCellValueMaps()->$column->$value;
}
}
}
$row[] = $value;
}
$result[] = $row;
}
if ($depth) {
$row = [];
$row[] = $this->language->translate('Total', 'labels', 'Report');
foreach ($aggregatedColumnList as $column) {
if (!in_array($column, $reportResult->getNumericColumnList())) {
$row[] = '';
continue;
}
$sum = 0;
if (!empty($reportResult->getSums()->$column)) {
$sum = $reportResult->getSums()->$column;
}
$row[] = $sum;
}
$result[] = $row;
}
return $result;
}
private function prepareXlsxFileName(Report $report): string
{
$name = preg_replace("/([^\w\s\d\-_~,;:\[\]().])/u", '_', $report->getName()) . ' ' . date('Y-m-d');
$fileExtension = $this->metadata->get(['app', 'export', 'formatDefs', 'xlsx', 'fileExtension']);
return $name . '.' . $fileExtension;
}
private function prepareCsvFileName(Report $report): string
{
$name = preg_replace("/([^\w\s\d\-_~,;:\[\]().])/u", '_', $report->getName()) . ' ' . date('Y-m-d');
$fileExtension = $this->metadata->get(['app', 'export', 'formatDefs', 'csv', 'fileExtension']);
return $name . '.' . $fileExtension;
}
/**
* @throws Forbidden
*/
private function checkAccess(Report $report, ?User $user): void
{
if ($user && !$this->aclManager->checkEntityRead($user, $report)) {
throw new Forbidden();
}
}
/**
* @return array<int, array<int, mixed>>
*/
private function sheetDataToRaw(Xlsx\SheetData $sheetData): array
{
$rows = [];
foreach ($sheetData->rows as $row) {
$cells = [];
foreach ($row->cells as $cell) {
$cells[] = $cell->value;
}
$rows[] = $cells;
}
return $rows;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export\Xlsx;
enum CellFunction
{
case Sum;
case Avg;
case Min;
case Max;
}

View File

@@ -0,0 +1,32 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export\Xlsx;
enum CellType
{
case Empty;
case Label;
case HeadGroup;
case HeadNonSummary;
case HeadSummary;
case Summary;
case NonSummary;
case Total;
case RowTotal;
}

View File

@@ -0,0 +1,32 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export\Xlsx;
readonly class DataCell
{
public function __construct(
public mixed $value,
public CellType $type,
public ?string $fieldType = null,
public ?CellFunction $function = null,
public ?DateFunction $dateFunction = null,
public ?int $decimalPlaces = null,
public ?bool $isNumeric = null,
) {}
}

View File

@@ -0,0 +1,30 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export\Xlsx;
readonly class DataRow
{
/**
* @param DataCell[] $cells
*/
public function __construct(
public array $cells,
public RowType $type,
) {}
}

View File

@@ -0,0 +1,26 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export\Xlsx;
enum DateFunction
{
case Month;
case Year;
case Day;
}

View File

@@ -0,0 +1,28 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export\Xlsx;
enum RowType
{
case Header;
case DataRow;
case HeadDataRow;
case SubDataRow;
case Total;
}

View File

@@ -0,0 +1,31 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Export\Xlsx;
readonly class SheetData
{
/**
* @param DataRow[] $rows
*/
public function __construct(
public array $rows,
public ?int $firstSummaryRowNumber = null,
public ?int $lastSummaryRowNumber = null,
) {}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -1,634 +0,0 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report;
use Espo\Core\AclManager;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\InjectableFactory;
use Espo\Core\Select\Where\Item as WhereItem;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Language;
use Espo\Core\Utils\Metadata;
use Espo\Entities\Attachment;
use Espo\Entities\Template;
use Espo\Entities\User;
use Espo\Modules\Advanced\Core\Report\ExportXlsx;
use Espo\Modules\Advanced\Entities\Report;
use Espo\Modules\Advanced\Tools\Report\GridType\Helper as GridHelper;
use Espo\Modules\Advanced\Tools\Report\GridType\Result as GridResult;
use Espo\Modules\Advanced\Tools\Report\GridType\Util as GridUtil;
use Espo\Modules\Crm\Entities\Opportunity;
use Espo\ORM\EntityManager;
use Espo\Tools\Pdf\Data;
use Espo\Tools\Pdf\Service as PdfService;
use PhpOffice\PhpSpreadsheet\Writer\Exception;
use RuntimeException;
class GridExportService
{
private const STUB_KEY = '__STUB__';
public function __construct(
private EntityManager $entityManager,
private AclManager $aclManager,
private Metadata $metadata,
private Config $config,
private Language $language,
private Service $service,
private GridHelper $gridHelper,
private GridUtil $gridUtil,
private InjectableFactory $injectableFactory
) {}
/**
* @throws Forbidden
* @throws NotFound
* @throws Error
*/
public function exportXlsx(string $id, ?WhereItem $where, ?User $user = null): string
{
/** @var ?Report $report */
$report = $this->entityManager->getEntityById(Report::ENTITY_TYPE, $id);
if (!$report) {
throw new NotFound();
}
if ($user && !$this->aclManager->checkEntityRead($user, $report)) {
throw new Forbidden();
}
$contents = $this->buildXlsxContents($id, $where, $user);
$name = preg_replace("/([^\w\s\d\-_~,;:\[\]().])/u", '_', $report->getName()) . ' ' . date('Y-m-d');
$mimeType = $this->metadata->get(['app', 'export', 'formatDefs', 'xlsx', 'mimeType']);
$fileExtension = $this->metadata->get(['app', 'export', 'formatDefs', 'xlsx', 'fileExtension']);
$fileName = $name . '.' . $fileExtension;
$attachment = $this->entityManager->getNewEntity(Attachment::ENTITY_TYPE);
$attachment->set('name', $fileName);
$attachment->set('role', 'Export File');
$attachment->set('type', $mimeType);
$attachment->set('contents', $contents);
$attachment->set([
'relatedType' => Report::ENTITY_TYPE,
'relatedId' => $id,
]);
$this->entityManager->saveEntity($attachment);
return $attachment->getId();
}
/**
* @throws Error
* @throws Forbidden
* @throws NotFound
* @throws Exception
* @throws BadRequest
*/
public function buildXlsxContents(string $id, ?WhereItem $where, ?User $user = null): string
{
/** @var ?Report $report */
$report = $this->entityManager->getEntityById(Report::ENTITY_TYPE, $id);
if (!$report) {
throw new NotFound();
}
$entityType = $report->getTargetEntityType();
$groupCount = count($report->getGroupBy());
$columnList = $report->getColumns();
$groupByList = $report->getGroupBy();
$reportResult = null;
if (
$report->getType() === Report::TYPE_JOINT_GRID ||
!$report->getGroupBy()
) {
$reportResult = $this->service->runGrid($id, $where, $user);
$columnList = $reportResult->getColumnList();
$groupByList = $reportResult->getGroupByList();
$groupCount = count($groupByList);
}
if (!$reportResult) {
$reportResult = $this->service->runGrid($id, $where, $user);
}
$result = [];
if ($groupCount === 2) {
foreach ($reportResult->getSummaryColumnList() as $column) {
$result[] = $this->getGridReportResultForExport($id, $where, $column, $user, $reportResult);
}
} else {
$result[] = $this->getGridReportResultForExport($id, $where, null, $user, $reportResult);
}
$columnTypes = [];
foreach ($columnList as $item) {
$columnData = $this->gridHelper->getDataFromColumnName($entityType, $item, $reportResult);
$type = $this->metadata
->get(['entityDefs', $columnData->entityType, 'fields', $columnData->field, 'type']);
if (
$entityType === Opportunity::ENTITY_TYPE &&
$columnData->field === 'amountWeightedConverted'
) {
$type = 'currencyConverted';
}
if ($columnData->function === 'COUNT') {
$type = 'int';
}
$columnTypes[$item] = $type;
}
$columnLabels = [];
if ($groupCount === 2) {
$columnNameMap = $reportResult->getColumnNameMap() ?? [];
foreach ($columnList as $column) {
$columnLabels[$column] = $columnNameMap[$column];
}
}
$exportParams = [
'exportName' => $report->getName(),
'columnList' => $columnList,
'columnTypes' => $columnTypes,
'chartType' => $report->get('chartType'),
'groupByList' => $groupByList,
'columnLabels' => $columnLabels,
'reportResult' => $reportResult,
'groupLabel' => '',
];
if ($groupCount) {
$group = $groupByList[$groupCount - 1];
$exportParams['groupLabel'] = $this->gridUtil->translateGroupName($entityType, $group);
}
$export = $this->injectableFactory->create(ExportXlsx::class);
return $export->process($entityType, $exportParams, $result);
}
/**
* @throws Forbidden
* @throws NotFound
* @throws Error
*/
public function exportCsv(
string $id,
?WhereItem $where,
?string $column = null,
?User $user = null
): string {
/** @var ?Report $report */
$report = $this->entityManager->getEntityById(Report::ENTITY_TYPE, $id);
if (!$report) {
throw new NotFound();
}
if ($user && !$this->aclManager->checkEntityRead($user, $report)) {
throw new Forbidden();
}
$contents = $this->getGridReportCsv($id, $where, $column, $user);
$name = preg_replace("/([^\w\s\d\-_~,;:\[\]().])/u", '_', $report->getName()) . ' ' . date('Y-m-d');
$mimeType = $this->metadata->get(['app', 'export', 'formatDefs', 'csv', 'mimeType']);
$fileExtension = $this->metadata->get(['app', 'export', 'formatDefs', 'csv', 'fileExtension']);
$fileName = $name . '.' . $fileExtension;
$attachment = $this->entityManager->getEntity('Attachment');
$attachment->set('name', $fileName);
$attachment->set('role', 'Export File');
$attachment->set('type', $mimeType);
$attachment->set('contents', $contents);
$attachment->set([
'relatedType' => Report::ENTITY_TYPE,
'relatedId' => $id,
]);
$this->entityManager->saveEntity($attachment);
return $attachment->getId();
}
/**
* @throws Forbidden
* @throws Error
* @throws NotFound
* @throws BadRequest
*/
private function getGridReportCsv(
string $id,
?WhereItem $where,
?string $column = null,
?User $user = null
): string {
$result = $this->getGridReportResultForExport($id, $where, $column, $user);
$delimiter = $this->config->get('exportDelimiter', ';');
$fp = fopen('php://temp', 'w');
if ($fp === false) {
throw new RuntimeException("Could not open temp.");
}
foreach ($result as $row) {
fputcsv($fp, $row, $delimiter);
}
rewind($fp);
$csv = stream_get_contents($fp);
fclose($fp);
if ($csv === false) {
throw new RuntimeException("Could not get from stream.");
}
return $csv;
}
/**
* @return array<int, mixed>[]
* @throws Error
* @throws Forbidden
* @throws NotFound
* @throws BadRequest
*/
private function getGridReportResultForExport(
string $id,
?WhereItem $where,
?string $currentColumn = null,
?User $user = null,
?GridResult $reportResult = null
): array {
if (!$reportResult) {
$reportResult = $this->service->runGrid($id, $where, $user);
}
$depth = count($reportResult->getGroupByList());
$reportData = $reportResult->getReportData();
$result = [];
if ($depth == 2) {
$groupName1 = $reportResult->getGroupByList()[0];
$groupName2 = $reportResult->getGroupByList()[1];
$group1NonSummaryColumnList = [];
$group2NonSummaryColumnList = [];
if ($reportResult->getGroup1NonSummaryColumnList() !== null) {
$group1NonSummaryColumnList = $reportResult->getGroup1NonSummaryColumnList();
}
if ($reportResult->getGroup2NonSummaryColumnList() !== null) {
$group2NonSummaryColumnList = $reportResult->getGroup2NonSummaryColumnList();
}
$row = [];
$row[] = '';
foreach ($group2NonSummaryColumnList as $column) {
$text = $reportResult->getColumnNameMap()[$column];
$row[] = $text;
}
foreach ($reportResult->getGrouping()[0] ?? [] as $gr1) {
$label = $gr1;
if (empty($label)) {
$label = $this->language->translate('-Empty-', 'labels', 'Report');
}
else if (!empty($reportResult->getGroupValueMap()[$groupName1][$gr1])) {
$label = $reportResult->getGroupValueMap()[$groupName1][$gr1];
}
$row[] = $label;
}
$result[] = $row;
foreach ($reportResult->getGrouping()[1] ?? [] as $gr2) {
$row = [];
$label = $gr2;
if (empty($label)) {
$label = $this->language->translate('-Empty-', 'labels', 'Report');
}
else if (!empty($reportResult->getGroupValueMap()[$groupName2][$gr2])) {
$label = $reportResult->getGroupValueMap()[$groupName2][$gr2];
}
$row[] = $label;
foreach ($group2NonSummaryColumnList as $column) {
$row[] = $this->getCellDisplayValueFromResult(1, $gr2, $column, $reportResult);
}
foreach ($reportResult->getGrouping()[0] ?? [] as $gr1) {
$value = 0;
if (!empty($reportData->$gr1) && !empty($reportData->$gr1->$gr2)) {
if (!empty($reportData->$gr1->$gr2->$currentColumn)) {
$value = $reportData->$gr1->$gr2->$currentColumn;
}
}
$row[] = $value;
}
$result[] = $row;
}
$row = [];
$row[] = $this->language->translate('Total', 'labels', 'Report');
foreach ($group2NonSummaryColumnList as $ignored) {
$row[] = '';
}
foreach ($reportResult->getGrouping()[0] ?? [] as $gr1) {
$sum = 0;
if (!empty($reportResult->getGroup1Sums()->$gr1)) {
if (!empty($reportResult->getGroup1Sums()->$gr1->$currentColumn)) {
$sum = $reportResult->getGroup1Sums()->$gr1->$currentColumn;
}
}
$row[] = $sum;
}
$result[] = $row;
if (count($group1NonSummaryColumnList)) {
$result[] = [];
}
foreach ($group1NonSummaryColumnList as $column) {
$row = [];
$text = $reportResult->getColumnNameMap()[$column];
$row[] = $text;
foreach ($group2NonSummaryColumnList as $ignored) {
$row[] = '';
}
foreach ($reportResult->getGrouping()[0] ?? [] as $gr1) {
$row[] = $this->getCellDisplayValueFromResult(0, $gr1, $column, $reportResult);
}
$result[] = $row;
}
} else if ($depth === 1 || $depth === 0) {
$aggregatedColumnList = $reportResult->getAggregatedColumnList();
if ($depth === 1) {
$groupName = $reportResult->getGroupByList()[0];
} else {
$groupName = self::STUB_KEY;
}
$row = [];
$row[] = '';
foreach ($aggregatedColumnList as $column) {
$label = $column;
if (!empty($reportResult->getColumnNameMap()[$column])) {
$label = $reportResult->getColumnNameMap()[$column];
}
$row[] = $label;
}
$result[] = $row;
foreach ($reportResult->getGrouping()[0] ?? [] as $gr) {
$row = [];
$label = $gr;
if (empty($label)) {
$label = $this->language->translate('-Empty-', 'labels', 'Report');
}
else if (
!empty($reportResult->getGroupValueMap()[$groupName]) &&
array_key_exists($gr, $reportResult->getGroupValueMap()[$groupName])
) {
$label = $reportResult->getGroupValueMap()[$groupName][$gr];
}
$row[] = $label;
foreach ($aggregatedColumnList as $column) {
if (in_array($column, $reportResult->getNumericColumnList())) {
$value = 0;
if (!empty($reportData->$gr)) {
if (!empty($reportData->$gr->$column)) {
$value = $reportData->$gr->$column;
}
}
}
else {
$value = '';
if (property_exists($reportData, $gr) && property_exists($reportData->$gr, $column)) {
$value = $reportData->$gr->$column;
if (
!is_null($value) &&
property_exists($reportResult->getCellValueMaps(), $column) &&
property_exists($reportResult->getCellValueMaps()->$column, $value)
) {
$value = $reportResult->getCellValueMaps()->$column->$value;
}
}
}
$row[] = $value;
}
$result[] = $row;
}
if ($depth) {
$row = [];
$row[] = $this->language->translate('Total', 'labels', 'Report');
foreach ($aggregatedColumnList as $column) {
if (!in_array($column, $reportResult->getNumericColumnList())) {
$row[] = '';
continue;
}
$sum = 0;
if (!empty($reportResult->getSums()->$column)) {
$sum = $reportResult->getSums()->$column;
}
$row[] = $sum;
}
$result[] = $row;
}
}
return $result;
}
/**
* @return mixed
*/
public function getCellDisplayValueFromResult(
int $groupIndex,
string $gr1,
string $column,
GridResult $reportResult
) {
$groupName = $reportResult->getGroupByList()[$groupIndex];
$dataMap = $reportResult->getNonSummaryData()->$groupName;
$value = '';
if ($this->gridHelper->isColumnNumeric($column, $reportResult)) {
$value = 0;
}
if (
property_exists($dataMap, $gr1) &&
property_exists($dataMap->$gr1, $column)
) {
$value = $dataMap->$gr1->$column;
}
if (
!$this->gridHelper->isColumnNumeric($column, $reportResult) &&
!is_null($value)
) {
if (property_exists($reportResult->getCellValueMaps(), $column)) {
if (property_exists($reportResult->getCellValueMaps()->$column, $value)) {
$value = $reportResult->getCellValueMaps()->$column->$value;
}
}
}
if (is_null($value)) {
$value = '';
}
return $value;
}
/**
* @throws Forbidden
* @throws Error
* @throws NotFound
*/
public function exportPdf(
string $id,
?WhereItem $where,
string $templateId,
?User $user = null
): string {
$report = $this->entityManager->getEntityById(Report::ENTITY_TYPE, $id);
$template = $this->entityManager->getEntityById(Template::ENTITY_TYPE, $templateId);
if (!$report || !$template) {
throw new NotFound();
}
if ($user) {
if (!$this->aclManager->checkEntityRead($user, $report)) {
throw new Forbidden("No access to report.");
}
if (!$this->aclManager->checkEntityRead($user, $template)) {
throw new Forbidden("No access to template.");
}
}
$additionalData = [
'user' => $user,
'reportWhere' => $where,
];
$pdfService = $this->injectableFactory->create(PdfService::class);
$contents = $pdfService
->generate(
Report::ENTITY_TYPE,
$report->getId(),
$template->getId(),
null,
Data::create()->withAdditionalTemplateData((object) $additionalData)
)
->getString();
$attachment = $this->entityManager->createEntity(Attachment::ENTITY_TYPE, [
'contents' => $contents,
'role' => 'Export File',
'type' => 'application/pdf',
'relatedId' => $id,
'relatedType' => Report::ENTITY_TYPE,
]);
return $attachment->getId();
}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -24,23 +24,9 @@ use stdClass;
class Data
{
public const COLUMN_TYPE_SUMMARY = 'Summary';
public const TABLE_MODE_REGULAR = 'Regular';
public const TABLE_MODE_NORMALIZED = 'Normalized';
private string $entityType;
private ?string $success;
/** @var string[] */
private array $columns;
/** @var string[] */
private array $groupBy;
/** @var string[] */
private array $orderBy;
private bool $applyAcl;
private ?WhereItem $filtersWhere;
private ?string $chartType;
/** @var ?array<string, string> */
private ?array $chartColors;
private ?string $chartColor;
/** @var ?stdClass[] */
private ?array $chartDataList;
/** @var string[] */
private array $aggregatedColumns = [];
private stdClass $columnsData;
@@ -53,31 +39,21 @@ class Data
* @param ?stdClass[] $chartDataList
*/
public function __construct(
string $entityType,
array $columns,
array $groupBy,
array $orderBy,
bool $applyAcl,
?WhereItem $filtersWhere,
?string $chartType,
?array $chartColors,
?string $chartColor,
?array $chartDataList,
?string $success,
?stdClass $columnsData
private string $entityType,
private array $columns,
private array $groupBy,
private array $orderBy,
private bool $applyAcl = false,
private ?WhereItem $filtersWhere = null,
private ?string $chartType = null,
private ?array $chartColors = null,
private ?string $chartColor = null,
private ?array $chartDataList = null,
private ?string $success = null,
?stdClass $columnsData = null,
private ?string $tableMode = null,
) {
$this->entityType = $entityType;
$this->columns = $columns;
$this->groupBy = $groupBy;
$this->orderBy = $orderBy;
$this->applyAcl = $applyAcl;
$this->filtersWhere = $filtersWhere;
$this->chartType = $chartType;
$this->chartColors = $chartColors;
$this->chartColor = $chartColor;
$this->chartDataList = $chartDataList;
$this->success = $success;
$this->columnsData = $columnsData;
$this->columnsData = $columnsData ?? (object) [];
}
public function getEntityType(): string
@@ -213,4 +189,9 @@ class Data
return $obj;
}
public function getTableMode(): ?string
{
return $this->tableMode;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType\Data;
readonly class Column
{
public function __construct(
public string $name,
public string $label,
public string $fieldType,
public ColumnType $type = ColumnType::Summary,
public bool $isNumeric = true,
public bool $isAggregated = true,
) {}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType\Data;
use Espo\Modules\Advanced\Tools\Report\GridType\Data;
enum ColumnType: string
{
case Summary = Data::COLUMN_TYPE_SUMMARY;
case NonSummary = 'Non-Summary';
}

View File

@@ -0,0 +1,28 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType\Data;
readonly class Group
{
public function __construct(
public string $name,
public ?string $label,
public ?string $valueLabelKey = null,
) {}
}

View File

@@ -0,0 +1,27 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType\Data;
readonly class Order
{
public function __construct(
public string $column,
public OrderDirection $direction,
) {}
}

View File

@@ -0,0 +1,25 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType\Data;
enum OrderDirection: string
{
case asc = 'ASC';
case desc = 'DESC';
}

View File

@@ -0,0 +1,29 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType\Data;
class Row
{
/**
* @param array<string, mixed> $row
*/
public function __construct(
public array $row,
) {}
}

View File

@@ -0,0 +1,47 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType\Data;
readonly class Rows
{
/**
* @param Row[] $rows
*/
public function __construct(
public array $rows,
) {}
/**
* @param array<string, mixed>[] $rows
*/
public static function fromAssocList(array $rows): self
{
return new self(
rows: array_map(fn ($it) => new Row(row: $it), $rows),
);
}
/**
* @return array<string, mixed>[]
*/
public function toAssocList(): array
{
return array_map(fn (Row $it) => $it->row, $this->rows);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType\Data;
readonly class SwitchItem
{
public function __construct(
public string $name,
public string $label,
public ?string $entityType = null,
) {}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -44,20 +44,22 @@ class GridBuilder
array $groupList,
array $columns,
array &$sums,
stdClass $cellValueMaps,
?stdClass $cellValueMaps = null,
array $groups = [],
int $number = 0
): stdClass {
$cellValueMaps ??= (object) [];
$gridData = $this->buildInternal(
$data,
$rows,
$groupList,
$columns,
$sums,
$cellValueMaps,
$groups,
$number
data: $data,
rows: $rows,
groupList: $groupList,
columns: $columns,
sums: $sums,
cellValueMaps: $cellValueMaps,
groups: $groups,
number: $number,
);
foreach ($gridData as $k => $v) {

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,13 +11,14 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType;
use Espo\Modules\Advanced\Entities\Report;
use stdClass;
class Result
@@ -80,6 +81,8 @@ class Result
* @param ?string $chartType
* @param ?stdClass[] $chartDataList
* @param ?stdClass $columnDecimalPlacesMap
* @param array<string, string> $groupNameMap
* @param ?array<int, stdClass & object{name: string, label: string, entityType: string}> $subReportSwitchDataList
*/
public function __construct(
private ?string $entityType,
@@ -103,7 +106,12 @@ class Result
private ?string $chartType = null,
private ?array $chartDataList = null,
?stdClass $columnDecimalPlacesMap = null,
private bool $emptyStringGroupExcluded = false
private bool $emptyStringGroupExcluded = false,
private bool $noSubReport = false,
private ?string $currency = null,
private array $groupNameMap = [],
private ?array $subReportSwitchDataList = null,
private ?string $tableMode = null,
) {
$this->subListColumnList = $subListColumnList ?? [];
$this->nonSummaryColumnGroupMap = $nonSummaryColumnGroupMap ?? (object) [];
@@ -202,11 +210,11 @@ class Result
}
/**
* @return array<string, string>|null
* @return array<string, string>
*/
public function getColumnNameMap(): ?array
public function getColumnNameMap(): array
{
return $this->columnNameMap;
return $this->columnNameMap ?? [];
}
/**
@@ -643,10 +651,23 @@ class Result
return $this->emptyStringGroupExcluded;
}
public function getCurrency(): ?string
{
return $this->currency;
}
/**
* @return array<string, string>
*/
public function getGroupNameMap(): array
{
return $this->groupNameMap;
}
public function toRaw(): stdClass
{
return (object) [
'type' => 'Grid',
'type' => Report::TYPE_GRID,
'entityType' => $this->entityType, // string
'depth' => count($this->groupByList), // int
'columnList' => $this->columnList, // string[]
@@ -659,7 +680,7 @@ class Result
'nonSummaryColumnGroupMap' => $this->nonSummaryColumnGroupMap, // stdClass
'subListData' => $this->subListData, // object<stdClass[]>
'sums' => $this->sums, // object<int|float>
'groupValueMap' => $this->groupValueMap, // array<string, array<string, mixed>>
'groupValueMap' => (object) $this->groupValueMap, // array<string, array<string, mixed>>
'columnNameMap' => $this->columnNameMap, // array<string, string>
'columnTypeMap' => $this->columnTypeMap, // array<string, string>
'cellValueMaps' => $this->cellValueMaps, // object<object> (when grouping by link)
@@ -684,6 +705,16 @@ class Result
'columnReportIdMap' => (object) $this->columnReportIdMap,
'columnSubReportLabelMap' => (object) $this->columnSubReportLabelMap,
'emptyStringGroupExcluded' => $this->emptyStringGroupExcluded,
'noSubReport' => $this->noSubReport,
'currency' => $this->currency,
'subReportSwitchDataList' => $this->subReportSwitchDataList,
'tableMode' => $this->tableMode,
'groupNameMap' => (object) $this->groupNameMap,
];
}
public function getTableMode(): ?string
{
return $this->tableMode;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType;
use Espo\Modules\Advanced\Tools\Report\GridType\Data\Column;
use Espo\Modules\Advanced\Tools\Report\GridType\Data\Group;
use Espo\Modules\Advanced\Tools\Report\GridType\Data\Order;
use Espo\Modules\Advanced\Tools\Report\GridType\Data\SwitchItem;
readonly class ResultData
{
/**
* @param Column[] $columns
* @param ?SwitchItem[] $switchItems
* @param Order[] $orders,
*/
public function __construct(
public string $entityType,
public Group $group,
public array $columns,
public ?Group $secondGroup = null,
public array $orders = [],
public ?string $currency = null,
public ?string $chartType = null,
public ?array $switchItems = null,
public bool $noSubReport = false,
public ?string $tableMode = null,
) {}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -430,6 +430,7 @@ class ResultHelper
array $rows
): void {
$columnData = $this->helper->getDataFromColumnName($entityType, $originalGroupItem);
$fieldType = $columnData->fieldType;
@@ -986,6 +987,10 @@ class ResultHelper
}
if (count($data->getGroupBy()) === 2) {
if ($data->getTableMode() === Data::TABLE_MODE_NORMALIZED) {
return;
}
$this->populateRows2($data, $groupList, $grouping, $rows, $nonSummaryColumnList);
}
}
@@ -1473,4 +1478,14 @@ class ResultHelper
sort($list);
}
/**
* @param array<string, string> $groupNameMap
*/
public function populateGroupNameMap(Data $data, array &$groupNameMap): void
{
foreach ($data->getGroupBy() as $groupBy) {
$groupNameMap[$groupBy] = $this->util->translateGroupName($data->getEntityType(), $groupBy);
}
}
}

View File

@@ -0,0 +1,232 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\GridType;
use Espo\Core\Select\Where\Item;
use Espo\Modules\Advanced\Tools\Report\GridType\Data\ColumnType;
use Espo\Modules\Advanced\Tools\Report\GridType\Data\Rows;
/**
* @noinspection PhpUnused
*/
class ResultPreparator
{
public function __construct(
private ResultHelper $resultHelper,
private GridBuilder $gridBuilder,
) {}
/**
* Prepares a result object for a Grid report with one group-by.
*
* @param Rows $rows A query results.
* @param ?Item $where Runtime filters.
*/
public function prepare(ResultData $resultData, Rows $rows, ?Item $where = null): Result
{
$groupBy = $resultData->group->name;
$secondGroupBy = $resultData->secondGroup->name ?? null;
$groupValues = [];
$rows = $rows->toAssocList();
foreach ($rows as $row) {
$groupValues[] = $row[$groupBy] ?? null;
}
$grouping = [$groupValues];
$groupList = [$groupBy];
if ($secondGroupBy) {
$groupList[] = $secondGroupBy;
$secondGroupValues = [];
foreach ($rows as $row) {
$secondGroupValues[] = $row[$secondGroupBy] ?? null;
}
$grouping = [$groupValues, $secondGroupValues];
}
$columnList = [];
$numericColumnList = [];
$summaryColumnList = [];
$nonSummaryColumnList = [];
$aggregatedColumnList = [];
$columnNameMap = [];
$columnTypeMap = [];
$columnData = (object) [];
foreach ($resultData->columns as $item) {
$columnList[] = $item->name;
$columnNameMap[$item->name] = $item->label;
$columnTypeMap[$item->name] = $item->fieldType;
if ($item->isNumeric) {
$numericColumnList[] = $item->name;
}
if ($item->isAggregated) {
$aggregatedColumnList[] = $item->name;
if ($item->type !== ColumnType::Summary) {
$nonSummaryColumnList[] = $item->name;
}
}
if ($item->type === ColumnType::Summary) {
$summaryColumnList[] = $item->name;
}
$columnData->{$item->name} = (object) [
'type' => $item->type->value,
];
}
$orderByList = [];
foreach ($resultData->orders as $order) {
$orderByList[] = $order->direction->value . ':' . $order->column;
}
$data = new Data(
entityType: $resultData->entityType,
columns: $columnList,
groupBy: $groupList,
orderBy: $orderByList,
columnsData: $columnData,
tableMode: $resultData->tableMode,
);
$groupValueMap = [];
$emptyStringGroupExcluded = false;
$sums = [];
$this->resultHelper->fixRows($rows, $groupList, $emptyStringGroupExcluded);
$this->resultHelper->populateGrouping($data, $groupList, $rows, $where, $grouping);
$this->resultHelper->populateRows($data, $groupList, $grouping, $rows, []);
$this->resultHelper->populateGroupValueMapForDateFunctions($data, $grouping, $groupValueMap);
$reportData = $this->gridBuilder->build($data, $rows, $groupList, $columnList, $sums);
$cellValueMaps = (object) [];
$nonSummaryColumnGroupMap = (object) [];
$nonSummaryData = null;
if (count($groupList) === 2 && $nonSummaryColumnList) {
$nonSummaryData = $this->gridBuilder->buildNonSummary(
columnList: $data->getColumns(),
summaryColumnList: $summaryColumnList,
data: $data,
rows: $rows,
groupList: $groupList,
cellValueMaps: $cellValueMaps,
nonSummaryColumnGroupMap: $nonSummaryColumnGroupMap,
);
}
if ($resultData->group->valueLabelKey) {
$groupValueMap =
$this->prepareGroupValueMap($rows, $resultData->group->name, $resultData->group->valueLabelKey);
}
if ($resultData->secondGroup?->valueLabelKey) {
$secondGroupValueMap =
$this->prepareGroupValueMap($rows, $resultData->secondGroup->name, $resultData->secondGroup->valueLabelKey);
$groupValueMap = array_merge($groupValueMap, $secondGroupValueMap);
}
$subReportSwitchDataList = null;
if ($resultData->switchItems) {
foreach ($resultData->switchItems as $item) {
$subReportSwitchDataList[] = (object) [
'name' => $item->name,
'label' => $item->label,
'entityType' => $item->entityType,
];
}
}
$groupNameMap = [$groupBy => $resultData->group->label];
if ($resultData->secondGroup) {
$groupNameMap[$resultData->secondGroup->name] = $resultData->secondGroup->label;
}
$result = new Result(
entityType: $resultData->entityType,
groupByList: $groupList,
columnList: $columnList,
numericColumnList: $numericColumnList,
summaryColumnList: $summaryColumnList,
nonSummaryColumnList: $nonSummaryColumnList,
aggregatedColumnList: $aggregatedColumnList,
nonSummaryColumnGroupMap: $nonSummaryColumnGroupMap,
sums: (object) $sums,
groupValueMap: $groupValueMap,
columnNameMap: $columnNameMap,
columnTypeMap: $columnTypeMap,
cellValueMaps: $cellValueMaps,
grouping: $grouping,
reportData: $reportData,
nonSummaryData: $nonSummaryData,
chartType: $resultData->chartType,
emptyStringGroupExcluded: $emptyStringGroupExcluded,
noSubReport: $resultData->noSubReport,
currency: $resultData->currency,
groupNameMap: $groupNameMap,
subReportSwitchDataList: $subReportSwitchDataList,
tableMode: $resultData->tableMode,
);
if ($resultData->secondGroup) {
$this->resultHelper->calculateSums($data, $result);
}
return $result;
}
/**
* @param array<string, mixed>[] $rows
* @param string $groupByAlias
* @return array<string, mixed>
*/
private function prepareGroupValueMap(array $rows, string $groupByAlias, string $key): array
{
$groupValueMap = [];
foreach ($rows as $row) {
$name = $row[$key] ?? null;
if ($name) {
$groupValueMap[$groupByAlias] ??= [];
$groupValueMap[$groupByAlias][$row[$groupByAlias]] = $name;
}
}
return $groupValueMap;
}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -0,0 +1,54 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\Internal;
use Espo\Core\Utils\Metadata;
use Espo\Modules\Advanced\Entities\Report;
use RuntimeException;
class InternalReportHelper
{
public function __construct(
private Metadata $metadata,
) {}
public function populateFields(Report $report): void
{
if (!$report->getInternalClassName()) {
throw new RuntimeException("Non-internal report.");
}
$reportParams = $this->getInternalParams($report->getInternalClassName());
$report->set('entityType', $reportParams['entityType'] ?? null);
$report->set('type', $reportParams['type'] ?? null);
$report->set('depth', $reportParams['depth'] ?? null);
$report->set('runtimeFilters', $reportParams['runtimeFilters'] ?? null);
$report->set('columns', $reportParams['columns'] ?? null);
$report->set('isInternal', true);
}
/**
* @return array<string, mixed>
*/
private function getInternalParams(string $name): array
{
return $this->metadata->get("app.advancedReport.internalReports.$name") ?? [];
}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -297,7 +297,7 @@ class JointGridExecutor
$result->setSums($sums);
}
foreach (($subReportResult->getColumnNameMap() ?? []) as $key => $name) {
foreach ($subReportResult->getColumnNameMap() as $key => $name) {
$map = $result->getColumnNameMap();
$map[$key] = $name;
$result->setColumnNameMap($map);

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -20,10 +20,12 @@ namespace Espo\Modules\Advanced\Tools\Report;
use Espo\Core\Acl\Table as AclTable;
use Espo\Core\AclManager;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\InjectableFactory;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\Where\Item as WhereItem;
use Espo\Entities\User;
@@ -32,43 +34,34 @@ use Espo\Modules\Advanced\Tools\Report\ListType\ExportParams;
use Espo\Modules\Advanced\Tools\Report\ListType\RunParams as ListRunParams;
use Espo\Modules\Advanced\Tools\Report\ListType\SubReportParams;
use Espo\ORM\Defs as OrmDefs;
use Espo\ORM\EntityCollection;
use Espo\ORM\EntityManager;
use Espo\ORM\SthCollection;
use Espo\Tools\Export\Export as ExportTool;
use Espo\Tools\Export\Params as ExportToolParams;
class ListExportService
{
private AclManager $aclManager;
private InjectableFactory $injectableFactory;
private Service $service;
private OrmDefs $ormDefs;
private EntityManager $entityManager;
public function __construct(
AclManager $aclManager,
InjectableFactory $injectableFactory,
Service $service,
OrmDefs $ormDefs,
EntityManager $entityManager
) {
$this->aclManager = $aclManager;
$this->injectableFactory = $injectableFactory;
$this->service = $service;
$this->ormDefs = $ormDefs;
$this->entityManager = $entityManager;
}
private AclManager $aclManager,
private InjectableFactory $injectableFactory,
private Service $service,
private OrmDefs $ormDefs,
private EntityManager $entityManager,
) {}
/**
* @throws Error
* @throws Forbidden
* @throws NotFound
* @throws BadRequest
*/
public function export(
string $id,
SearchParams $searchParams,
ExportParams $exportParams,
?SubReportParams $subReportParams = null,
?User $user = null
?User $user = null,
): string {
$runParams = ListRunParams::create()->withIsExport();
@@ -82,14 +75,13 @@ class ListExportService
if ($exportParams->getFieldList() === null) {
$runParams = $runParams->withFullSelect();
}
else {
} else {
$customColumnList = [];
foreach ($exportParams->getFieldList() as $item) {
$value = $item;
if (strpos($item, '_') !== false) {
if (str_contains($item, '_')) {
$value = str_replace('_', '.', $item);
}
@@ -115,23 +107,22 @@ class ListExportService
$reportResult = $subReportParams ?
$this->service->runSubReportList(
$id,
$searchParams,
$subReportParams,
$user,
$runParams
id: $id,
searchParams: $searchParams,
subReportParams: $subReportParams,
user: $user,
runParams: $runParams,
) :
$this->service->runList(
$id,
$searchParams,
$user,
$runParams
id: $id,
searchParams: $searchParams,
user: $user,
runParams: $runParams,
);
$collection = $reportResult->getCollection();
/** @var ?Report $report */
$report = $this->entityManager->getEntityById(Report::ENTITY_TYPE, $id);
$report = $this->entityManager->getRDBRepositoryByClass(Report::class)->getById($id);
if (!$report) {
throw new NotFound("Report $id not found.");
@@ -139,6 +130,13 @@ class ListExportService
$entityType = $report->getTargetEntityType();
if (
$subReportParams &&
($collection instanceof EntityCollection || $collection instanceof SthCollection)
) {
$entityType = $collection->getEntityType();
}
if (
$user &&
!$this->aclManager->checkScope($user, $entityType, AclTable::ACTION_READ)
@@ -146,42 +144,10 @@ class ListExportService
throw new Forbidden("No 'read' access for '$entityType' scope.");
}
$attributeList = null;
if ($exportParams->getAttributeList()) {
$attributeList = [];
foreach ($exportParams->getAttributeList() as $attribute) {
if (strpos($attribute, '_')) {
[$link, $field] = explode('_', $attribute);
$foreignType = $this->getForeignFieldType($entityType, $link, $field);
if ($foreignType === 'link') {
$attributeList[] = $attribute . 'Id';
$attributeList[] = $attribute . 'Name';
continue;
}
}
$attributeList[] = $attribute;
}
}
$exportParamsNew = $this->prepareExportParams($exportParams, $entityType, $report);
$export = $this->injectableFactory->create(ExportTool::class);
$exportParamsNew = ExportToolParams::create($entityType)
->withAttributeList($attributeList)
->withFieldList($exportParams->getFieldList())
->withFormat($exportParams->getFormat())
->withName($report->getName())
->withFileName($report->getName() . ' ' . date('Y-m-d'));
foreach ($exportParams->getParams() as $k => $v) {
$exportParamsNew = $exportParamsNew->withParam($k, $v);
}
return $export
->setParams($exportParamsNew)
->setCollection($collection)
@@ -211,4 +177,58 @@ class ListExportService
return $entityDefs->getField($field)->getType();
}
private function prepareExportParams(
ExportParams $exportParams,
string $entityType,
Report $report,
): ExportToolParams {
$attributeList = null;
if ($exportParams->getAttributeList()) {
$attributeList = $this->prepareAttributeList($entityType, $exportParams->getAttributeList());
}
$exportParamsNew = ExportToolParams::create($entityType)
->withAttributeList($attributeList)
->withFieldList($exportParams->getFieldList())
->withFormat($exportParams->getFormat())
->withName($report->getName())
->withFileName($report->getName() . ' ' . date('Y-m-d'));
foreach ($exportParams->getParams() as $k => $v) {
$exportParamsNew = $exportParamsNew->withParam($k, $v);
}
return $exportParamsNew;
}
/**
* @param string[] $setAttributeList
* @return string[]
*/
public function prepareAttributeList(string $entityType, array $setAttributeList): array
{
$attributeList = [];
foreach ($setAttributeList as $attribute) {
if (strpos($attribute, '_')) {
[$link, $field] = explode('_', $attribute);
$foreignType = $this->getForeignFieldType($entityType, $link, $field);
if ($foreignType === FieldType::LINK) {
$attributeList[] = $attribute . 'Id';
$attributeList[] = $attribute . 'Name';
continue;
}
}
$attributeList[] = $attribute;
}
return $attributeList;
}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -25,10 +25,12 @@ use Espo\Core\Select\SelectBuilderFactory;
use Espo\Core\Select\Where\Item as WhereItem;
use Espo\Core\Select\Where\ItemBuilder as WhereItemBuilder;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\FieldUtil;
use Espo\Entities\Preferences;
use Espo\Entities\User;
use Espo\Modules\Advanced\Tools\Report\SelectHelper;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\Part\Selection;
use Espo\ORM\Query\SelectBuilder;
@@ -38,7 +40,8 @@ class QueryPreparator
private SelectHelper $selectHelper,
private SelectBuilderFactory $selectBuilderFactory,
private Config $config,
private EntityManager $entityManager
private EntityManager $entityManager,
private FieldUtil $fieldUtil,
) {}
/**
@@ -51,11 +54,27 @@ class QueryPreparator
public function prepare(
Data $data,
?SearchParams $searchParams = null,
?User $user = null
?User $user = null,
): SelectBuilder {
$searchParams = $searchParams ?? SearchParams::create();
$selectAttributes = [Attribute::ID];
// Needed for dependent attributes.
foreach ($data->getColumns() as $column) {
if (str_contains($column, '.')) {
continue;
}
array_push(
$selectAttributes,
...$this->fieldUtil->getAttributeList($data->getEntityType(), $column)
);
}
$searchParams = $searchParams->withSelect($selectAttributes);
$orderBy = $searchParams->getOrderBy();
$order = $searchParams->getOrder();
@@ -68,10 +87,12 @@ class QueryPreparator
$searchParams = $this->applyTimeZoneToSearchParams($searchParams, $user);
}
//print_r($searchParams);die;
$selectBuilder = $this->selectBuilderFactory
->create()
->from($data->getEntityType())
->withSearchParams($searchParams->withSelect(['id']));
->withSearchParams($searchParams);
if ($user) {
$selectBuilder
@@ -102,7 +123,7 @@ class QueryPreparator
// Add columns applied from order-by.
$queryBuilder->select(
// Prevent issue in ORM (as of v7.5).
// Prevent issue in ORM (as of v7.5).
array_map(function (Selection $selection) {
return !$selection->getAlias() ?
$selection->getExpression() :
@@ -110,7 +131,7 @@ class QueryPreparator
}, $intermediateQuery->getSelect())
);
$this->selectHelper->handleColumns($data->getColumns(), $queryBuilder);
$this->selectHelper->handleColumns($data->getColumns(), $queryBuilder, true);
}
if ($data->getFiltersWhere()) {

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -1,4 +1,20 @@
<?php
/***********************************************************************************
* The contents of this file are subject to the Extension License Agreement
* ("Agreement") which can be viewed at
* https://www.espocrm.com/extension-license-agreement/.
* By copying, installing downloading, or using this file, You have unconditionally
* agreed to the terms and conditions of the Agreement, and You may not use this
* file except in compliance with the Agreement. Under the terms of the Agreement,
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
* redistribute, market, publish, commercialize, or otherwise transfer rights or
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report\ListType;
@@ -6,17 +22,19 @@ use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\Where\Item as WhereItem;
use Espo\Core\Utils\FieldUtil;
use Espo\Entities\User;
use Espo\Modules\Advanced\Tools\Report\GridType\Data as Data;
use Espo\Modules\Advanced\Tools\Report\SelectHelper;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\Select;
class SubListQueryPreparator
{
public function __construct(
private SubReportQueryPreparator $subReportQueryPreparator,
private SelectHelper $selectHelper
private SelectHelper $selectHelper,
private FieldUtil $fieldUtil,
) {}
/**
@@ -36,7 +54,21 @@ class SubListQueryPreparator
?User $user,
): Select {
$searchParams = SearchParams::create()->withSelect(['id']);
$selectAttributes = [Attribute::ID];
// Needed for dependent attributes.
foreach ($data->getColumns() as $column) {
if (str_contains($column, '.') || str_contains($column, ':')) {
continue;
}
array_push(
$selectAttributes,
...$this->fieldUtil->getAttributeList($data->getEntityType(), $column)
);
}
$searchParams = SearchParams::create()->withSelect($selectAttributes);
if ($where) {
$searchParams = $searchParams->withWhere($where);

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -20,24 +20,17 @@ namespace Espo\Modules\Advanced\Tools\Report\ListType;
class SubReportParams
{
/** @var ?scalar */
private $groupValue;
/** @var ?scalar */
private $groupValue2;
/**
* @param ?scalar $groupValue
* @param ?scalar $groupValue2
*/
public function __construct(
private int $groupIndex,
$groupValue,
private $groupValue,
private bool $hasGroupValue2 = false,
$groupValue2 = null
) {
$this->groupValue = $groupValue;
$this->groupValue2 = $groupValue2;
}
private $groupValue2 = null,
private ?string $target = null,
) {}
public function getGroupIndex(): int
{
@@ -64,4 +57,9 @@ class SubReportParams
{
return $this->groupValue2;
}
public function getTarget(): ?string
{
return $this->target;
}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -21,10 +21,12 @@ namespace Espo\Modules\Advanced\Tools\Report;
use Espo\Core\Acl;
use Espo\Core\Acl\Table as AclTable;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error\Body;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Record\ServiceContainer;
use Espo\Entities\User;
use Espo\Modules\Advanced\Entities\Report;
use Espo\Modules\Advanced\Tools\Report\Internal\InternalReportHelper;
use Espo\ORM\EntityManager;
use stdClass;
@@ -37,6 +39,7 @@ class PreviewReportProvider
private ServiceContainer $serviceContainer,
private User $user,
private ReportHelper $reportHelper,
private InternalReportHelper $internalReportHelper,
) {}
/**
@@ -45,9 +48,57 @@ class PreviewReportProvider
*/
public function get(stdClass $data): Report
{
$report = $this->entityManager->getRDBRepositoryByClass(Report::class)->getNew();
$report = $this->prepareReport($data);
unset($data->isInternal);
foreach ($report->getJoinedReportIdList() as $subReportId) {
$subReport = $this->entityManager->getRDBRepositoryByClass(Report::class)->getById($subReportId);
if (!$subReport) {
continue;
}
$this->reportHelper->checkReportCanBeRun($subReport);
if (!$this->acl->checkEntityRead($subReport)) {
throw new Forbidden("No access to sub-report.");
}
}
$this->reportHelper->checkReportCanBeRun($report);
$this->accessCheck($report);
return $report;
}
/**
* @throws Forbidden
*/
private function accessCheck(Report $report): void
{
if (
!$this->user->isAdmin() &&
($report->isInternal() || $report->getInternalClassName())
) {
throw Forbidden::createWithBody('onlyAdminCanPreviewInternalReports',
Body::create()->withMessageTranslation('onlyAdminCanPreviewInternalReports', Report::ENTITY_TYPE)
);
}
if (
$report->getTargetEntityType() &&
!$this->acl->checkScope($report->getTargetEntityType(), AclTable::ACTION_READ)
) {
throw new Forbidden("No 'read' access to target entity.");
}
}
/**
* @throws BadRequest
*/
private function prepareReport(stdClass $data): Report
{
$report = $this->entityManager->getRDBRepositoryByClass(Report::class)->getNew();
$attributeList = [
'entityType',
@@ -70,6 +121,9 @@ class PreviewReportProvider
'joinedReports',
'joinedReportLabel',
'joinedReportDataList',
'isInternal',
'internalClassName',
'internalParams',
];
foreach (array_keys(get_object_vars($data)) as $attribute) {
@@ -77,36 +131,19 @@ class PreviewReportProvider
unset($data->$attribute);
}
}
$report->setMultiple($data);
$report->setApplyAcl();
$report->setName('Unnamed');
$report
->setApplyAcl()
->setName('Unnamed');
if ($report->getInternalClassName()) {
$this->internalReportHelper->populateFields($report);
}
$this->serviceContainer->getByClass(Report::class)->processValidation($report, $data);
foreach ($report->getJoinedReportIdList() as $subReportId) {
$subReport = $this->entityManager->getRDBRepositoryByClass(Report::class)->getById($subReportId);
if (!$subReport) {
continue;
}
$this->reportHelper->checkReportCanBeRun($subReport);
if (!$this->acl->checkEntityRead($subReport)) {
throw new Forbidden("No access to sub-report.");
}
}
$this->reportHelper->checkReportCanBeRun($report);
if (
$report->getTargetEntityType() &&
!$this->acl->checkScope($report->getTargetEntityType(), AclTable::ACTION_READ)
) {
throw new Forbidden("No 'read' access to target entity.");
}
return $report;
}
}

View File

@@ -11,13 +11,14 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
namespace Espo\Modules\Advanced\Tools\Report;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Formula\Exceptions\Error as FormulaError;
@@ -53,19 +54,28 @@ class ReportHelper
private FormulaManager $formulaManager,
private Config $config,
private Preferences $preferences,
private FormulaChecker $formulaChecker
private FormulaChecker $formulaChecker,
) {}
/**
* @throws Error
* @return ListReport|GridReport
*/
public function createInternalReport(Report $report): object
public function createInternalReport(Report $report): ListReport|GridReport
{
$className = $report->get('internalClassName');
$name = $report->get('internalClassName');
if ($className && stripos($className, ':') !== false) {
[$moduleName, $reportName] = explode(':', $className);
if (!$name) {
throw new Error('Internal report name is not specified.');
}
$className = $this->metadata->get("app.advancedReport.internalReports.$name.className");
if (!$className) {
if (stripos($name, ':') === false) {
throw new Error("Internal report $name is not defined.");
}
[$moduleName, $reportName] = explode(':', $name);
if ($moduleName === 'Custom') {
$className = "Espo\\Custom\\Reports\\$reportName";
@@ -74,13 +84,17 @@ class ReportHelper
}
}
if (!$className) {
throw new Error('No class name specified for internal report.');
if (!class_exists($className)) {
throw new Error("Class $className for report $name does not exist.");
}
/** @var class-string<ListReport|GridReport> $className */
return $this->injectableFactory->create($className);
$binding = BindingContainerBuilder::create()
->bindInstance(Report::class, $report)
->build();
return $this->injectableFactory->createWithBinding($className, $binding);
}
/**
@@ -109,18 +123,19 @@ class ReportHelper
}
return new GridData(
$report->getTargetEntityType(),
$report->getColumns(),
$report->getGroupBy(),
$report->getOrderBy(),
$report->getApplyAcl(),
$this->fetchFiltersWhereFromReport($report),
$report->get('chartType'),
get_object_vars($report->get('chartColors') ?? (object) []),
$report->get('chartColor'),
$report->get('chartDataList'),
($report->get('data') ?? (object) [])->success ?? null,
$report->getColumnsData(),
entityType: $report->getTargetEntityType(),
columns: $report->getColumns(),
groupBy: $report->getGroupBy(),
orderBy: $report->getOrderBy(),
applyAcl: $report->getApplyAcl(),
filtersWhere: $this->fetchFiltersWhereFromReport($report),
chartType: $report->getChartType(),
chartColors: get_object_vars($report->get('chartColors') ?? (object) []),
chartColor: $report->get('chartColor'),
chartDataList: $report->get('chartDataList'),
success: ($report->get('data') ?? (object) [])->success ?? null,
columnsData: $report->getColumnsData(),
tableMode: $report->getTableMode(),
);
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -20,8 +20,13 @@ namespace Espo\Modules\Advanced\Tools\Report;
use DateTime;
use DateTimeZone;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\Binding\ContextualBinder;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\InjectableFactory;
use Espo\Core\Select\Applier\AdditionalApplier;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\Where\Converter;
use Espo\Core\Select\Where\ConverterFactory;
use Espo\Core\Select\Where\Item as WhereItem;
@@ -58,7 +63,8 @@ class SelectHelper
private GridHelper $gridHelper,
private FieldUtil $fieldUtil,
private User $user,
private ConverterFactory $converterFactory
private ConverterFactory $converterFactory,
private InjectableFactory $injectableFactory,
) {}
/**
@@ -415,8 +421,9 @@ class SelectHelper
/**
* @param string[] $columns
* @param bool $isList Should be true only for List report. Should not be true for Sub-List.
*/
public function handleColumns(array $columns, SelectBuilder $queryBuilder): void
public function handleColumns(array $columns, SelectBuilder $queryBuilder, bool $isList = false): void
{
$entityType = $queryBuilder->build()->getFrom();
@@ -425,21 +432,24 @@ class SelectHelper
}
foreach ($columns as $item) {
$this->handleColumnsItem($item, $entityType, $queryBuilder);
$this->handleColumnsItem($item, $entityType, $queryBuilder, $isList);
}
}
/**
* @todo Use the selectDefs attribute dependency map.
* @todo Use the selectDefs attribute dependency map? Or not needed as already applied with the select manager.
*/
private function handleColumnsItem(
string $item,
string $entityType,
SelectBuilder $queryBuilder
SelectBuilder $queryBuilder,
bool $isList = false,
): void {
$columnData = $this->gridHelper->getDataFromColumnName($entityType, $item);
$entityDefs = $this->entityManager->getDefs()->getEntity($entityType);
if ($columnData->function && !$columnData->link && $columnData->field) {
$this->addSelect($item, $queryBuilder);
@@ -460,6 +470,10 @@ class SelectHelper
return;
}
if ($isList) {
return;
}
if (str_contains($item, ':') && str_contains($item, '.')) {
$this->handleLeftJoins($item, $entityType, $queryBuilder);
}
@@ -486,8 +500,11 @@ class SelectHelper
if ($type === 'currency') {
$this->addSelect($item, $queryBuilder);
$this->addSelect($item . 'Currency', $queryBuilder);
$this->addSelect($item . 'Converted', $queryBuilder);
if (!$entityDefs->tryGetField($item)?->getParam('notStorable')) {
$this->addSelect($item . 'Currency', $queryBuilder);
$this->addSelect($item . 'Converted', $queryBuilder);
}
return;
}
@@ -695,6 +712,8 @@ class SelectHelper
throw new LogicException("No from.");
}
$this->applyWhereFilterAdditionalAppliers($entityType, $whereItem, $queryBuilder);
// Supposed to be applied by the scanner.
//$this->applyLeftJoinsFromWhere($whereItem, $queryBuilder);
@@ -886,4 +905,47 @@ class SelectHelper
{
return $this->converterFactory->create($entityType, $this->user);
}
/**
* @return class-string<AdditionalApplier>[]
*/
private function getWhereFiltersApplierClassNameList(string $entityType): array
{
/** @var class-string<AdditionalApplier>[] */
return $this->metadata
->get("app.advancedReport.entityParams.$entityType.whereFilterAdditionalApplierClassNameList") ?? [];
}
/**
* @param class-string<AdditionalApplier> $className
*/
private function createAdditionalApplier(string $entityType, string $className): AdditionalApplier
{
return $this->injectableFactory->createWithBinding(
$className,
BindingContainerBuilder::create()
->bindInstance(User::class, $this->user)
->inContext($className, function (ContextualBinder $binder) use ($entityType) {
$binder->bindValue('$entityType', $entityType);
})
->build()
);
}
private function applyWhereFilterAdditionalAppliers(
string $entityType,
WhereItem $whereItem,
SelectBuilder $queryBuilder,
): void {
$additionalApplierClassNameList = $this->getWhereFiltersApplierClassNameList($entityType);
foreach ($additionalApplierClassNameList as $className) {
$applier = $this->createAdditionalApplier($entityType, $className);
$searchParams = SearchParams::create()->withWhere($whereItem);
$applier->apply($queryBuilder, $searchParams);
}
}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -35,6 +35,7 @@ use Espo\Entities\Job;
use Espo\Entities\User;
use Espo\Modules\Advanced\Business\Report\EmailBuilder;
use Espo\Modules\Advanced\Entities\Report as ReportEntity;
use Espo\Modules\Advanced\Tools\Report\Export\GridExportService;
use Espo\Modules\Advanced\Tools\Report\GridType\Result as GridResult;
use Espo\Modules\Advanced\Tools\Report\Jobs\Send;
use Espo\Modules\Advanced\Tools\Report\ListType\Result as ListResult;
@@ -60,7 +61,8 @@ class SendingService
private Config $config,
private FieldUtil $fieldUtil,
private InjectableFactory $injectableFactory,
private EmailBuilder $emailBuilder
private EmailBuilder $emailBuilder,
private ListExportService $listExportService,
) {}
private function getSendingListMaxCount(): int
@@ -218,25 +220,19 @@ class SendingService
throw new RuntimeException("Bad result.");
}
if (!$entityType) {
throw new RuntimeException("No entity type.");
}
$fieldList = $report->getColumns();
foreach ($fieldList as $key => $field) {
if (strpos($field, '.')) {
$fieldList[$key] = str_replace('.', '_', $field);
foreach ($fieldList as $i => $field) {
if (str_contains($field, '.')) {
$fieldList[$i] = str_replace('.', '_', $field);
}
}
$attributeList = [];
foreach ($fieldList as $field) {
$fieldAttributeList = $this->fieldUtil->getAttributeList($report->getTargetEntityType(), $field);
if (count($fieldAttributeList) > 0) {
$attributeList = array_merge($attributeList, $fieldAttributeList);
} else {
$attributeList[] = $field;
}
}
$attributeList = $this->prepareListAttributeList($fieldList, $report, $entityType);
$exportParams = ExportToolParams::create($entityType)
->withFieldList($fieldList)
@@ -432,4 +428,27 @@ class SendingService
{
$this->emailBuilder->buildEmailData($data, $result, $report, true);
}
/**
* @param string[] $fieldList
* @return string[]
*/
private function prepareListAttributeList(array $fieldList, ReportEntity $report, ?string $entityType): array
{
$attributeList = [];
foreach ($fieldList as $field) {
if (str_contains($field, '_')) {
$attributeList[] = $field;
continue;
}
$itAttributeList = $this->fieldUtil->getAttributeList($report->getTargetEntityType(), $field);
$attributeList = array_merge($attributeList, $itAttributeList);
}
return $this->listExportService->prepareAttributeList($entityType, $attributeList);
}
}

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/
@@ -19,6 +19,7 @@
namespace Espo\Modules\Advanced\Tools\Report;
use Espo\Core\Acl\Table as AclTable;
use Espo\Core\Currency\ConfigDataProvider as CurrencyConfig;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
@@ -85,7 +86,8 @@ class Service
private ListLoadProcessor $listLoadProcessor,
private Log $log,
private GridQueryPreparator $gridQueryPreparator,
private SubListQueryPreparator $subListQueryPreparator
private SubListQueryPreparator $subListQueryPreparator,
private CurrencyConfig $currencyConfig,
) {}
/**
@@ -612,6 +614,7 @@ class Service
$columnTypeMap = [];
$columnDecimalPlacesMap = [];
$columnNameMap = [];
$groupNameMap = [];
$nonSummaryColumnList = array_values(array_diff($data->getColumns(), $summaryColumnList));
$emptyStringGroupExcluded = false;
@@ -627,6 +630,7 @@ class Service
$this->gridResultHelper->populateGroupValueMapByLinkColumns($data, $linkColumnList, $rows, $groupValueMap);
$this->gridResultHelper->populateGroupValueMapForDateFunctions($data, $grouping, $groupValueMap);
$this->gridResultHelper->populateColumnInfo($data, $columnTypeMap, $columnDecimalPlacesMap, $columnNameMap);
$this->gridResultHelper->populateGroupNameMap($data, $groupNameMap);
$this->gridResultHelper->sortGrouping($data, $grouping, $groupValueMap);
$reportData = $this->gridBuilder->build(
@@ -679,6 +683,9 @@ class Service
chartDataList: $data->getChartDataList(), // stdClass[]
columnDecimalPlacesMap: (object) $columnDecimalPlacesMap, // object<?int>,
emptyStringGroupExcluded: $emptyStringGroupExcluded,
currency: $this->currencyConfig->getDefaultCurrency(),
groupNameMap: $groupNameMap,
tableMode: $data->getTableMode(),
);
$resultObject->setSuccess($data->getSuccess());

View File

@@ -11,7 +11,7 @@
* usage to the software or any modified version or derivative work of the software
* created by or for you.
*
* Copyright (C) 2015-2025 EspoCRM, Inc.
* Copyright (C) 2015-2026 EspoCRM, Inc.
*
* License ID: 19bc86a68a7bb01f458cb391d43a9212
************************************************************************************/