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:
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
1454
custom/Espo/Modules/Advanced/Tools/Report/Export/ExportXlsx.php
Normal file
1454
custom/Espo/Modules/Advanced/Tools/Report/Export/ExportXlsx.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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") ?? [];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
@@ -348,7 +348,7 @@ class SendEmailService
|
||||
|
||||
private function applyTrackingUrlsToEmailBody(string $body, string $toEmailAddress): string
|
||||
{
|
||||
$siteUrl = $this->config->get('siteUrl');
|
||||
$siteUrl = $this->getSiteUrl();
|
||||
|
||||
if (!str_contains($body, '{trackingUrl:')) {
|
||||
return $body;
|
||||
@@ -586,7 +586,7 @@ class SendEmailService
|
||||
Sender $sender,
|
||||
): string {
|
||||
|
||||
$siteUrl = $this->config->get('siteUrl');
|
||||
$siteUrl = $this->getSiteUrl();
|
||||
|
||||
$hash = $this->hasher->hash($toEmailAddress);
|
||||
|
||||
@@ -658,4 +658,10 @@ class SendEmailService
|
||||
|
||||
return [$subject, $body];
|
||||
}
|
||||
|
||||
|
||||
private function getSiteUrl(): ?string
|
||||
{
|
||||
return $this->config->get('workflowEmailSiteUrl') ?? $this->config->get('siteUrl');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
************************************************************************************/
|
||||
|
||||
Reference in New Issue
Block a user