Initial commit
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table as AclTable;
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Record\SearchParamsFetcher;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Entities\Report;
|
||||
use Espo\Modules\Advanced\Tools\Report\PreviewReportProvider;
|
||||
use Espo\Modules\Advanced\Tools\Report\Service;
|
||||
use JsonException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GetRunListPreview implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private Service $service,
|
||||
private Acl $acl,
|
||||
private User $user,
|
||||
private SearchParamsFetcher $searchParamsFetcher,
|
||||
private PreviewReportProvider $previewReportProvider,
|
||||
) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$this->checkAccess();
|
||||
|
||||
$data = $this->fetchData($request);
|
||||
$report = $this->previewReportProvider->get($data);
|
||||
|
||||
if ($report->getType() !== Report::TYPE_LIST) {
|
||||
throw new BadRequest("Non-list type.");
|
||||
}
|
||||
|
||||
$searchParams = $this->searchParamsFetcher->fetch($request);
|
||||
|
||||
// Passing the user is important.
|
||||
$result = $this->service->reportRunList($report, $searchParams, $this->user);
|
||||
|
||||
return ResponseComposer::json([
|
||||
'list' => $result->getCollection()->getValueMapList(),
|
||||
'total' => $result->getTotal(),
|
||||
'columns' => $result->getColumns(),
|
||||
'columnsData' => $result->getColumnsData(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function fetchData(Request $request): stdClass
|
||||
{
|
||||
try {
|
||||
$data = Json::decode($request->getQueryParam('data'));
|
||||
} catch (JsonException) {
|
||||
throw new BadRequest("Bad data.");
|
||||
}
|
||||
|
||||
if (!$data instanceof stdClass) {
|
||||
throw new BadRequest("No data.");
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkAccess(): void
|
||||
{
|
||||
if (!$this->acl->checkScope(Report::ENTITY_TYPE, AclTable::ACTION_CREATE)) {
|
||||
throw new Forbidden("No 'create' access.");
|
||||
}
|
||||
|
||||
if ($this->user->isPortal()) {
|
||||
throw new Forbidden("No access from portal.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table as AclTable;
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Entities\Report;
|
||||
use Espo\Modules\Advanced\Tools\Report\PreviewReportProvider;
|
||||
use Espo\Modules\Advanced\Tools\Report\Service;
|
||||
use JsonException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class PostRunGridPreview implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private Service $service,
|
||||
private Acl $acl,
|
||||
private User $user,
|
||||
private PreviewReportProvider $previewReportProvider,
|
||||
) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$this->checkAccess();
|
||||
|
||||
$data = $this->fetchData($request);
|
||||
$report = $this->previewReportProvider->get($data);
|
||||
|
||||
if (!in_array($report->getType(), [Report::TYPE_GRID, Report::TYPE_JOINT_GRID])) {
|
||||
throw new BadRequest("Bad report type.");
|
||||
}
|
||||
|
||||
$where = $request->getParsedBody()->where ?? null;
|
||||
|
||||
$whereItem = null;
|
||||
|
||||
if ($where) {
|
||||
$whereItem = WhereItem::fromRawAndGroup(self::normalizeWhere($where));
|
||||
}
|
||||
|
||||
// Passing the user is important.
|
||||
$result = $this->service->reportRunGridOrJoint($report, $whereItem, $this->user);
|
||||
|
||||
return ResponseComposer::json($result->toRaw());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function fetchData(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody()->data ?? null;
|
||||
|
||||
if (!$data instanceof stdClass) {
|
||||
throw new BadRequest("No data.");
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private static function normalizeWhere(mixed $where): mixed
|
||||
{
|
||||
try {
|
||||
return Json::decode(Json::encode($where), true);
|
||||
} catch (JsonException) {
|
||||
throw new BadRequest("Bad where");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkAccess(): void
|
||||
{
|
||||
if (!$this->acl->checkScope(Report::ENTITY_TYPE, AclTable::ACTION_CREATE)) {
|
||||
throw new Forbidden("No 'create' access.");
|
||||
}
|
||||
|
||||
if ($this->user->isPortal()) {
|
||||
throw new Forbidden("No access from portal.");
|
||||
}
|
||||
}
|
||||
}
|
||||
124
custom/Espo/Modules/Advanced/Tools/Report/ExportService.php
Normal file
124
custom/Espo/Modules/Advanced/Tools/Report/ExportService.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?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\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Entities\Report;
|
||||
use Espo\Modules\Advanced\Entities\Report as ReportEntity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class ExportService
|
||||
{
|
||||
private const LIST_REPORT_MAX_SIZE = 3000;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Config $config,
|
||||
private Service $service,
|
||||
private SendingService $sendingService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function prepareExportAttachment(Report $report, ?User $user = null): Attachment
|
||||
{
|
||||
$result = $this->prepareResult($report, $user);
|
||||
|
||||
$attachmentId = $this->sendingService->getExportAttachmentId($report, $result, null, $user);
|
||||
|
||||
if (!$attachmentId) {
|
||||
throw new Error("Could not generate an export file for report {$report->getId()}.");
|
||||
}
|
||||
|
||||
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($attachmentId);
|
||||
|
||||
if (!$attachment) {
|
||||
throw new Error("Could not fetch the export attachment.");
|
||||
}
|
||||
|
||||
$this->prepareAttachmentFields($attachment);
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
private function getSendingListMaxCount(): int
|
||||
{
|
||||
return $this->config->get('reportSendingListMaxCount', self::LIST_REPORT_MAX_SIZE);
|
||||
}
|
||||
|
||||
private function prepareListSearchParams(ReportEntity $report): SearchParams
|
||||
{
|
||||
$searchParams = SearchParams::create()
|
||||
->withMaxSize($this->getSendingListMaxCount());
|
||||
|
||||
$orderByList = $report->getOrderByList();
|
||||
|
||||
if ($orderByList) {
|
||||
$arr = explode(':', $orderByList);
|
||||
|
||||
/**
|
||||
* @var 'ASC'|'DESC' $orderDirection
|
||||
* @noinspection PhpRedundantVariableDocTypeInspection
|
||||
*/
|
||||
$orderDirection = strtoupper($arr[0]);
|
||||
|
||||
$searchParams = $searchParams
|
||||
->withOrderBy($arr[1])
|
||||
->withOrder($orderDirection);
|
||||
}
|
||||
|
||||
return $searchParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function prepareResult(ReportEntity $report, ?User $user): ListType\Result|GridType\Result
|
||||
{
|
||||
if ($report->getType() === ReportEntity::TYPE_LIST) {
|
||||
$searchParams = $this->prepareListSearchParams($report);
|
||||
|
||||
return $this->service->runList($report->getId(), $searchParams, $user);
|
||||
}
|
||||
|
||||
return $this->service->runGrid($report->getId(), null, $user);
|
||||
}
|
||||
|
||||
private function prepareAttachmentFields(Attachment $attachment): void
|
||||
{
|
||||
$attachment->setRole(Attachment::ROLE_EXPORT_FILE);
|
||||
$attachment->setParent(null);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
}
|
||||
}
|
||||
100
custom/Espo/Modules/Advanced/Tools/Report/FormulaChecker.php
Normal file
100
custom/Espo/Modules/Advanced/Tools/Report/FormulaChecker.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?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\Exceptions\Error\Body;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class FormulaChecker
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $allowedFunctionList = [
|
||||
'ifThen',
|
||||
'ifThenElse',
|
||||
'env\\userAttribute',
|
||||
'record\\attribute',
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private array $allowedNamespaceList = [
|
||||
'datetime',
|
||||
'number',
|
||||
'string',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
public function sanitize(string $script): string
|
||||
{
|
||||
$script = str_replace('record\\attribute', 'report\\recordAttribute', $script);
|
||||
|
||||
if (!class_exists("Espo\\Core\\Formula\\Functions\\EnvGroup\\UserAttributeSafeType")) {
|
||||
return $script;
|
||||
}
|
||||
|
||||
return str_replace('env\\userAttribute', 'env\\userAttributeSafe', $script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a formula script for a complex expression.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function check(string $script): void
|
||||
{
|
||||
$script = str_replace(["\n", "\r", "\t", ' '], '', $script);
|
||||
$script = str_replace(';', ' ', $script);
|
||||
|
||||
preg_match_all('/[a-zA-Z1-9\\\\]*\(/', $script, $matches);
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
if (!$matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
$allowedFunctionList = array_merge(
|
||||
$this->allowedFunctionList,
|
||||
$this->metadata->get('app.advancedReport.allowedFilterFormulaFunctionList', [])
|
||||
);
|
||||
|
||||
foreach ($matches[0] as $part) {
|
||||
$part = substr($part, 0, -1);
|
||||
|
||||
if (in_array($part, $allowedFunctionList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->allowedNamespaceList as $namespace) {
|
||||
if (str_starts_with($part, $namespace . '\\')) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
throw Forbidden::createWithBody(
|
||||
"Not allowed formula in filter.",
|
||||
Body::create()
|
||||
->withMessageTranslation('notAllowedFormulaInFilter', 'Report')
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
634
custom/Espo/Modules/Advanced/Tools/Report/GridExportService.php
Normal file
634
custom/Espo/Modules/Advanced/Tools/Report/GridExportService.php
Normal file
@@ -0,0 +1,634 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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\GridType;
|
||||
|
||||
class ColumnData
|
||||
{
|
||||
public ?string $function;
|
||||
public string $field;
|
||||
public ?string $entityType;
|
||||
public ?string $link;
|
||||
public ?string $fieldType;
|
||||
|
||||
public function __construct(
|
||||
?string $function,
|
||||
string $field,
|
||||
?string $entityType,
|
||||
?string $link,
|
||||
?string $fieldType
|
||||
) {
|
||||
$this->function = $function;
|
||||
$this->field = $field;
|
||||
$this->entityType = $entityType;
|
||||
$this->link = $link;
|
||||
$this->fieldType = $fieldType;
|
||||
}
|
||||
}
|
||||
216
custom/Espo/Modules/Advanced/Tools/Report/GridType/Data.php
Normal file
216
custom/Espo/Modules/Advanced/Tools/Report/GridType/Data.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?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\GridType;
|
||||
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use stdClass;
|
||||
|
||||
class Data
|
||||
{
|
||||
public const COLUMN_TYPE_SUMMARY = 'Summary';
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @param string[] $columns
|
||||
* @param string[] $groupBy
|
||||
* @param string[] $orderBy
|
||||
* @param ?string[] $chartColors
|
||||
* @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
|
||||
) {
|
||||
$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;
|
||||
}
|
||||
|
||||
public function getEntityType(): string
|
||||
{
|
||||
return $this->entityType;
|
||||
}
|
||||
|
||||
public function getSuccess(): ?string
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getOrderBy(): array
|
||||
{
|
||||
return $this->orderBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getColumns(): array
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getGroupBy(): array
|
||||
{
|
||||
return $this->groupBy;
|
||||
}
|
||||
|
||||
public function applyAcl(): bool
|
||||
{
|
||||
return $this->applyAcl;
|
||||
}
|
||||
|
||||
public function getFiltersWhere(): ?WhereItem
|
||||
{
|
||||
return $this->filtersWhere;
|
||||
}
|
||||
|
||||
public function getChartType(): ?string
|
||||
{
|
||||
return $this->chartType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getChartColors(): ?array
|
||||
{
|
||||
return $this->chartColors;
|
||||
}
|
||||
|
||||
public function getChartColor(): ?string
|
||||
{
|
||||
return $this->chartColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?stdClass[]
|
||||
*/
|
||||
public function getChartDataList(): ?array
|
||||
{
|
||||
return $this->chartDataList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAggregatedColumns(): array
|
||||
{
|
||||
return $this->aggregatedColumns;
|
||||
}
|
||||
|
||||
public function getColumnLabel(string $column): ?string
|
||||
{
|
||||
if (!isset($this->columnsData->$column)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = $this->columnsData->$column;
|
||||
|
||||
if (!is_object($item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $item->label ?? null;
|
||||
}
|
||||
|
||||
public function getColumnType(string $column): ?string
|
||||
{
|
||||
if (!isset($this->columnsData->$column)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = $this->columnsData->$column;
|
||||
|
||||
if (!is_object($item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $item->type ?? null;
|
||||
}
|
||||
|
||||
public function getColumnDecimalPlaces(string $column): ?int
|
||||
{
|
||||
if (!isset($this->columnsData->$column)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = $this->columnsData->$column;
|
||||
|
||||
if (!is_object($item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $item->decimalPlaces ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $aggregatedColumns
|
||||
*/
|
||||
public function withAggregatedColumns(array $aggregatedColumns): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->aggregatedColumns = $aggregatedColumns;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
<?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\GridType;
|
||||
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Data as GridData;
|
||||
use stdClass;
|
||||
|
||||
class GridBuilder
|
||||
{
|
||||
private const ROUND_PRECISION = 4;
|
||||
private const STUB_KEY = '__STUB__';
|
||||
|
||||
public function __construct(
|
||||
private Util $util,
|
||||
private Helper $helper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>[] $rows
|
||||
* @param string[] $groupList
|
||||
* @param string[] $columns
|
||||
* @param array<string, numeric> $sums
|
||||
* @param string[] $groups
|
||||
*/
|
||||
public function build(
|
||||
Data $data,
|
||||
array $rows,
|
||||
array $groupList,
|
||||
array $columns,
|
||||
array &$sums,
|
||||
stdClass $cellValueMaps,
|
||||
array $groups = [],
|
||||
int $number = 0
|
||||
): stdClass {
|
||||
|
||||
$gridData = $this->buildInternal(
|
||||
$data,
|
||||
$rows,
|
||||
$groupList,
|
||||
$columns,
|
||||
$sums,
|
||||
$cellValueMaps,
|
||||
$groups,
|
||||
$number
|
||||
);
|
||||
|
||||
foreach ($gridData as $k => $v) {
|
||||
$gridData[$k] = (object) $v;
|
||||
|
||||
/** @var array<string, mixed> $v */
|
||||
|
||||
foreach ($v as $k1 => $v1) {
|
||||
if (is_array($v1)) {
|
||||
$gridData[$k]->$k1 = (object) $v1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (object) $gridData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>[] $rows
|
||||
* @param string[] $groupList
|
||||
* @param string[] $columns
|
||||
* @param array<string, numeric> $sums
|
||||
* @param string[] $groups
|
||||
* @return array<string|int, array<string|int, mixed>|numeric>
|
||||
*/
|
||||
public function buildInternal(
|
||||
Data $data,
|
||||
array $rows,
|
||||
array $groupList,
|
||||
array $columns,
|
||||
array &$sums,
|
||||
stdClass $cellValueMaps,
|
||||
array $groups,
|
||||
int $number
|
||||
): array {
|
||||
|
||||
$entityType = $data->getEntityType();
|
||||
|
||||
if (count($data->getGroupBy()) === 0) {
|
||||
$groupList = [self::STUB_KEY];
|
||||
}
|
||||
|
||||
$k = count($groups);
|
||||
|
||||
$gridData = [];
|
||||
|
||||
if ($k <= count($groupList) - 1) {
|
||||
$groupColumn = $groupList[$k];
|
||||
|
||||
$keys = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
foreach ($groups as $i => $g) {
|
||||
$groupAlias = $this->util->sanitizeSelectAlias($groupList[$i]);
|
||||
|
||||
if ($row[$groupAlias] !== $g) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$groupAlias = $this->util->sanitizeSelectAlias($groupColumn);
|
||||
|
||||
$key = $row[$groupAlias];
|
||||
|
||||
if (!in_array($key, $keys)) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($keys as $number => $key) {
|
||||
$gr = $groups;
|
||||
$gr[] = $key;
|
||||
|
||||
$gridData[$key] = $this->buildInternal(
|
||||
$data,
|
||||
$rows,
|
||||
$groupList,
|
||||
$columns,
|
||||
$sums,
|
||||
$cellValueMaps,
|
||||
$gr,
|
||||
$number + 1
|
||||
);
|
||||
}
|
||||
|
||||
return $gridData;
|
||||
}
|
||||
|
||||
$s = &$sums;
|
||||
|
||||
for ($i = 0; $i < count($groups) - 1; $i++) {
|
||||
/** @var array<string, mixed> $s */
|
||||
|
||||
$group = $groups[$i];
|
||||
|
||||
if (!array_key_exists($group, $s)) {
|
||||
$s[$group] = [];
|
||||
}
|
||||
|
||||
$s = &$s[$group];
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
foreach ($groups as $i => $g) {
|
||||
$groupAlias = $this->util->sanitizeSelectAlias($groupList[$i]);
|
||||
|
||||
if ($row[$groupAlias] != $g) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$selectAlias = $this->util->sanitizeSelectAlias($column);
|
||||
|
||||
if ($this->helper->isColumnNumeric($column, $data)) {
|
||||
if (empty($s[$column])) {
|
||||
$s[$column] = 0;
|
||||
|
||||
if (str_starts_with($column, 'MIN:')) {
|
||||
$s[$column] = null;
|
||||
}
|
||||
else if (str_starts_with($column, 'MAX:')) {
|
||||
$s[$column] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$value = str_starts_with($column, 'COUNT:') ?
|
||||
intval($row[$selectAlias]) :
|
||||
floatval($row[$selectAlias]);
|
||||
|
||||
if (str_starts_with($column, 'MIN:')) {
|
||||
if (is_null($s[$column]) || $s[$column] >= $value) {
|
||||
$s[$column] = $value;
|
||||
}
|
||||
}
|
||||
else if (str_starts_with($column, 'MAX:')) {
|
||||
if (is_null($s[$column]) || $s[$column] < $value) {
|
||||
$s[$column] = $value;
|
||||
}
|
||||
}
|
||||
else if (str_starts_with($column, 'AVG:')) {
|
||||
$s[$column] = $s[$column] + ($value - $s[$column]) / floatval($number);
|
||||
}
|
||||
else {
|
||||
$s[$column] = $s[$column] + $value;
|
||||
}
|
||||
|
||||
if (is_float($s[$column])) {
|
||||
$s[$column] = round($s[$column], self::ROUND_PRECISION);
|
||||
}
|
||||
|
||||
$gridData[$column] = $value;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$columnData = $this->helper->getDataFromColumnName($entityType, $column);
|
||||
|
||||
if (!property_exists($cellValueMaps, $column)) {
|
||||
$cellValueMaps->$column = (object) [];
|
||||
}
|
||||
|
||||
$fieldType = $columnData->fieldType;
|
||||
|
||||
$value = null;
|
||||
|
||||
if (array_key_exists($selectAlias, $row)) {
|
||||
$value = $row[$selectAlias];
|
||||
}
|
||||
|
||||
if ($fieldType === 'link') {
|
||||
$selectAlias = $this->util->sanitizeSelectAlias($column . 'Id');
|
||||
|
||||
$value = $row[$selectAlias];
|
||||
}
|
||||
|
||||
$gridData[$column] = $value;
|
||||
|
||||
if (!is_null($value) && !property_exists($cellValueMaps->$column, $value)) {
|
||||
$displayValue = $this->util->getCellDisplayValue($value, $columnData);
|
||||
|
||||
if (!is_null($displayValue)) {
|
||||
$cellValueMaps->$column->$value = $displayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $gridData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $columnList
|
||||
* @param string[] $summaryColumnList
|
||||
* @param array<string, array<string, mixed>> $rows
|
||||
* @param string[] $groupList
|
||||
*/
|
||||
public function buildNonSummary(
|
||||
array $columnList,
|
||||
array $summaryColumnList,
|
||||
GridData $data,
|
||||
array $rows,
|
||||
array $groupList,
|
||||
stdClass $cellValueMaps,
|
||||
stdClass $nonSummaryColumnGroupMap
|
||||
): ?stdClass {
|
||||
|
||||
if (count($data->getGroupBy()) !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (count($columnList) <= count($summaryColumnList)) {
|
||||
return (object) [];
|
||||
}
|
||||
|
||||
$nonSummaryData = (object) [];
|
||||
|
||||
foreach ($data->getGroupBy() as $i => $groupColumn) {
|
||||
$nonSummaryData->$groupColumn = (object) [];
|
||||
|
||||
$groupAlias = $this->util->sanitizeSelectAlias($groupList[$i]);
|
||||
|
||||
foreach ($columnList as $column) {
|
||||
if (in_array($column, $summaryColumnList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!str_starts_with($column, $groupColumn . '.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nonSummaryColumnGroupMap->$column = $groupColumn;
|
||||
|
||||
$columnData = $this->helper->getDataFromColumnName($data->getEntityType(), $column);
|
||||
|
||||
$columnKey = $column;
|
||||
|
||||
if ($columnData->fieldType === 'link') {
|
||||
$columnKey .= 'Id';
|
||||
}
|
||||
|
||||
$columnAlias = $this->util->sanitizeSelectAlias($columnKey);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$groupValue = $row[$groupAlias];
|
||||
|
||||
if (!property_exists($nonSummaryData->$groupColumn, $groupValue)) {
|
||||
$nonSummaryData->$groupColumn->$groupValue = (object) [];
|
||||
}
|
||||
|
||||
$value = $row[$columnAlias] ?? null;
|
||||
|
||||
if (is_null($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nonSummaryData->$groupColumn->$groupValue->$column = $value;
|
||||
|
||||
if (!property_exists($cellValueMaps, $column)) {
|
||||
$cellValueMaps->$column = (object) [];
|
||||
}
|
||||
|
||||
if (!property_exists($cellValueMaps->$column, $value)) {
|
||||
$cellValueMaps->$column->$value = $this->util->getCellDisplayValue($value, $columnData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $nonSummaryData;
|
||||
}
|
||||
}
|
||||
367
custom/Espo/Modules/Advanced/Tools/Report/GridType/Helper.php
Normal file
367
custom/Espo/Modules/Advanced/Tools/Report/GridType/Helper.php
Normal file
@@ -0,0 +1,367 @@
|
||||
<?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\GridType;
|
||||
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Result as GridResult;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\QueryComposer\Util as QueryComposerUtil;
|
||||
|
||||
class Helper
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $numericFieldTypeList = [
|
||||
'currency',
|
||||
'currencyConverted',
|
||||
'int',
|
||||
'float',
|
||||
'enumInt',
|
||||
'enumFloat',
|
||||
'duration',
|
||||
'decimal',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private AclManager $aclManager,
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function getDataFromColumnName(string $entityType, string $column, ?GridResult $result = null): ColumnData
|
||||
{
|
||||
if ($result && $result->isJoint()) {
|
||||
$entityType = $result->getColumnEntityTypeMap()[$column];
|
||||
$column = $result->getColumnOriginalMap()[$column];
|
||||
}
|
||||
|
||||
$field = $column;
|
||||
$link = null;
|
||||
$function = null;
|
||||
|
||||
if (str_contains($field, ':')) {
|
||||
[$function, $field] = explode(':', $field, 2);
|
||||
}
|
||||
|
||||
if (str_contains($field, ':') || str_contains($field, '(') || substr_count($field, '.') > 2) {
|
||||
if (substr_count($field, '.') === 1 && !str_contains($field, ',')) {
|
||||
$attrs = QueryComposerUtil::getAllAttributesFromComplexExpression($column);
|
||||
|
||||
if (count($attrs) === 1) {
|
||||
$attr = $attrs[0];
|
||||
[$link, $field] = explode('.', $attr);
|
||||
|
||||
return new ColumnData(
|
||||
function: $function,
|
||||
field: $field,
|
||||
entityType: null,
|
||||
link: $link,
|
||||
fieldType: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new ColumnData(
|
||||
function: $function,
|
||||
field: '',
|
||||
entityType: null,
|
||||
link: null,
|
||||
fieldType: null,
|
||||
);
|
||||
}
|
||||
|
||||
$fieldEntityType = $entityType;
|
||||
|
||||
if (str_contains($field, '.')) {
|
||||
[$link, $field] = explode('.', $field, 2);
|
||||
|
||||
$fieldEntityType = $this->metadata->get(['entityDefs', $entityType, 'links', $link, 'entity']);
|
||||
}
|
||||
|
||||
$fieldType = $this->metadata->get(['entityDefs', $fieldEntityType, 'fields', $field, 'type']);
|
||||
|
||||
return new ColumnData(
|
||||
function: $function,
|
||||
field: $field,
|
||||
entityType: $fieldEntityType,
|
||||
link: $link,
|
||||
fieldType: $fieldType,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Data|Result $data
|
||||
*/
|
||||
public function isColumnNumeric(string $item, $data): bool
|
||||
{
|
||||
if ($data instanceof Result) {
|
||||
if (in_array($item, $data->getNumericColumnList())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if ($data instanceof Data) {
|
||||
$type = $data->getColumnType($item);
|
||||
|
||||
if ($type !== null) {
|
||||
return $type == Data::COLUMN_TYPE_SUMMARY;
|
||||
}
|
||||
}
|
||||
|
||||
$columnData = $this->getDataFromColumnName($data->getEntityType(), $item);
|
||||
|
||||
if (in_array($columnData->function, ['COUNT', 'SUM', 'AVG'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (in_array($columnData->fieldType, $this->numericFieldTypeList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isColumnEligibleForSubList(string $item, Data $data): bool
|
||||
{
|
||||
$groupBy = $data->getGroupBy()[0] ?? null;
|
||||
|
||||
$columnData = $this->getDataFromColumnName($data->getEntityType(), $item);
|
||||
|
||||
if (!str_contains($item, '.')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$groupBy) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($columnData->link === $groupBy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isColumnSummary(string $item, Data $data): bool
|
||||
{
|
||||
$type = $data->getColumnType($item);
|
||||
|
||||
if ($type !== null) {
|
||||
return $type === Data::COLUMN_TYPE_SUMMARY;
|
||||
}
|
||||
|
||||
$function = null;
|
||||
|
||||
if (strpos($item, ':') > 0) {
|
||||
[$function] = explode(':', $item);
|
||||
}
|
||||
|
||||
if (in_array($function, ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isColumnDateFunction(string $column): bool
|
||||
{
|
||||
$list = [
|
||||
'MONTH:',
|
||||
'YEAR:',
|
||||
'DAY:',
|
||||
'MONTH:',
|
||||
'YEAR:',
|
||||
'DAY:',
|
||||
'QUARTER:',
|
||||
'QUARTER_',
|
||||
'WEEK_0:',
|
||||
'WEEK_1:',
|
||||
'YEAR_',
|
||||
'QUARTER_FISCAL:',
|
||||
'YEAR_FISCAL:',
|
||||
];
|
||||
|
||||
foreach ($list as $item) {
|
||||
if (str_starts_with($column, $item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isColumnSubListAggregated(string $item): bool
|
||||
{
|
||||
if (!str_contains($item, ':')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_contains($item, ',')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_contains($item, '.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_contains($item, '(')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$function = explode(':', $item)[0];
|
||||
|
||||
if ($function === 'COUNT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($function, ['SUM', 'MAX', 'MIN', 'AVG'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $itemList
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function checkColumnsAvailability(string $entityType, array $itemList): void
|
||||
{
|
||||
foreach ($itemList as $item) {
|
||||
$this->checkColumnAvailability($entityType, $item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkColumnAvailability(string $entityType, string $item): void
|
||||
{
|
||||
if (str_contains($item, ':')) {
|
||||
$argumentList = QueryComposerUtil::getAllAttributesFromComplexExpression($item);
|
||||
|
||||
foreach ($argumentList as $argument) {
|
||||
$this->checkColumnAvailability($entityType, $argument);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$field = $item;
|
||||
|
||||
if (str_contains($field, '.')) {
|
||||
[$link, $field] = explode('.', $field);
|
||||
|
||||
$entityType = $this->metadata->get(['entityDefs', $entityType, 'links', $link, 'entity']);
|
||||
|
||||
if (!$entityType) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
in_array($field, $this->aclManager->getScopeRestrictedFieldList($entityType, 'onlyAdmin')) ||
|
||||
in_array($field, $this->aclManager->getScopeRestrictedFieldList($entityType, 'internal')) ||
|
||||
in_array($field, $this->aclManager->getScopeRestrictedFieldList($entityType, 'forbidden'))
|
||||
) {
|
||||
throw new Forbidden;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Check whether it's working.
|
||||
* @return string[]
|
||||
*/
|
||||
public function obtainLinkColumnList(Data $data): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($data->getGroupBy() as $item) {
|
||||
$columnData = $this->getDataFromColumnName($data->getEntityType(), $item);
|
||||
|
||||
if ($columnData->function) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$columnData->link) {
|
||||
if (in_array($columnData->fieldType, ['link', 'file', 'image'])) {
|
||||
$list[] = $item;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($data->getEntityType());
|
||||
|
||||
if (!$entityDefs->hasRelation($columnData->link)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relationType = $entityDefs
|
||||
->getRelation($columnData->link)
|
||||
->getType();
|
||||
|
||||
if (
|
||||
(
|
||||
$relationType === Entity::BELONGS_TO ||
|
||||
$relationType === Entity::HAS_ONE
|
||||
) &&
|
||||
in_array($columnData->fieldType, ['link', 'file', 'image'])
|
||||
) {
|
||||
$list[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $columns
|
||||
* @return string[]
|
||||
*/
|
||||
public function obtainLinkColumnListFromColumns(Data $data, array $columns): array
|
||||
{
|
||||
$typeList = [
|
||||
'link',
|
||||
'file',
|
||||
'image',
|
||||
'linkOne',
|
||||
'linkParent',
|
||||
];
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($columns as $item) {
|
||||
$columnData = $this->getDataFromColumnName($data->getEntityType(), $item);
|
||||
|
||||
if ($columnData->function || $columnData->link) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($columnData->fieldType, $typeList)) {
|
||||
$list[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\GridType;
|
||||
|
||||
class JointData
|
||||
{
|
||||
/** @var ?array<int, object{id: string, label: string}> */
|
||||
private ?array $joinedReportDataList;
|
||||
private ?string $chartType;
|
||||
|
||||
/**
|
||||
* @param ?array<int, object{id: string, label: string}> $joinedReportDataList
|
||||
* @param string|null $chartType
|
||||
*/
|
||||
public function __construct(
|
||||
?array $joinedReportDataList,
|
||||
?string $chartType
|
||||
) {
|
||||
$this->joinedReportDataList = $joinedReportDataList;
|
||||
$this->chartType = $chartType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, object{id: string, label: string}>
|
||||
*/
|
||||
public function getJoinedReportDataList(): array
|
||||
{
|
||||
return $this->joinedReportDataList;
|
||||
}
|
||||
|
||||
public function getChartType(): ?string
|
||||
{
|
||||
return $this->chartType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?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\GridType;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Select\SelectBuilderFactory;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Tools\Report\SelectHelper;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\Part\Expression as Expr;
|
||||
use Espo\ORM\Query\Select;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
class QueryPreparator
|
||||
{
|
||||
private const WHERE_TYPE_AND = 'and';
|
||||
|
||||
public function __construct(
|
||||
private SelectHelper $selectHelper,
|
||||
private SelectBuilderFactory $selectBuilderFactory,
|
||||
private Helper $helper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function prepare(Data $data, ?WhereItem $where, ?User $user): Select
|
||||
{
|
||||
[$whereItem, $havingItem] = $this->obtainWhereAndHavingItems($data);
|
||||
|
||||
$queryBuilder = SelectBuilder::create()
|
||||
->from($data->getEntityType(), lcfirst($data->getEntityType()));
|
||||
|
||||
$this->selectHelper->handleGroupBy($data->getGroupBy(), $queryBuilder);
|
||||
$this->selectHelper->handleColumns($data->getAggregatedColumns(), $queryBuilder);
|
||||
$this->selectHelper->handleOrderBy($data->getOrderBy(), $queryBuilder);
|
||||
$this->selectHelper->handleFiltersHaving($havingItem, $queryBuilder, true);
|
||||
|
||||
$preFilterQuery = $queryBuilder->build();
|
||||
|
||||
$queryBuilder = $this->cloneWithAccessControlAndWhere($data, $where, $user, $preFilterQuery);
|
||||
|
||||
$this->selectHelper->handleFiltersWhere($whereItem, $queryBuilder/*, true*/);
|
||||
$this->handleAdditional($queryBuilder);
|
||||
|
||||
if (!$this->useSubQuery($queryBuilder)) {
|
||||
return $queryBuilder->build();
|
||||
}
|
||||
|
||||
// @todo Remove when v8.5 is min. supported.
|
||||
$subQuery = $queryBuilder
|
||||
->select(['id'])
|
||||
->group([])
|
||||
->order([])
|
||||
->having([])
|
||||
->build();
|
||||
|
||||
return SelectBuilder::create()
|
||||
->clone($preFilterQuery)
|
||||
->where(
|
||||
Cond::in(Expr::column('id'), $subQuery)
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function cloneWithAccessControlAndWhere(
|
||||
Data $data,
|
||||
?WhereItem $where,
|
||||
?User $user,
|
||||
Select $preFilterQuery
|
||||
): SelectBuilder {
|
||||
|
||||
$selectBuilder = $this->selectBuilderFactory
|
||||
->create()
|
||||
->clone($preFilterQuery);
|
||||
|
||||
if ($user) {
|
||||
$selectBuilder
|
||||
->forUser($user)
|
||||
->withWherePermissionCheck();
|
||||
}
|
||||
|
||||
if ($user && $data->applyAcl()) {
|
||||
$selectBuilder->withAccessControlFilter();
|
||||
}
|
||||
|
||||
// @todo Revise.
|
||||
$selectBuilder->buildQueryBuilder();
|
||||
|
||||
if ($where) {
|
||||
$selectBuilder->withWhere($where);
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnnecessaryLocalVariableInspection */
|
||||
$queryBuilder = $selectBuilder->buildQueryBuilder();
|
||||
|
||||
/*if ($where) {
|
||||
// Supposed to be already applied by the scanner.
|
||||
$this->selectHelper->applyLeftJoinsFromWhere($where, $queryBuilder);
|
||||
}*/
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Data $data
|
||||
* @return array{0: WhereItem, 1: WhereItem}
|
||||
*/
|
||||
private function obtainWhereAndHavingItems(Data $data): array
|
||||
{
|
||||
return $data->getFiltersWhere() ?
|
||||
$this->selectHelper->splitHavingItem($data->getFiltersWhere()) :
|
||||
[
|
||||
WhereItem::createBuilder()
|
||||
->setType(self::WHERE_TYPE_AND)
|
||||
->setItemList([])
|
||||
->build(),
|
||||
WhereItem::createBuilder()
|
||||
->setType(self::WHERE_TYPE_AND)
|
||||
->setItemList([])
|
||||
->build()
|
||||
];
|
||||
}
|
||||
|
||||
private function useSubQuery(SelectBuilder $queryBuilder): bool
|
||||
{
|
||||
$isDistinct = $queryBuilder->build()->isDistinct();
|
||||
|
||||
if (!$isDistinct) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($queryBuilder->build()->getSelect() as $selectItem) {
|
||||
$itemExpr = $selectItem->getExpression()->getValue();
|
||||
|
||||
if (
|
||||
str_starts_with($itemExpr, 'SUM:') ||
|
||||
str_starts_with($itemExpr, 'AVG:')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function handleAdditional(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
foreach ($queryBuilder->build()->getGroup() as $groupBy) {
|
||||
$groupColumn = $groupBy->getValue();
|
||||
|
||||
if ($this->helper->isColumnDateFunction($groupColumn)) {
|
||||
$queryBuilder->where(["$groupColumn!=" => null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
689
custom/Espo/Modules/Advanced/Tools/Report/GridType/Result.php
Normal file
689
custom/Espo/Modules/Advanced/Tools/Report/GridType/Result.php
Normal file
@@ -0,0 +1,689 @@
|
||||
<?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\GridType;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Result
|
||||
{
|
||||
private bool $isJoint = false;
|
||||
/** @var string[] */
|
||||
private array $subListColumnList;
|
||||
private stdClass $nonSummaryColumnGroupMap;
|
||||
private stdClass $subListData;
|
||||
private stdClass $sums;
|
||||
/** @var array<string, array<string, mixed>> */
|
||||
private array $groupValueMap;
|
||||
private stdClass $cellValueMaps;
|
||||
private stdClass $reportData;
|
||||
private stdClass $nonSummaryData;
|
||||
/** @var ?string */
|
||||
private ?string $success = null;
|
||||
private stdClass $chartColors;
|
||||
/** @var ?string */
|
||||
private ?string $chartColor = null;
|
||||
private stdClass $columnDecimalPlacesMap;
|
||||
/** @var ?string[] */
|
||||
private ?array $group1NonSummaryColumnList = null;
|
||||
/** @var ?string[] */
|
||||
private ?array $group2NonSummaryColumnList = null;
|
||||
private ?stdClass $group1Sums = null;
|
||||
private ?stdClass $group2Sums = null;
|
||||
|
||||
/** @var array<string, string> */
|
||||
private array $columnEntityTypeMap = [];
|
||||
/** @var array<string, string> */
|
||||
private array $columnOriginalMap = [];
|
||||
|
||||
/** @var ?string[] */
|
||||
private ?array $entityTypeList = null;
|
||||
/** @var array<string, string> */
|
||||
private array $columnReportIdMap = [];
|
||||
/** @var array<string, string> */
|
||||
private array $columnSubReportLabelMap = [];
|
||||
|
||||
/**
|
||||
* @param ?string $entityType
|
||||
* @param string[] $groupByList
|
||||
* @param string[] $columnList
|
||||
* @param string[] $numericColumnList
|
||||
* @param string[] $summaryColumnList
|
||||
* @param string[] $nonSummaryColumnList
|
||||
* @param string[] $subListColumnList
|
||||
* @param string[] $aggregatedColumnList
|
||||
* @param ?stdClass $nonSummaryColumnGroupMap
|
||||
* @param ?stdClass $subListData
|
||||
* @param ?stdClass $sums
|
||||
* @param ?array<string, array<string, mixed>> $groupValueMap
|
||||
* @param ?string[] $columnNameMap
|
||||
* @param ?string[] $columnTypeMap
|
||||
* @param ?stdClass $cellValueMaps
|
||||
* @param array{0?: string[], 1?: string[]} $grouping
|
||||
* @param ?stdClass $reportData
|
||||
* @param ?stdClass $nonSummaryData
|
||||
* @param ?string $chartType
|
||||
* @param ?stdClass[] $chartDataList
|
||||
* @param ?stdClass $columnDecimalPlacesMap
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $entityType,
|
||||
private array $groupByList,
|
||||
private array $columnList,
|
||||
private array $numericColumnList = [],
|
||||
private array $summaryColumnList = [],
|
||||
private array $nonSummaryColumnList = [],
|
||||
?array $subListColumnList = null,
|
||||
private array $aggregatedColumnList = [],
|
||||
?stdClass $nonSummaryColumnGroupMap = null,
|
||||
?stdClass $subListData = null,
|
||||
?stdClass $sums = null,
|
||||
?array $groupValueMap = null,
|
||||
private ?array $columnNameMap = null,
|
||||
private ?array $columnTypeMap = null,
|
||||
?stdClass $cellValueMaps = null,
|
||||
private array $grouping = [],
|
||||
?stdClass $reportData = null,
|
||||
?stdClass $nonSummaryData = null,
|
||||
private ?string $chartType = null,
|
||||
private ?array $chartDataList = null,
|
||||
?stdClass $columnDecimalPlacesMap = null,
|
||||
private bool $emptyStringGroupExcluded = false
|
||||
) {
|
||||
$this->subListColumnList = $subListColumnList ?? [];
|
||||
$this->nonSummaryColumnGroupMap = $nonSummaryColumnGroupMap ?? (object) [];
|
||||
$this->subListData = $subListData ?? (object) [];
|
||||
$this->sums = $sums ?? (object) [];
|
||||
$this->groupValueMap = $groupValueMap ?? [];
|
||||
$this->cellValueMaps = $cellValueMaps ?? (object) [];
|
||||
$this->reportData = $reportData ?? (object) [];
|
||||
$this->nonSummaryData = $nonSummaryData ?? (object) [];
|
||||
$this->columnDecimalPlacesMap = $columnDecimalPlacesMap ?? (object) [];
|
||||
$this->chartColors = (object) [];
|
||||
}
|
||||
|
||||
public function getEntityType(): ?string
|
||||
{
|
||||
return $this->entityType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getGroupByList(): array
|
||||
{
|
||||
return $this->groupByList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getColumnList(): array
|
||||
{
|
||||
return $this->columnList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNumericColumnList(): array
|
||||
{
|
||||
return $this->numericColumnList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSummaryColumnList(): array
|
||||
{
|
||||
return $this->summaryColumnList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNonSummaryColumnList(): array
|
||||
{
|
||||
return $this->nonSummaryColumnList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSubListColumnList(): array
|
||||
{
|
||||
return $this->subListColumnList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAggregatedColumnList(): array
|
||||
{
|
||||
return $this->aggregatedColumnList;
|
||||
}
|
||||
|
||||
public function getNonSummaryColumnGroupMap(): stdClass
|
||||
{
|
||||
return $this->nonSummaryColumnGroupMap;
|
||||
}
|
||||
|
||||
public function getSubListData(): stdClass
|
||||
{
|
||||
return $this->subListData;
|
||||
}
|
||||
|
||||
public function getSums(): stdClass
|
||||
{
|
||||
return $this->sums;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function getGroupValueMap(): array
|
||||
{
|
||||
return $this->groupValueMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
public function getColumnNameMap(): ?array
|
||||
{
|
||||
return $this->columnNameMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]|null
|
||||
*/
|
||||
public function getColumnTypeMap(): ?array
|
||||
{
|
||||
return $this->columnTypeMap;
|
||||
}
|
||||
|
||||
public function getCellValueMaps(): stdClass
|
||||
{
|
||||
return $this->cellValueMaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0?: string[], 1?: string[]}
|
||||
*/
|
||||
public function getGrouping(): array
|
||||
{
|
||||
return $this->grouping;
|
||||
}
|
||||
|
||||
public function getReportData(): stdClass
|
||||
{
|
||||
return $this->reportData;
|
||||
}
|
||||
|
||||
public function getNonSummaryData(): stdClass
|
||||
{
|
||||
return $this->nonSummaryData;
|
||||
}
|
||||
|
||||
public function getChartType(): ?string
|
||||
{
|
||||
return $this->chartType;
|
||||
}
|
||||
|
||||
public function getSuccess(): ?string
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function getChartColors(): stdClass
|
||||
{
|
||||
return $this->chartColors;
|
||||
}
|
||||
|
||||
public function getChartColor(): ?string
|
||||
{
|
||||
return $this->chartColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?stdClass[]
|
||||
*/
|
||||
public function getChartDataList(): ?array
|
||||
{
|
||||
return $this->chartDataList;
|
||||
}
|
||||
|
||||
public function getColumnDecimalPlacesMap(): stdClass
|
||||
{
|
||||
return $this->columnDecimalPlacesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getGroup1NonSummaryColumnList(): ?array
|
||||
{
|
||||
return $this->group1NonSummaryColumnList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getGroup2NonSummaryColumnList(): ?array
|
||||
{
|
||||
return $this->group2NonSummaryColumnList;
|
||||
}
|
||||
|
||||
public function getGroup1Sums(): ?stdClass
|
||||
{
|
||||
return $this->group1Sums;
|
||||
}
|
||||
|
||||
public function getGroup2Sums(): ?stdClass
|
||||
{
|
||||
return $this->group2Sums;
|
||||
}
|
||||
|
||||
public function isJoint(): bool
|
||||
{
|
||||
return $this->isJoint;
|
||||
}
|
||||
|
||||
public function setEntityType(?string $entityType): Result
|
||||
{
|
||||
$this->entityType = $entityType;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $groupByList
|
||||
*/
|
||||
public function setGroupByList(array $groupByList): Result
|
||||
{
|
||||
$this->groupByList = $groupByList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $columnList
|
||||
*/
|
||||
public function setColumnList(array $columnList): Result
|
||||
{
|
||||
$this->columnList = $columnList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $numericColumnList
|
||||
*/
|
||||
public function setNumericColumnList(array $numericColumnList): Result
|
||||
{
|
||||
$this->numericColumnList = $numericColumnList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $summaryColumnList
|
||||
*/
|
||||
public function setSummaryColumnList(array $summaryColumnList): Result
|
||||
{
|
||||
$this->summaryColumnList = $summaryColumnList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $nonSummaryColumnList
|
||||
*/
|
||||
public function setNonSummaryColumnList(array $nonSummaryColumnList): Result
|
||||
{
|
||||
$this->nonSummaryColumnList = $nonSummaryColumnList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $subListColumnList
|
||||
*/
|
||||
public function setSubListColumnList(array $subListColumnList): Result
|
||||
{
|
||||
$this->subListColumnList = $subListColumnList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $aggregatedColumnList
|
||||
*/
|
||||
public function setAggregatedColumnList(array $aggregatedColumnList): Result
|
||||
{
|
||||
$this->aggregatedColumnList = $aggregatedColumnList;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setNonSummaryColumnGroupMap(stdClass $nonSummaryColumnGroupMap): Result
|
||||
{
|
||||
$this->nonSummaryColumnGroupMap = $nonSummaryColumnGroupMap;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSubListData(stdClass $subListData): Result
|
||||
{
|
||||
$this->subListData = $subListData;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSums(stdClass $sums): Result
|
||||
{
|
||||
$this->sums = $sums;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?array<string, array<string, mixed>> $groupValueMap
|
||||
* @return Result
|
||||
*/
|
||||
public function setGroupValueMap(?array $groupValueMap): Result
|
||||
{
|
||||
$this->groupValueMap = $groupValueMap;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $columnNameMap
|
||||
*/
|
||||
public function setColumnNameMap(?array $columnNameMap): Result
|
||||
{
|
||||
$this->columnNameMap = $columnNameMap;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $columnTypeMap
|
||||
*/
|
||||
public function setColumnTypeMap(?array $columnTypeMap): Result
|
||||
{
|
||||
$this->columnTypeMap = $columnTypeMap;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCellValueMaps(?stdClass $cellValueMaps): Result
|
||||
{
|
||||
$this->cellValueMaps = $cellValueMaps;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{0?: string[], 1?: string[]} $grouping
|
||||
*/
|
||||
public function setGrouping(array $grouping): Result
|
||||
{
|
||||
$this->grouping = $grouping;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setReportData(stdClass $reportData): Result
|
||||
{
|
||||
$this->reportData = $reportData;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setNonSummaryData(stdClass $nonSummaryData): Result
|
||||
{
|
||||
$this->nonSummaryData = $nonSummaryData;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setChartType(?string $chartType): Result
|
||||
{
|
||||
$this->chartType = $chartType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSuccess(?string $success): Result
|
||||
{
|
||||
$this->success = $success;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setChartColors(?stdClass $chartColors): Result
|
||||
{
|
||||
$this->chartColors = $chartColors ?? (object) [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setChartColor(?string $chartColor): Result
|
||||
{
|
||||
$this->chartColor = $chartColor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?stdClass[] $chartDataList
|
||||
*/
|
||||
public function setChartDataList(?array $chartDataList): Result
|
||||
{
|
||||
$this->chartDataList = $chartDataList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setColumnDecimalPlacesMap(stdClass $columnDecimalPlacesMap): Result
|
||||
{
|
||||
$this->columnDecimalPlacesMap = $columnDecimalPlacesMap;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $group1NonSummaryColumnList
|
||||
* @return Result
|
||||
*/
|
||||
public function setGroup1NonSummaryColumnList(?array $group1NonSummaryColumnList): Result
|
||||
{
|
||||
$this->group1NonSummaryColumnList = $group1NonSummaryColumnList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $group2NonSummaryColumnList
|
||||
*/
|
||||
public function setGroup2NonSummaryColumnList(?array $group2NonSummaryColumnList): Result
|
||||
{
|
||||
$this->group2NonSummaryColumnList = $group2NonSummaryColumnList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setGroup1Sums(?stdClass $group1Sums): Result
|
||||
{
|
||||
$this->group1Sums = $group1Sums;
|
||||
|
||||
foreach (get_object_vars($this->group1Sums) as $k => $v) {
|
||||
if (is_array($v)) {
|
||||
$this->group1Sums->$k = (object) $v;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setGroup2Sums(?stdClass $group2Sums): Result
|
||||
{
|
||||
$this->group2Sums = $group2Sums;
|
||||
|
||||
foreach (get_object_vars($this->group2Sums) as $k => $v) {
|
||||
if (is_array($v)) {
|
||||
$this->group2Sums->$k = (object) $v;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIsJoint(bool $isJoint): void
|
||||
{
|
||||
$this->isJoint = $isJoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getColumnEntityTypeMap(): array
|
||||
{
|
||||
return $this->columnEntityTypeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $columnEntityTypeMap
|
||||
* @return Result
|
||||
*/
|
||||
public function setColumnEntityTypeMap(array $columnEntityTypeMap): Result
|
||||
{
|
||||
$this->columnEntityTypeMap = $columnEntityTypeMap;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getColumnOriginalMap(): array
|
||||
{
|
||||
return $this->columnOriginalMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $columnOriginalMap
|
||||
* @return Result
|
||||
*/
|
||||
public function setColumnOriginalMap(array $columnOriginalMap): Result
|
||||
{
|
||||
$this->columnOriginalMap = $columnOriginalMap;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getEntityTypeList(): ?array
|
||||
{
|
||||
return $this->entityTypeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $entityTypeList
|
||||
* @return Result
|
||||
*/
|
||||
public function setEntityTypeList(?array $entityTypeList): Result
|
||||
{
|
||||
$this->entityTypeList = $entityTypeList;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getColumnReportIdMap(): array
|
||||
{
|
||||
return $this->columnReportIdMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $columnReportIdMap
|
||||
* @return Result
|
||||
*/
|
||||
public function setColumnReportIdMap(array $columnReportIdMap): Result
|
||||
{
|
||||
$this->columnReportIdMap = $columnReportIdMap;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getColumnSubReportLabelMap(): array
|
||||
{
|
||||
return $this->columnSubReportLabelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $columnSubReportLabelMap
|
||||
*/
|
||||
public function setColumnSubReportLabelMap(array $columnSubReportLabelMap): Result
|
||||
{
|
||||
$this->columnSubReportLabelMap = $columnSubReportLabelMap;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
public function isEmptyStringGroupExcluded(): bool
|
||||
{
|
||||
return $this->emptyStringGroupExcluded;
|
||||
}
|
||||
|
||||
public function toRaw(): stdClass
|
||||
{
|
||||
return (object) [
|
||||
'type' => 'Grid',
|
||||
'entityType' => $this->entityType, // string
|
||||
'depth' => count($this->groupByList), // int
|
||||
'columnList' => $this->columnList, // string[]
|
||||
'groupByList' => $this->groupByList, // string[]
|
||||
'numericColumnList' => $this->numericColumnList,
|
||||
'summaryColumnList' => $this->summaryColumnList,
|
||||
'nonSummaryColumnList' => $this->nonSummaryColumnList,
|
||||
'subListColumnList' => $this->subListColumnList, // string[]
|
||||
'aggregatedColumnList' => $this->aggregatedColumnList, // string[]
|
||||
'nonSummaryColumnGroupMap' => $this->nonSummaryColumnGroupMap, // stdClass
|
||||
'subListData' => $this->subListData, // object<stdClass[]>
|
||||
'sums' => $this->sums, // object<int|float>
|
||||
'groupValueMap' => $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)
|
||||
'grouping' => $this->grouping, // array{string[]}|array{string[], string[]}
|
||||
'reportData' => $this->reportData, // object<object>|object<object<object>>
|
||||
// group => (group-value => value-map, only for grid-2
|
||||
'nonSummaryData' => $this->nonSummaryData, // object<object<object>>
|
||||
'success' => $this->success,
|
||||
'chartColors' => $this->chartColors, // stdClass
|
||||
'chartColor' => $this->chartColor, // ?string
|
||||
'chartType' => $this->chartType, // ?string
|
||||
'chartDataList' => $this->chartDataList, // stdClass[]
|
||||
'columnDecimalPlacesMap' => $this->columnDecimalPlacesMap, // object<?int>
|
||||
'group1NonSummaryColumnList' => $this->group1NonSummaryColumnList,
|
||||
'group2NonSummaryColumnList' => $this->group2NonSummaryColumnList,
|
||||
'group1Sums' => $this->group1Sums,
|
||||
'group2Sums' => $this->group2Sums,
|
||||
'isJoint' => $this->isJoint,
|
||||
'entityTypeList' => $this->entityTypeList,
|
||||
'columnEntityTypeMap' => (object) $this->columnEntityTypeMap,
|
||||
'columnOriginalMap' => (object) $this->columnOriginalMap,
|
||||
'columnReportIdMap' => (object) $this->columnReportIdMap,
|
||||
'columnSubReportLabelMap' => (object) $this->columnSubReportLabelMap,
|
||||
'emptyStringGroupExcluded' => $this->emptyStringGroupExcluded,
|
||||
];
|
||||
}
|
||||
}
|
||||
1476
custom/Espo/Modules/Advanced/Tools/Report/GridType/ResultHelper.php
Normal file
1476
custom/Espo/Modules/Advanced/Tools/Report/GridType/ResultHelper.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
<?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\GridType;
|
||||
|
||||
class RunParams
|
||||
{
|
||||
private bool $skipRuntimeFiltersCheck;
|
||||
|
||||
public function __construct(
|
||||
bool $skipRuntimeFiltersCheck = false
|
||||
) {
|
||||
$this->skipRuntimeFiltersCheck = $skipRuntimeFiltersCheck;
|
||||
}
|
||||
|
||||
public function skipRuntimeFiltersCheck(): bool
|
||||
{
|
||||
return $this->skipRuntimeFiltersCheck;
|
||||
}
|
||||
|
||||
public function withSkipRuntimeFiltersCheck(bool $value = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->skipRuntimeFiltersCheck = $value;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
||||
204
custom/Espo/Modules/Advanced/Tools/Report/GridType/Util.php
Normal file
204
custom/Espo/Modules/Advanced/Tools/Report/GridType/Util.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?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\GridType;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Util
|
||||
{
|
||||
private int $aliasMaxLength = 128;
|
||||
|
||||
private Metadata $metadata;
|
||||
private EntityManager $entityManager;
|
||||
private Language $language;
|
||||
private DateTime $dateTime;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
EntityManager $entityManager,
|
||||
Language $language,
|
||||
DateTime $dateTime,
|
||||
Config $config
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->language = $language;
|
||||
$this->dateTime = $dateTime;
|
||||
|
||||
if ($config->get('database.platform') === 'Postgresql') {
|
||||
$this->aliasMaxLength = 63;
|
||||
}
|
||||
}
|
||||
|
||||
public function sanitizeSelectAlias(string $alias): string
|
||||
{
|
||||
$alias = preg_replace('/[^A-Za-z\r\n0-9_:\'" .,\-()]+/', '', $alias) ?? '';
|
||||
|
||||
if (strlen($alias) > $this->aliasMaxLength) {
|
||||
$alias = preg_replace('!\s+!', ' ', $alias);
|
||||
}
|
||||
|
||||
if (strlen($alias) > $this->aliasMaxLength) {
|
||||
$alias = substr($alias, 0, $this->aliasMaxLength);
|
||||
}
|
||||
|
||||
return $alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Use ColumnData object.
|
||||
* @param scalar|string[] $value
|
||||
* @return scalar|string[]
|
||||
*/
|
||||
public function getCellDisplayValue($value, object $columnData)
|
||||
{
|
||||
/** @var ColumnData $columnData */
|
||||
|
||||
$displayValue = $value;
|
||||
|
||||
$fieldType = $columnData->fieldType;
|
||||
|
||||
if ($fieldType === 'link') {
|
||||
if ($value && is_string($value)) {
|
||||
try {
|
||||
/** @var ?string $foreignEntityType */
|
||||
$foreignEntityType = $this->metadata->get(
|
||||
['entityDefs', $columnData->entityType, 'links', $columnData->field, 'entity']);
|
||||
|
||||
if ($foreignEntityType) {
|
||||
$e = $this->entityManager->getEntityById($foreignEntityType, $value);
|
||||
|
||||
if ($e) {
|
||||
$displayValue = $e->get('name');
|
||||
}
|
||||
}
|
||||
} catch (Exception) {}
|
||||
}
|
||||
} else if ($fieldType === 'enum') {
|
||||
$displayValue = is_string($value) ?
|
||||
$this->language->translateOption($value, $columnData->field, $columnData->entityType) :
|
||||
'';
|
||||
|
||||
$translation = $this->metadata
|
||||
->get(['entityDefs', $columnData->entityType, 'fields', $columnData->field, 'translation']);
|
||||
|
||||
$optionsReference = $this->metadata
|
||||
->get(['entityDefs', $columnData->entityType, 'fields', $columnData->field, 'optionsReference']);
|
||||
|
||||
if (!$translation && $optionsReference) {
|
||||
$translation = str_replace('.', '.options.', $optionsReference);
|
||||
}
|
||||
|
||||
if ($translation && (is_string($value) || is_int($value))) {
|
||||
$translationMap = $this->language->get(explode('.', $translation));
|
||||
|
||||
if (is_array($translationMap) && array_key_exists($value, $translationMap)) {
|
||||
$displayValue = $translationMap[$value];
|
||||
}
|
||||
}
|
||||
} else if ($fieldType === 'datetime' || $fieldType === 'datetimeOptional') {
|
||||
if ($value && is_string($value)) {
|
||||
$displayValue = $this->dateTime->convertSystemDateTime($value);
|
||||
}
|
||||
} else if ($fieldType === 'date') {
|
||||
if ($value && is_string($value)) {
|
||||
$displayValue = $this->dateTime->convertSystemDate($value);
|
||||
}
|
||||
} else if ($fieldType === 'multiEnum' || $fieldType === 'checklist' || $fieldType === 'array') {
|
||||
if (is_array($value)) {
|
||||
$displayValue = array_map(
|
||||
function ($item) use ($columnData) {
|
||||
return $this->language->translateOption(
|
||||
$item,
|
||||
$columnData->field,
|
||||
$columnData->entityType
|
||||
);
|
||||
},
|
||||
$value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($displayValue)) {
|
||||
$displayValue = $value;
|
||||
}
|
||||
|
||||
return $displayValue;
|
||||
}
|
||||
|
||||
public function translateGroupName(string $entityType, string $item): string
|
||||
{
|
||||
if (str_contains($item, ':(')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->translateColumnName($entityType, $item);
|
||||
}
|
||||
|
||||
public function translateColumnName(string $entityType, string $item): string
|
||||
{
|
||||
if (str_contains($item, ':(')) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
$field = $item;
|
||||
$function = null;
|
||||
|
||||
if (str_contains($item, ':')) {
|
||||
[$function, $field] = explode(':', $item);
|
||||
}
|
||||
|
||||
$groupLabel = '';
|
||||
$entityTypeLocal = $entityType;
|
||||
|
||||
if (str_contains($field, '.')) {
|
||||
[$link, $field] = explode('.', $field);
|
||||
|
||||
$entityTypeLocal = $this->metadata->get(['entityDefs', $entityType, 'links', $link, 'entity']);
|
||||
//$groupLabel .= $this->language->translate($link, 'links', $entityType);
|
||||
//$groupLabel .= '.';
|
||||
}
|
||||
|
||||
if ($this->metadata->get(['entityDefs', $entityTypeLocal, 'fields', $field, 'type']) == 'currencyConverted') {
|
||||
$field = str_replace('Converted', '', $field);
|
||||
}
|
||||
|
||||
$groupLabel .= $this->language->translateLabel($field, 'fields', $entityTypeLocal);
|
||||
|
||||
if ($function) {
|
||||
$functionLabel = $this->language->translateLabel($function, 'functions', 'Report');
|
||||
|
||||
if ($function === 'COUNT' && $field === 'id') {
|
||||
return $functionLabel;
|
||||
}
|
||||
|
||||
if ($function !== 'SUM') {
|
||||
$groupLabel = $functionLabel . ': ' . $groupLabel;
|
||||
}
|
||||
}
|
||||
|
||||
return $groupLabel;
|
||||
}
|
||||
}
|
||||
159
custom/Espo/Modules/Advanced/Tools/Report/Jobs/Send.php
Normal file
159
custom/Espo/Modules/Advanced/Tools/Report/Jobs/Send.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?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\Jobs;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
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\ListType\Result as ListResult;
|
||||
use Espo\Modules\Advanced\Tools\Report\SendingService;
|
||||
use Espo\Modules\Advanced\Tools\Report\Service;
|
||||
use Espo\ORM\EntityManager;
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
|
||||
class Send implements Job
|
||||
{
|
||||
private const LIST_REPORT_MAX_SIZE = 3000;
|
||||
|
||||
private Config $config;
|
||||
private Service $service;
|
||||
private EntityManager $entityManager;
|
||||
private SendingService $sendingService;
|
||||
private EmailBuilder $emailBuilder;
|
||||
private Log $log;
|
||||
|
||||
public function __construct(
|
||||
Config $config,
|
||||
Service $service,
|
||||
EntityManager $entityManager,
|
||||
SendingService $sendingService,
|
||||
EmailBuilder $emailBuilder,
|
||||
Log $log
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->service = $service;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->sendingService = $sendingService;
|
||||
$this->emailBuilder = $emailBuilder;
|
||||
$this->log = $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$data = $data->getRaw();
|
||||
|
||||
$reportId = $data->reportId;
|
||||
$userId = $data->userId;
|
||||
|
||||
/** @var ?ReportEntity $report */
|
||||
$report = $this->entityManager->getEntityById(ReportEntity::ENTITY_TYPE, $reportId);
|
||||
|
||||
if (!$report) {
|
||||
throw new RuntimeException("Report Sending: No report $reportId.");
|
||||
}
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new RuntimeException("Report Sending: No user $userId.");
|
||||
}
|
||||
|
||||
if ($report->getType() === ReportEntity::TYPE_LIST) {
|
||||
$searchParams = SearchParams::create()
|
||||
->withMaxSize($this->getSendingListMaxCount());
|
||||
|
||||
$orderByList = $report->getOrderByList();
|
||||
|
||||
if ($orderByList) {
|
||||
$arr = explode(':', $orderByList);
|
||||
|
||||
/**
|
||||
* @var 'ASC'|'DESC' $orderDirection
|
||||
* @noinspection PhpRedundantVariableDocTypeInspection
|
||||
*/
|
||||
$orderDirection = strtoupper($arr[0]);
|
||||
|
||||
$searchParams = $searchParams
|
||||
->withOrderBy($arr[1])
|
||||
->withOrder($orderDirection);
|
||||
}
|
||||
|
||||
$result = $this->service->runList($reportId, $searchParams, $user);
|
||||
}
|
||||
else {
|
||||
$result = $this->service->runGrid($reportId, null, $user);
|
||||
}
|
||||
|
||||
$reportResult = $result;
|
||||
|
||||
if ($result instanceof ListResult) {
|
||||
$reportResult = [];
|
||||
|
||||
foreach ($result->getCollection() as $e) {
|
||||
$reportResult[] = get_object_vars($e->getValueMap());
|
||||
}
|
||||
|
||||
if (
|
||||
count($reportResult) === 0 &&
|
||||
$report->get('emailSendingDoNotSendEmptyReport')
|
||||
) {
|
||||
$this->log->debug('Report Sending: Report ' . $report->get('name') . ' is empty and was not sent.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($reportResult instanceof ListResult) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$attachmentId = $this->sendingService->getExportAttachmentId($report, $result, null, $user);
|
||||
|
||||
$this->emailBuilder->buildEmailData($data, $reportResult, $report);
|
||||
|
||||
$this->emailBuilder->sendEmail(
|
||||
$data->userId,
|
||||
$data->emailSubject,
|
||||
$data->emailBody,
|
||||
$attachmentId
|
||||
);
|
||||
}
|
||||
|
||||
private function getSendingListMaxCount(): int
|
||||
{
|
||||
return $this->config->get('reportSendingListMaxCount', self::LIST_REPORT_MAX_SIZE);
|
||||
}
|
||||
}
|
||||
569
custom/Espo/Modules/Advanced/Tools/Report/JointGridExecutor.php
Normal file
569
custom/Espo/Modules/Advanced/Tools/Report/JointGridExecutor.php
Normal file
@@ -0,0 +1,569 @@
|
||||
<?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\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Entities\Report;
|
||||
use Espo\Modules\Advanced\Reports\GridReport;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Helper as GridHelper;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\JointData;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Result as GridResult;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\ResultHelper;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Util as GridUtil;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class JointGridExecutor
|
||||
{
|
||||
private const STUB_KEY = '__STUB__';
|
||||
|
||||
private EntityManager $entityManager;
|
||||
private ReportHelper $reportHelper;
|
||||
private GridHelper $gridHelper;
|
||||
private ResultHelper $resultHelper;
|
||||
private Language $language;
|
||||
private GridUtil $gridUtil;
|
||||
private Service $service;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
ReportHelper $reportHelper,
|
||||
GridHelper $gridHelper,
|
||||
ResultHelper $resultHelper,
|
||||
Language $language,
|
||||
GridUtil $gridUtil,
|
||||
Service $service
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->reportHelper = $reportHelper;
|
||||
$this->gridHelper = $gridHelper;
|
||||
$this->resultHelper = $resultHelper;
|
||||
$this->language = $language;
|
||||
$this->gridUtil = $gridUtil;
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?array<string, ?WhereItem> $idWhereMap
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function execute(
|
||||
JointData $data,
|
||||
?User $user = null,
|
||||
?array $idWhereMap = null
|
||||
): GridResult {
|
||||
|
||||
if ($data->getJoinedReportDataList() === []) {
|
||||
throw new Error("Bad report.");
|
||||
}
|
||||
|
||||
$result = null;
|
||||
$groupColumn = null;
|
||||
$reportList = [];
|
||||
$groupCount = null;
|
||||
|
||||
foreach ($data->getJoinedReportDataList() as $item) {
|
||||
if (empty($item->id)) {
|
||||
throw new Error("Bad report.");
|
||||
}
|
||||
|
||||
/** @var ?Report $report */
|
||||
$report = $this->entityManager->getEntity(Report::ENTITY_TYPE, $item->id);
|
||||
|
||||
if (!$report) {
|
||||
throw new Error("Sub-report $item->id doesn't exist.");
|
||||
}
|
||||
|
||||
$reportList[] = $report;
|
||||
}
|
||||
|
||||
foreach ($data->getJoinedReportDataList() as $i => $item) {
|
||||
$report = $reportList[$i];
|
||||
|
||||
$where = null;
|
||||
|
||||
if ($idWhereMap && isset($idWhereMap[$item->id])) {
|
||||
$where = $idWhereMap[$item->id];
|
||||
}
|
||||
|
||||
if ($report->isInternal()) {
|
||||
$reportObj = $this->reportHelper->createInternalReport($report);
|
||||
|
||||
if (!$reportObj instanceof GridReport) {
|
||||
throw new Error("Bad report class.");
|
||||
}
|
||||
|
||||
$subReportResult = $reportObj->run($where, $user);
|
||||
}
|
||||
else {
|
||||
if ($report->getType() !== Report::TYPE_GRID) {
|
||||
throw new Error("Bad sub-report.");
|
||||
}
|
||||
|
||||
$this->reportHelper->checkReportCanBeRun($report);
|
||||
|
||||
$subReportResult = $this->service->executeGridReport(
|
||||
$this->reportHelper->fetchGridDataFromReport($report),
|
||||
$where,
|
||||
$user
|
||||
);
|
||||
}
|
||||
|
||||
$subReportNumericColumnList = $subReportResult->getNumericColumnList();
|
||||
$subReportAggregatedColumnList = $subReportResult->getAggregatedColumnList();
|
||||
|
||||
$subReportResult->setColumnOriginalMap([]);
|
||||
$subReportResult->setNumericColumnList([]);
|
||||
$subReportResult->setAggregatedColumnList([]);
|
||||
|
||||
$columnToUnsetList = [];
|
||||
$columnOriginalMap = $subReportResult->getColumnOriginalMap();
|
||||
$subReportColumnList = $subReportResult->getColumnList();
|
||||
|
||||
foreach ($subReportColumnList as &$columnPointer) {
|
||||
$originalColumnName = $columnPointer;
|
||||
|
||||
$newColumnName = $columnPointer . '@'. $i;
|
||||
|
||||
$columnOriginalMap[$newColumnName] = $columnPointer;
|
||||
|
||||
if (in_array($originalColumnName, $subReportNumericColumnList)) {
|
||||
$subReportResult->setNumericColumnList(
|
||||
array_merge(
|
||||
$subReportResult->getNumericColumnList(),
|
||||
[$newColumnName]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
$subReportAggregatedColumnList &&
|
||||
in_array($originalColumnName, $subReportAggregatedColumnList)
|
||||
) {
|
||||
$subReportResult->setAggregatedColumnList(
|
||||
array_merge(
|
||||
$subReportResult->getAggregatedColumnList(),
|
||||
[$newColumnName]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!in_array($columnPointer, $subReportAggregatedColumnList)) {
|
||||
$columnToUnsetList[] = $newColumnName;
|
||||
}
|
||||
|
||||
$columnPointer = $newColumnName;
|
||||
}
|
||||
|
||||
$subReportColumnList = array_values(array_filter(
|
||||
$subReportColumnList,
|
||||
function (string $item) use ($columnToUnsetList) {
|
||||
return !in_array($item, $columnToUnsetList);
|
||||
}
|
||||
));
|
||||
|
||||
$subReportResult->setColumnList($subReportColumnList);
|
||||
$subReportResult->setColumnOriginalMap($columnOriginalMap);
|
||||
|
||||
$sums = [];
|
||||
|
||||
foreach (get_object_vars($subReportResult->getSums()) as $key => $sum) {
|
||||
$sums[$key . '@'. $i] = $sum;
|
||||
}
|
||||
|
||||
$subReportResult->setSums((object) $sums);
|
||||
|
||||
$columnNameMap = [];
|
||||
|
||||
foreach ($subReportResult->getColumnNameMap() as $key => $name) {
|
||||
if (!str_contains($key, '.')) {
|
||||
if (!empty($item->label)) {
|
||||
$name = $item->label . ' · ' . $name;
|
||||
}
|
||||
}
|
||||
|
||||
$columnNameMap[$key . '@'. $i] = $name;
|
||||
}
|
||||
|
||||
$subReportResult->setColumnNameMap($columnNameMap);
|
||||
|
||||
$columnTypeMap = [];
|
||||
|
||||
foreach ($subReportResult->getColumnTypeMap() as $key => $type) {
|
||||
$columnTypeMap[$key . '@'. $i] = $type;
|
||||
}
|
||||
|
||||
$subReportResult->setColumnTypeMap($columnTypeMap);
|
||||
|
||||
$columnDecimalPlacesMap = [];
|
||||
|
||||
foreach (get_object_vars($subReportResult->getColumnDecimalPlacesMap()) as $key => $type) {
|
||||
$columnDecimalPlacesMap[$key . '@'. $i] = $type;
|
||||
}
|
||||
|
||||
$subReportResult->setColumnDecimalPlacesMap((object) $columnDecimalPlacesMap);
|
||||
|
||||
$chartColors = [];
|
||||
|
||||
if ($subReportResult->getChartColor()) {
|
||||
$chartColors[$subReportResult->getColumnList()[0]] = $subReportResult->getChartColor();
|
||||
}
|
||||
|
||||
foreach (get_object_vars($subReportResult->getChartColors()) as $key => $color) {
|
||||
$chartColors[$key . '@'. $i] = $color;
|
||||
}
|
||||
|
||||
$subReportResult->setChartColors((object) $chartColors);
|
||||
|
||||
$cellValueMaps = (object) [];
|
||||
|
||||
foreach (get_object_vars($subReportResult->getCellValueMaps()) as $column => $map) {
|
||||
$cellValueMaps->{$column . '@'. $i} = $map;
|
||||
}
|
||||
|
||||
$subReportResult->setCellValueMaps($cellValueMaps);
|
||||
|
||||
$reportData = $subReportResult->getReportData();
|
||||
|
||||
foreach (get_object_vars($subReportResult->getReportData()) as $key => $dataItem) {
|
||||
$newDataItem = (object) [];
|
||||
|
||||
foreach (get_object_vars($dataItem) as $key1 => $value) {
|
||||
$newDataItem->{$key1 . '@'. $i} = $value;
|
||||
}
|
||||
|
||||
$reportData->$key = $newDataItem;
|
||||
}
|
||||
|
||||
$subReportResult->setReportData($reportData);
|
||||
|
||||
if ($i === 0) {
|
||||
$groupCount = count($report->getGroupBy());
|
||||
|
||||
if ($groupCount) {
|
||||
$groupColumn = $report->getGroupBy()[0];
|
||||
}
|
||||
|
||||
if ($groupCount > 2) {
|
||||
throw new Error("Grouping by 2 columns is not supported in joint reports.");
|
||||
}
|
||||
|
||||
$result = $subReportResult;
|
||||
|
||||
$result->setEntityTypeList([$report->getTargetEntityType()]);
|
||||
$result->setColumnEntityTypeMap([]);
|
||||
$result->setColumnReportIdMap([]);
|
||||
$result->setColumnSubReportLabelMap([]);
|
||||
} else {
|
||||
if ($groupCount === null) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
if (count($report->getGroupBy()) !== $groupCount) {
|
||||
throw new Error("Sub-reports must have the same Group By number.");
|
||||
}
|
||||
|
||||
foreach ($subReportResult->getColumnList() as $column) {
|
||||
$columnList = $result->getColumnList();
|
||||
$columnList[] = $column;
|
||||
$result->setColumnList($columnList);
|
||||
}
|
||||
|
||||
foreach (get_object_vars($subReportResult->getSums()) as $key => $value) {
|
||||
$sums = $result->getSums();
|
||||
$sums->$key = $value;
|
||||
$result->setSums($sums);
|
||||
}
|
||||
|
||||
foreach (($subReportResult->getColumnNameMap() ?? []) as $key => $name) {
|
||||
$map = $result->getColumnNameMap();
|
||||
$map[$key] = $name;
|
||||
$result->setColumnNameMap($map);
|
||||
}
|
||||
|
||||
foreach (($subReportResult->getColumnTypeMap() ?? []) as $key => $type) {
|
||||
$map = $result->getColumnTypeMap();
|
||||
$map[$key] = $type;
|
||||
$result->setColumnTypeMap($map);
|
||||
}
|
||||
|
||||
foreach (get_object_vars($subReportResult->getChartColors()) as $key => $value) {
|
||||
$map = $result->getChartColors();
|
||||
$map->$key = $value;
|
||||
$result->setChartColors($map);
|
||||
}
|
||||
|
||||
foreach ($subReportResult->getColumnOriginalMap() as $key => $value) {
|
||||
$map = $result->getColumnOriginalMap();
|
||||
$map[$key] = $value;
|
||||
$result->setColumnOriginalMap($map);
|
||||
}
|
||||
|
||||
foreach (get_object_vars($subReportResult->getCellValueMaps()) as $column => $value) {
|
||||
$map = $result->getCellValueMaps();
|
||||
$map->$column = $value;
|
||||
$result->setCellValueMaps($map);
|
||||
}
|
||||
|
||||
foreach ($subReportResult->getGroupValueMap() as $group => $v) {
|
||||
$map = $result->getGroupValueMap();
|
||||
|
||||
if (!array_key_exists($group, $map)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$group] = array_replace($map[$group], $v);
|
||||
|
||||
$result->setGroupValueMap($map);
|
||||
}
|
||||
|
||||
foreach ($subReportResult->getNumericColumnList() as $item1) {
|
||||
$list = $result->getNumericColumnList();
|
||||
$list[] = $item1;
|
||||
$result->setNumericColumnList($list);
|
||||
}
|
||||
|
||||
foreach ($subReportResult->getAggregatedColumnList() as $item1) {
|
||||
$list = $result->getAggregatedColumnList();
|
||||
$list[] = $item1;
|
||||
$result->setAggregatedColumnList($list);
|
||||
}
|
||||
|
||||
foreach (($subReportResult->getGrouping()[0] ?? []) as $groupName) {
|
||||
if (in_array($groupName, $result->getGrouping()[0] ?? [])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list = $result->getGrouping()[0] ?? [];
|
||||
$list[] = $groupName;
|
||||
$result->setGrouping([$list]);
|
||||
}
|
||||
|
||||
foreach (get_object_vars($subReportResult->getReportData()) as $key => $dataItem) {
|
||||
$reportData = $result->getReportData();
|
||||
|
||||
if (property_exists($reportData, $key)) {
|
||||
foreach (get_object_vars($dataItem) as $key1 => $value) {
|
||||
$reportData->$key->$key1 = $value;
|
||||
}
|
||||
} else {
|
||||
$reportData->$key = $dataItem;
|
||||
}
|
||||
|
||||
$result->setReportData($reportData);
|
||||
}
|
||||
|
||||
$entityTypeList = $result->getEntityTypeList() ?? [];
|
||||
$entityTypeList[] = $report->getTargetEntityType();
|
||||
$result->setEntityTypeList($entityTypeList);
|
||||
}
|
||||
|
||||
foreach ($subReportResult->getColumnList() as $column) {
|
||||
$columnEntityTypeMap = $result->getColumnEntityTypeMap();
|
||||
$columnReportIdMap = $result->getColumnReportIdMap();
|
||||
$columnSubReportLabelMap = $result->getColumnSubReportLabelMap();
|
||||
|
||||
$columnEntityTypeMap[$column] = $report->getTargetEntityType();
|
||||
$columnReportIdMap[$column] = $report->getId();
|
||||
|
||||
$columnSubReportLabelMap[$column] = !empty($item->label) ?
|
||||
$item->label :
|
||||
$this->language->translateLabel($report->getTargetEntityType(), 'scopeNamesPlural');
|
||||
|
||||
$result->setColumnEntityTypeMap($columnEntityTypeMap);
|
||||
$result->setColumnReportIdMap($columnReportIdMap);
|
||||
$result->setColumnSubReportLabelMap($columnSubReportLabelMap);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$groupColumn &&
|
||||
isset($result->getGrouping()[0])
|
||||
) {
|
||||
$list = $result->getGrouping()[0];
|
||||
$this->resultHelper->prepareGroupingRange($groupColumn, $list);
|
||||
|
||||
$result->setGrouping([$list]);
|
||||
}
|
||||
|
||||
foreach (get_object_vars($result->getReportData()) as $key => $dataItem) {
|
||||
foreach ($result->getColumnList() as $column) {
|
||||
if (property_exists($dataItem, $column)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$originalColumn = $result->getColumnOriginalMap()[$column];
|
||||
$originalEntityType = $result->getColumnEntityTypeMap()[$column];
|
||||
|
||||
[, $i] = explode('@', $column);
|
||||
|
||||
$i = (int) $i;
|
||||
|
||||
$report = $reportList[$i];
|
||||
|
||||
$gridData = $this->reportHelper->fetchGridDataFromReport($report);
|
||||
|
||||
$reportData = $result->getReportData();
|
||||
|
||||
if ($this->gridHelper->isColumnNumeric($originalColumn, $gridData)) {
|
||||
$reportData->$key->$column = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = null;
|
||||
|
||||
if ($groupColumn && $groupColumn !== self::STUB_KEY) {
|
||||
$subReportGroupColumn = $report->getGroupBy()[0];
|
||||
|
||||
if (str_starts_with($originalColumn, $subReportGroupColumn)) {
|
||||
$displayValue = null;
|
||||
|
||||
$columnData = $this->gridHelper->getDataFromColumnName($originalEntityType, $originalColumn);
|
||||
|
||||
$e = $this->entityManager->getEntity($columnData->entityType, $key);
|
||||
|
||||
if ($e) {
|
||||
$value = $e->get($columnData->field);
|
||||
|
||||
if ($columnData->fieldType === 'link') {
|
||||
$value = $e->get($columnData->field . 'Id');
|
||||
|
||||
$displayValue = $e->get($columnData->field . 'Name');
|
||||
} else {
|
||||
$displayValue = $this->gridUtil->getCellDisplayValue($value, $columnData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_null($displayValue)) {
|
||||
$maps = $result->getCellValueMaps();
|
||||
|
||||
if (!property_exists($maps, $column)) {
|
||||
$maps->$column = (object) [];
|
||||
}
|
||||
|
||||
$maps->$column->$value = $displayValue;
|
||||
|
||||
$result->setCellValueMaps($maps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$reportData->$key->$column = $value;
|
||||
|
||||
$result->setReportData($reportData);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setSummaryColumnList($result, $reportList);
|
||||
|
||||
$result->setSubListColumnList([]);
|
||||
$result->setChartType($data->getChartType());
|
||||
$result->setIsJoint(true);
|
||||
|
||||
$this->setChartColors($result);
|
||||
$this->setChartDataList($reportList, $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Report[] $reportList
|
||||
*/
|
||||
private function setChartDataList(array $reportList, GridResult $result): void
|
||||
{
|
||||
$chartColumnList = [];
|
||||
$chartY2ColumnList = [];
|
||||
|
||||
foreach ($reportList as $i => $report) {
|
||||
$gridData = $this->reportHelper->fetchGridDataFromReport($report);
|
||||
|
||||
$itemDataList = $gridData->getChartDataList();
|
||||
|
||||
if ($itemDataList && count($itemDataList)) {
|
||||
foreach ($itemDataList[0]->columnList ?? [] as $item) {
|
||||
$chartColumnList[] = $item . '@' . $i;
|
||||
}
|
||||
|
||||
foreach ($itemDataList[0]->y2ColumnList ?? [] as $item) {
|
||||
$chartY2ColumnList[] = $item . '@' . $i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($chartColumnList === [] && $chartY2ColumnList === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result->setChartDataList([
|
||||
(object) [
|
||||
'columnList' => $chartColumnList,
|
||||
'y2ColumnList' => $chartY2ColumnList,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
private function setChartColors(GridResult $result): void
|
||||
{
|
||||
$colorList = [];
|
||||
$chartColors = $result->getChartColors();
|
||||
|
||||
foreach (get_object_vars($chartColors) as $key => $value) {
|
||||
if (in_array($value, $colorList)) {
|
||||
unset($chartColors->$key);
|
||||
}
|
||||
|
||||
$colorList[] = $value;
|
||||
}
|
||||
|
||||
if (array_keys(get_object_vars($chartColors)) === []) {
|
||||
$chartColors = null;
|
||||
}
|
||||
|
||||
$result->setChartColors($chartColors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Report[] $reportList
|
||||
*/
|
||||
private function setSummaryColumnList(GridResult $result, array $reportList): void
|
||||
{
|
||||
$summaryColumnList = [];
|
||||
|
||||
foreach ($result->getColumnList() as $column) {
|
||||
[, $i] = explode('@', $column);
|
||||
|
||||
$report = $reportList[$i];
|
||||
|
||||
$gridData = $this->reportHelper->fetchGridDataFromReport($report);
|
||||
|
||||
if ($this->gridHelper->isColumnSummary($column, $gridData)) {
|
||||
$summaryColumnList[] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
$result->setSummaryColumnList($summaryColumnList);
|
||||
}
|
||||
}
|
||||
214
custom/Espo/Modules/Advanced/Tools/Report/ListExportService.php
Normal file
214
custom/Espo/Modules/Advanced/Tools/Report/ListExportService.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?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\Acl\Table as AclTable;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Entities\Report;
|
||||
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\EntityManager;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function export(
|
||||
string $id,
|
||||
SearchParams $searchParams,
|
||||
ExportParams $exportParams,
|
||||
?SubReportParams $subReportParams = null,
|
||||
?User $user = null
|
||||
): string {
|
||||
|
||||
$runParams = ListRunParams::create()->withIsExport();
|
||||
|
||||
if (
|
||||
$user &&
|
||||
$this->aclManager->getPermissionLevel($user, 'exportPermission') !== AclTable::LEVEL_YES
|
||||
) {
|
||||
throw new Forbidden("Export is forbidden.");
|
||||
}
|
||||
|
||||
if ($exportParams->getFieldList() === null) {
|
||||
$runParams = $runParams->withFullSelect();
|
||||
}
|
||||
else {
|
||||
$customColumnList = [];
|
||||
|
||||
foreach ($exportParams->getFieldList() as $item) {
|
||||
$value = $item;
|
||||
|
||||
if (strpos($item, '_') !== false) {
|
||||
$value = str_replace('_', '.', $item);
|
||||
}
|
||||
|
||||
$customColumnList[] = $value;
|
||||
}
|
||||
|
||||
$runParams = $runParams->withCustomColumnList($customColumnList);
|
||||
}
|
||||
|
||||
if ($exportParams->getIds()) {
|
||||
$searchParams = $searchParams->withWhereAdded(
|
||||
WhereItem::createBuilder()
|
||||
->setAttribute('id')
|
||||
->setType('equals')
|
||||
->setValue($exportParams->getIds())
|
||||
->build()
|
||||
);
|
||||
}
|
||||
|
||||
if ($subReportParams) {
|
||||
$searchParams = $searchParams->withSelect($exportParams->getAttributeList());
|
||||
}
|
||||
|
||||
$reportResult = $subReportParams ?
|
||||
$this->service->runSubReportList(
|
||||
$id,
|
||||
$searchParams,
|
||||
$subReportParams,
|
||||
$user,
|
||||
$runParams
|
||||
) :
|
||||
$this->service->runList(
|
||||
$id,
|
||||
$searchParams,
|
||||
$user,
|
||||
$runParams
|
||||
);
|
||||
|
||||
$collection = $reportResult->getCollection();
|
||||
|
||||
/** @var ?Report $report */
|
||||
$report = $this->entityManager->getEntityById(Report::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$report) {
|
||||
throw new NotFound("Report $id not found.");
|
||||
}
|
||||
|
||||
$entityType = $report->getTargetEntityType();
|
||||
|
||||
if (
|
||||
$user &&
|
||||
!$this->aclManager->checkScope($user, $entityType, AclTable::ACTION_READ)
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
$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)
|
||||
->run()
|
||||
->getAttachmentId();
|
||||
}
|
||||
|
||||
private function getForeignFieldType(string $entityType, string $link, string $field): ?string
|
||||
{
|
||||
$entityDefs = $this->ormDefs->getEntity($entityType);
|
||||
|
||||
if (!$entityDefs->hasRelation($link)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$relationDefs = $entityDefs->getRelation($link);
|
||||
|
||||
if (!$relationDefs->hasForeignEntityType()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entityDefs = $this->ormDefs->getEntity($relationDefs->getForeignEntityType());
|
||||
|
||||
if (!$entityDefs->hasField($field)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $entityDefs->getField($field)->getType();
|
||||
}
|
||||
}
|
||||
84
custom/Espo/Modules/Advanced/Tools/Report/ListType/Data.php
Normal file
84
custom/Espo/Modules/Advanced/Tools/Report/ListType/Data.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?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\ListType;
|
||||
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Data
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $columns;
|
||||
/** @var ?string */
|
||||
private ?string $orderBy;
|
||||
|
||||
/**
|
||||
* @param string[] $columns
|
||||
*/
|
||||
public function __construct(
|
||||
private string $entityType,
|
||||
array $columns,
|
||||
?string $orderBy,
|
||||
private ?stdClass $columnsData,
|
||||
private ?WhereItem $filtersWhere
|
||||
) {
|
||||
$this->columns = $columns;
|
||||
$this->orderBy = $orderBy;
|
||||
}
|
||||
|
||||
public function getEntityType(): string
|
||||
{
|
||||
return $this->entityType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getColumns(): array
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
public function getOrderBy(): ?string
|
||||
{
|
||||
return $this->orderBy;
|
||||
}
|
||||
|
||||
public function getColumnsData(): ?stdClass
|
||||
{
|
||||
return $this->columnsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $columns
|
||||
*/
|
||||
public function withColumns(array $columns): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->columns = $columns;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function getFiltersWhere(): ?WhereItem
|
||||
{
|
||||
return $this->filtersWhere;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?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\ListType;
|
||||
|
||||
class ExportParams
|
||||
{
|
||||
|
||||
/**
|
||||
* @param ?string[] $attributeList
|
||||
* @param ?string[] $fieldList
|
||||
* @param ?string[] $ids
|
||||
* @param ?array<string, mixed> $params
|
||||
*/
|
||||
public function __construct(
|
||||
private ?array $attributeList,
|
||||
private ?array $fieldList,
|
||||
private ?string $format,
|
||||
private ?array $ids,
|
||||
private ?array $params
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getAttributeList(): ?array
|
||||
{
|
||||
return $this->attributeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getFieldList(): ?array
|
||||
{
|
||||
return $this->fieldList;
|
||||
}
|
||||
|
||||
public function getFormat(): ?string
|
||||
{
|
||||
return $this->format;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getIds(): ?array
|
||||
{
|
||||
return $this->ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
public function getParams(): ?array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?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\ListType;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
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\Entities\Preferences;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Tools\Report\SelectHelper;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\Part\Selection;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
class QueryPreparator
|
||||
{
|
||||
public function __construct(
|
||||
private SelectHelper $selectHelper,
|
||||
private SelectBuilderFactory $selectBuilderFactory,
|
||||
private Config $config,
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Complex expression check is not applied for search parameters as it's supposed
|
||||
* to be checked the by runtime filter checker.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function prepare(
|
||||
Data $data,
|
||||
?SearchParams $searchParams = null,
|
||||
?User $user = null
|
||||
): SelectBuilder {
|
||||
|
||||
$searchParams = $searchParams ?? SearchParams::create();
|
||||
|
||||
$orderBy = $searchParams->getOrderBy();
|
||||
$order = $searchParams->getOrder();
|
||||
|
||||
if ($orderBy && str_contains($orderBy, '_')) {
|
||||
$searchParams = $searchParams
|
||||
->withOrderBy(null);
|
||||
}
|
||||
|
||||
if ($searchParams->getWhere() && $user) {
|
||||
$searchParams = $this->applyTimeZoneToSearchParams($searchParams, $user);
|
||||
}
|
||||
|
||||
$selectBuilder = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from($data->getEntityType())
|
||||
->withSearchParams($searchParams->withSelect(['id']));
|
||||
|
||||
if ($user) {
|
||||
$selectBuilder
|
||||
->forUser($user)
|
||||
->withWherePermissionCheck()
|
||||
->withAccessControlFilter();
|
||||
}
|
||||
|
||||
// Applies access control check.
|
||||
$intermediateQuery = $selectBuilder->build();
|
||||
|
||||
$selectBuilder = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from($data->getEntityType())
|
||||
->withSearchParams($searchParams);
|
||||
|
||||
if ($user) {
|
||||
$selectBuilder
|
||||
->forUser($user)
|
||||
->withAccessControlFilter();
|
||||
}
|
||||
|
||||
$queryBuilder = $selectBuilder
|
||||
->buildQueryBuilder()
|
||||
->from($data->getEntityType(), lcfirst($data->getEntityType()));
|
||||
|
||||
if ($data->getColumns() !== []) {
|
||||
// Add columns applied from order-by.
|
||||
|
||||
$queryBuilder->select(
|
||||
// Prevent issue in ORM (as of v7.5).
|
||||
array_map(function (Selection $selection) {
|
||||
return !$selection->getAlias() ?
|
||||
$selection->getExpression() :
|
||||
$selection;
|
||||
}, $intermediateQuery->getSelect())
|
||||
);
|
||||
|
||||
$this->selectHelper->handleColumns($data->getColumns(), $queryBuilder);
|
||||
}
|
||||
|
||||
if ($data->getFiltersWhere()) {
|
||||
[$whereItem, $havingItem] = $this->selectHelper->splitHavingItem($data->getFiltersWhere());
|
||||
|
||||
$this->selectHelper->handleFiltersWhere($whereItem, $queryBuilder);
|
||||
$this->selectHelper->handleFiltersHaving($havingItem, $queryBuilder);
|
||||
}
|
||||
|
||||
if ($orderBy) {
|
||||
$this->selectHelper->handleOrderByForList($orderBy, $order, $queryBuilder);
|
||||
}
|
||||
|
||||
if ($searchParams->getWhere()) {
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->selectHelper->applyDistinctFromWhere($searchParams->getWhere(), $queryBuilder);
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyTimeZoneToSearchParams(SearchParams $searchParams, User $user): SearchParams
|
||||
{
|
||||
$where = $searchParams->getWhere();
|
||||
|
||||
if (!$where) {
|
||||
return $searchParams;
|
||||
}
|
||||
|
||||
return $searchParams->withWhere(
|
||||
$this->addUserTimeZoneToWhere($where, $user)
|
||||
);
|
||||
}
|
||||
|
||||
private function addUserTimeZoneToWhere(WhereItem $item, User $user, ?string $timeZone = null): WhereItem
|
||||
{
|
||||
$timeZone ??= $this->getUserTimeZone($user);
|
||||
|
||||
if (
|
||||
$item->getType() === WhereItem\Type::AND ||
|
||||
$item->getType() === WhereItem\Type::OR
|
||||
) {
|
||||
$items = [];
|
||||
|
||||
foreach ($item->getItemList() as $subItem) {
|
||||
$items[] = $this->addUserTimeZoneToWhere($subItem, $user, $timeZone);
|
||||
}
|
||||
|
||||
return WhereItemBuilder::create()
|
||||
->setType($item->getType())
|
||||
->setItemList($items)
|
||||
->build();
|
||||
}
|
||||
|
||||
$data = $item->getData();
|
||||
|
||||
if (!$data) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
if (
|
||||
!$data instanceof WhereItem\Data\DateTime &&
|
||||
!method_exists($data, 'withTimeZone')
|
||||
) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
return $item->withData(
|
||||
$data->withTimeZone($timeZone)
|
||||
);
|
||||
}
|
||||
|
||||
private function getUserTimeZone(User $user): string
|
||||
{
|
||||
$preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $user->getId());
|
||||
|
||||
if ($preferences->get('timeZone')) {
|
||||
return $preferences->get('timeZone');
|
||||
}
|
||||
|
||||
return $this->config->get('timeZone') ?? 'UTC';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?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\ListType;
|
||||
|
||||
use Espo\ORM\Collection;
|
||||
use Espo\ORM\Entity;
|
||||
use stdClass;
|
||||
|
||||
class Result
|
||||
{
|
||||
/** @var Collection<Entity> */
|
||||
private Collection $collection;
|
||||
private int $total;
|
||||
/** @var ?string[] */
|
||||
private ?array $columns;
|
||||
private ?stdClass $columnsData;
|
||||
|
||||
/**
|
||||
* @param Collection<Entity> $collection
|
||||
* @param ?string[] $columns
|
||||
*/
|
||||
public function __construct(
|
||||
Collection $collection,
|
||||
int $total,
|
||||
?array $columns = null,
|
||||
?stdClass $columnsData = null
|
||||
) {
|
||||
$this->collection = $collection;
|
||||
$this->total = $total;
|
||||
$this->columns = $columns;
|
||||
$this->columnsData = $columnsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Entity>
|
||||
*/
|
||||
public function getCollection(): Collection
|
||||
{
|
||||
return $this->collection;
|
||||
}
|
||||
|
||||
public function getTotal(): int
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getColumns(): ?array
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
public function getColumnsData(): ?stdClass
|
||||
{
|
||||
return $this->columnsData;
|
||||
}
|
||||
}
|
||||
107
custom/Espo/Modules/Advanced/Tools/Report/ListType/RunParams.php
Normal file
107
custom/Espo/Modules/Advanced/Tools/Report/ListType/RunParams.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?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\ListType;
|
||||
|
||||
class RunParams
|
||||
{
|
||||
private bool $skipRuntimeFiltersCheck = false;
|
||||
private bool $returnSthCollection = false;
|
||||
private bool $isExport = false;
|
||||
private bool $fullSelect = false;
|
||||
/** @var ?string[] */
|
||||
private ?array $customColumnList = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public function skipRuntimeFiltersCheck(): bool
|
||||
{
|
||||
return $this->skipRuntimeFiltersCheck;
|
||||
}
|
||||
|
||||
public function withSkipRuntimeFiltersCheck(bool $value = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->skipRuntimeFiltersCheck = $value;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function withReturnSthCollection(bool $value = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->returnSthCollection = $value;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withIsExport(bool $value = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->isExport = $value;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withFullSelect(bool $value = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->fullSelect = $value;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $value
|
||||
*/
|
||||
public function withCustomColumnList(?array $value): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->customColumnList = $value;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function returnSthCollection(): bool
|
||||
{
|
||||
return $this->returnSthCollection;
|
||||
}
|
||||
|
||||
public function isExport(): bool
|
||||
{
|
||||
return $this->isExport;
|
||||
}
|
||||
|
||||
public function isFullSelect(): bool
|
||||
{
|
||||
return $this->fullSelect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getCustomColumnList(): ?array
|
||||
{
|
||||
return $this->customColumnList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\Advanced\Tools\Report\ListType;
|
||||
|
||||
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\Entities\User;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Data as Data;
|
||||
use Espo\Modules\Advanced\Tools\Report\SelectHelper;
|
||||
use Espo\ORM\Query\Select;
|
||||
|
||||
class SubListQueryPreparator
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private SubReportQueryPreparator $subReportQueryPreparator,
|
||||
private SelectHelper $selectHelper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param ?scalar $groupValue
|
||||
* @param string[] $columnList
|
||||
* @param string[] $realColumnList
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function prepare(
|
||||
Data $data,
|
||||
$groupValue,
|
||||
array $columnList,
|
||||
array $realColumnList,
|
||||
?WhereItem $where,
|
||||
?User $user,
|
||||
): Select {
|
||||
|
||||
$searchParams = SearchParams::create()->withSelect(['id']);
|
||||
|
||||
if ($where) {
|
||||
$searchParams = $searchParams->withWhere($where);
|
||||
}
|
||||
|
||||
$queryBuilder = $this->subReportQueryPreparator->prepare(
|
||||
data: $data,
|
||||
searchParams: $searchParams,
|
||||
subReportParams: new SubReportParams(0, $groupValue),
|
||||
user: $user,
|
||||
);
|
||||
|
||||
$this->selectHelper->handleColumns($realColumnList, $queryBuilder);
|
||||
|
||||
$newOrderBy = [];
|
||||
|
||||
foreach ($data->getOrderBy() as $orderByItem) {
|
||||
$orderByColumn = explode(':', $orderByItem)[1] ?? null;
|
||||
|
||||
if (in_array($orderByColumn, $columnList)) {
|
||||
$newOrderBy[] = $orderByItem;
|
||||
}
|
||||
}
|
||||
|
||||
if ($newOrderBy !== []) {
|
||||
$queryBuilder->order([]);
|
||||
}
|
||||
|
||||
$this->selectHelper->handleOrderBy($newOrderBy, $queryBuilder);
|
||||
|
||||
return $queryBuilder->build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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\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 bool $hasGroupValue2 = false,
|
||||
$groupValue2 = null
|
||||
) {
|
||||
$this->groupValue = $groupValue;
|
||||
$this->groupValue2 = $groupValue2;
|
||||
}
|
||||
|
||||
public function getGroupIndex(): int
|
||||
{
|
||||
return $this->groupIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?scalar
|
||||
*/
|
||||
public function getGroupValue()
|
||||
{
|
||||
return $this->groupValue;
|
||||
}
|
||||
|
||||
public function hasGroupValue2(): bool
|
||||
{
|
||||
return $this->hasGroupValue2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?scalar
|
||||
*/
|
||||
public function getGroupValue2()
|
||||
{
|
||||
return $this->groupValue2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
<?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\ListType;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
use Espo\Core\Select\SelectBuilderFactory;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Data as GridData;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Helper as GridHelper;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\QueryPreparator as GridQueryPreparator;
|
||||
use Espo\Modules\Advanced\Tools\Report\SelectHelper;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\Part\Selection;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class SubReportQueryPreparator
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private SelectHelper $selectHelper,
|
||||
private GridHelper $gridHelper,
|
||||
private SelectBuilderFactory $selectBuilderFactory,
|
||||
private GridQueryPreparator $gridQueryPreparator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* A sub-report query preparator.
|
||||
*
|
||||
* Complex expression check is not applied for search parameters as it's supposed
|
||||
* to be checked the by runtime filter checker.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function prepare(
|
||||
GridData $data,
|
||||
SearchParams $searchParams,
|
||||
SubReportParams $subReportParams,
|
||||
?User $user = null,
|
||||
): SelectBuilder {
|
||||
|
||||
$entityType = $data->getEntityType();
|
||||
|
||||
$selectBuilder = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from($data->getEntityType())
|
||||
->withSearchParams($searchParams);
|
||||
|
||||
if ($user) {
|
||||
$selectBuilder
|
||||
->withWherePermissionCheck()
|
||||
->forUser($user);
|
||||
}
|
||||
|
||||
if ($user && $data->applyAcl()) {
|
||||
$selectBuilder->withAccessControlFilter();
|
||||
}
|
||||
|
||||
$queryBuilder = $selectBuilder->buildQueryBuilder();
|
||||
|
||||
$selectColumns = $queryBuilder->build()->getSelect();
|
||||
|
||||
$this->gridHelper->checkColumnsAvailability($entityType, $data->getGroupBy());
|
||||
|
||||
[$groupBy, $groupByOther] = $this->handleGroupBy($data, $subReportParams, $queryBuilder);
|
||||
|
||||
// Prevent issue in ORM (not needed as of v7.5).
|
||||
$selectColumns = array_map(function (Selection $selection) {
|
||||
return !$selection->getAlias() ?
|
||||
$selection->getExpression() :
|
||||
$selection;
|
||||
}, $selectColumns);
|
||||
|
||||
$queryBuilder
|
||||
->from($data->getEntityType(), lcfirst($data->getEntityType()))
|
||||
->select($selectColumns);
|
||||
|
||||
if ($data->getFiltersWhere()) {
|
||||
[$whereItem,] = $this->selectHelper->splitHavingItem($data->getFiltersWhere());
|
||||
|
||||
$this->selectHelper->handleFiltersWhere($whereItem, $queryBuilder);
|
||||
|
||||
$this->handleHaving(
|
||||
data: $data,
|
||||
subReportParams: $subReportParams,
|
||||
where: $searchParams->getWhere(),
|
||||
user: $user,
|
||||
groupBy: $groupBy,
|
||||
groupByOther: $groupByOther,
|
||||
queryBuilder: $queryBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
if ($searchParams->getWhere()) {
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->selectHelper->applyDistinctFromWhere($searchParams->getWhere(), $queryBuilder);
|
||||
}
|
||||
|
||||
$this->applyGroupWhereAll(
|
||||
data: $data,
|
||||
subReportParams: $subReportParams,
|
||||
groupBy: $groupBy,
|
||||
groupByOther: $groupByOther,
|
||||
queryBuilder: $queryBuilder,
|
||||
);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?string, ?string}
|
||||
*/
|
||||
private function handleGroupBy(
|
||||
GridData $data,
|
||||
SubReportParams $subReportParams,
|
||||
SelectBuilder $queryBuilder
|
||||
): array {
|
||||
|
||||
if (!$data->getGroupBy()) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$groupIndex = $subReportParams->getGroupIndex();
|
||||
|
||||
$this->selectHelper->handleGroupBy($data->getGroupBy(), $queryBuilder);
|
||||
|
||||
$groupByExpressions = $queryBuilder->build()->getGroup();
|
||||
|
||||
if (!isset($groupByExpressions[$groupIndex])) {
|
||||
throw new RuntimeException('No group by.');
|
||||
}
|
||||
|
||||
$groupBy = $groupByExpressions[$groupIndex]->getValue();
|
||||
|
||||
$queryBuilder->group([]);
|
||||
|
||||
if (count($data->getGroupBy()) === 1) {
|
||||
return [$groupBy, null];
|
||||
}
|
||||
|
||||
$groupBy1Type = $this->metadata
|
||||
->get(['entityDefs', $data->getEntityType(), 'fields', $data->getGroupBy()[0], 'type']);
|
||||
|
||||
if ($groupIndex === 1) {
|
||||
$groupByOther = $groupByExpressions[0]->getValue();
|
||||
|
||||
if ($groupBy1Type === 'linkParent') {
|
||||
$groupBy = $groupByExpressions[2]->getValue();
|
||||
}
|
||||
|
||||
return [$groupBy, $groupByOther];
|
||||
}
|
||||
|
||||
$groupByOther = $groupBy1Type === 'linkParent' ?
|
||||
$groupByExpressions[2]->getValue() :
|
||||
$groupByExpressions[1]->getValue();
|
||||
|
||||
return [$groupBy, $groupByOther];
|
||||
}
|
||||
|
||||
private function applyGroupWhereAll(
|
||||
GridData $data,
|
||||
SubReportParams $subReportParams,
|
||||
?string $groupBy,
|
||||
?string $groupByOther,
|
||||
SelectBuilder $queryBuilder
|
||||
): void {
|
||||
|
||||
if ($groupBy !== null) {
|
||||
$this->applyGroupWhere($data, $subReportParams, $groupBy, $queryBuilder);
|
||||
}
|
||||
|
||||
if (!$groupByOther) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$subReportParams->hasGroupValue2()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applyGroup2Where(
|
||||
$data,
|
||||
$subReportParams,
|
||||
$groupByOther,
|
||||
$queryBuilder
|
||||
);
|
||||
}
|
||||
|
||||
private function applyGroupWhere(
|
||||
GridData $data,
|
||||
SubReportParams $subReportParams,
|
||||
string $groupBy,
|
||||
SelectBuilder $queryBuilder
|
||||
): void {
|
||||
|
||||
$index = $subReportParams->getGroupIndex();
|
||||
$value = $subReportParams->getGroupValue();
|
||||
|
||||
$this->applyGroupByWhereValue(
|
||||
$data,
|
||||
$index,
|
||||
$value,
|
||||
$groupBy,
|
||||
$queryBuilder
|
||||
);
|
||||
}
|
||||
|
||||
private function applyGroup2Where(
|
||||
GridData $data,
|
||||
SubReportParams $subReportParams,
|
||||
string $groupBy,
|
||||
SelectBuilder $queryBuilder
|
||||
): void {
|
||||
|
||||
$value = $subReportParams->getGroupValue2();
|
||||
|
||||
$this->applyGroupByWhereValue(
|
||||
$data,
|
||||
1,
|
||||
$value,
|
||||
$groupBy,
|
||||
$queryBuilder
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?scalar $value
|
||||
*/
|
||||
private function applyGroupByWhereValue(
|
||||
GridData $data,
|
||||
int $index,
|
||||
$value,
|
||||
string $groupBy,
|
||||
SelectBuilder $queryBuilder
|
||||
): void {
|
||||
|
||||
$fieldType = $this->metadata
|
||||
->get(['entityDefs', $data->getEntityType(), 'fields', $data->getGroupBy()[$index], 'type']);
|
||||
|
||||
if ($fieldType === 'linkParent') {
|
||||
if ($value === null) {
|
||||
$queryBuilder->where([
|
||||
$data->getGroupBy()[$index] . 'Id' => null,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$arr = explode(':,:', (string) $value);
|
||||
|
||||
$valueType = $arr[0];
|
||||
$valueId = null;
|
||||
|
||||
if (count($arr)) {
|
||||
$valueId = $arr[1];
|
||||
}
|
||||
|
||||
if (!$valueId) {
|
||||
$valueId = null;
|
||||
}
|
||||
|
||||
$queryBuilder->where([
|
||||
$data->getGroupBy()[$index] . 'Type' => $valueType,
|
||||
$data->getGroupBy()[$index] . 'Id' => $valueId,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$queryBuilder->where([
|
||||
'OR' => [
|
||||
[$groupBy => null],
|
||||
]
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder->where([$groupBy => $value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function handleHaving(
|
||||
GridData $data,
|
||||
SubReportParams $subReportParams,
|
||||
?WhereItem $where,
|
||||
?User $user,
|
||||
?string $groupBy,
|
||||
?string $groupByOther,
|
||||
SelectBuilder $queryBuilder
|
||||
): void {
|
||||
|
||||
[, $havingItem] = $this->selectHelper->splitHavingItem($data->getFiltersWhere());
|
||||
|
||||
if ($havingItem->getItemList() === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$gridQuery = $this->gridQueryPreparator->prepare($data, $where, $user);
|
||||
|
||||
$subQueryBuilder = SelectBuilder::create()
|
||||
->clone($gridQuery);
|
||||
|
||||
$this->applyGroupWhereAll(
|
||||
$data,
|
||||
$subReportParams,
|
||||
$groupBy,
|
||||
$groupByOther,
|
||||
$subQueryBuilder
|
||||
);
|
||||
|
||||
if (!method_exists(Cond::class, 'exists')) { /** @phpstan-ignore-line */
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder->where(
|
||||
Cond::exists($subQueryBuilder->build())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?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\Acl;
|
||||
use Espo\Core\Acl\Table as AclTable;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Entities\Report;
|
||||
use Espo\ORM\EntityManager;
|
||||
use stdClass;
|
||||
|
||||
class PreviewReportProvider
|
||||
{
|
||||
public function __construct(
|
||||
private Service $service,
|
||||
private Acl $acl,
|
||||
private EntityManager $entityManager,
|
||||
private ServiceContainer $serviceContainer,
|
||||
private User $user,
|
||||
private ReportHelper $reportHelper,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function get(stdClass $data): Report
|
||||
{
|
||||
$report = $this->entityManager->getRDBRepositoryByClass(Report::class)->getNew();
|
||||
|
||||
unset($data->isInternal);
|
||||
|
||||
$attributeList = [
|
||||
'entityType',
|
||||
'type',
|
||||
'data',
|
||||
'columns',
|
||||
'groupBy',
|
||||
'orderBy',
|
||||
'orderByList',
|
||||
'filters',
|
||||
'filtersDataList',
|
||||
'runtimeFilters',
|
||||
'filtersData',
|
||||
'columnsData',
|
||||
'chartColors',
|
||||
'chartDataList',
|
||||
'chartOneColumns',
|
||||
'chartOneY2Columns',
|
||||
'chartType',
|
||||
'joinedReports',
|
||||
'joinedReportLabel',
|
||||
'joinedReportDataList',
|
||||
];
|
||||
|
||||
foreach (array_keys(get_object_vars($data)) as $attribute) {
|
||||
if (!in_array($attribute, $attributeList)) {
|
||||
unset($data->$attribute);
|
||||
}
|
||||
}
|
||||
$report->setMultiple($data);
|
||||
|
||||
$report->setApplyAcl();
|
||||
$report->setName('Unnamed');
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
504
custom/Espo/Modules/Advanced/Tools/Report/ReportHelper.php
Normal file
504
custom/Espo/Modules/Advanced/Tools/Report/ReportHelper.php
Normal file
@@ -0,0 +1,504 @@
|
||||
<?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\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Formula\Manager as FormulaManager;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\Preferences;
|
||||
use Espo\Modules\Advanced\Entities\Report;
|
||||
use Espo\Modules\Advanced\Reports\GridReport;
|
||||
use Espo\Modules\Advanced\Reports\ListReport;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Data as GridData;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\JointData;
|
||||
use Espo\Modules\Advanced\Tools\Report\ListType\Data as ListData;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class ReportHelper
|
||||
{
|
||||
private const WHERE_TYPE_AND = 'and';
|
||||
private const WHERE_TYPE_OR = 'or';
|
||||
private const WHERE_TYPE_HAVING = 'having';
|
||||
private const WHERE_TYPE_NOT = 'not';
|
||||
private const WHERE_TYPE_SUB_QUERY_IN = 'subQueryIn';
|
||||
private const WHERE_TYPE_SUB_QUERY_NOT_IN = 'subQueryNotIn';
|
||||
|
||||
private const ATTR_HAVING = '_having';
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private FormulaManager $formulaManager,
|
||||
private Config $config,
|
||||
private Preferences $preferences,
|
||||
private FormulaChecker $formulaChecker
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @return ListReport|GridReport
|
||||
*/
|
||||
public function createInternalReport(Report $report): object
|
||||
{
|
||||
$className = $report->get('internalClassName');
|
||||
|
||||
if ($className && stripos($className, ':') !== false) {
|
||||
[$moduleName, $reportName] = explode(':', $className);
|
||||
|
||||
if ($moduleName === 'Custom') {
|
||||
$className = "Espo\\Custom\\Reports\\$reportName";
|
||||
} else {
|
||||
$className = "Espo\\Modules\\$moduleName\\Reports\\$reportName";
|
||||
}
|
||||
}
|
||||
|
||||
if (!$className) {
|
||||
throw new Error('No class name specified for internal report.');
|
||||
}
|
||||
|
||||
/** @var class-string<ListReport|GridReport> $className */
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function checkReportCanBeRun(Report $report): void
|
||||
{
|
||||
if (
|
||||
in_array(
|
||||
$report->getTargetEntityType(),
|
||||
$this->metadata->get('entityDefs.Report.entityListToIgnore', [])
|
||||
)
|
||||
) {
|
||||
throw new Forbidden("Entity type is not allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function fetchGridDataFromReport(Report $report): GridData
|
||||
{
|
||||
if ($report->getType() !== Report::TYPE_GRID) {
|
||||
throw new Error("Non-grid report.");
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function fetchListDataFromReport(Report $report): ListData
|
||||
{
|
||||
if ($report->getType() !== Report::TYPE_LIST) {
|
||||
throw new Error("Non-list report.");
|
||||
}
|
||||
|
||||
return new ListData(
|
||||
$report->getTargetEntityType(),
|
||||
$report->getColumns(),
|
||||
$report->getOrderByList(),
|
||||
$report->getColumnsData(),
|
||||
$this->fetchFiltersWhereFromReport($report)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function fetchJointDataFromReport(Report $report): JointData
|
||||
{
|
||||
if ($report->getType() !== Report::TYPE_JOINT_GRID) {
|
||||
throw new Error("Non-joint-grid report.");
|
||||
}
|
||||
|
||||
return new JointData(
|
||||
$report->get('joinedReportDataList') ?? [],
|
||||
$report->get('chartType')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function fetchFiltersWhereFromReport(Report $report): ?WhereItem
|
||||
{
|
||||
$isNotList = $report->getType() !== Report::TYPE_LIST;
|
||||
|
||||
$raw = $report->get('filtersData') && !$report->get('filtersDataList') ?
|
||||
$this->convertFiltersData($report->get('filtersData')) :
|
||||
$this->convertFiltersDataList($report->get('filtersDataList') ?? [], $isNotList);
|
||||
|
||||
if (!$raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$raw = json_decode(
|
||||
/** @phpstan-ignore-next-line */
|
||||
json_encode($raw),
|
||||
true
|
||||
);
|
||||
|
||||
return WhereItem::fromRawAndGroup($raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, object{
|
||||
* where?: mixed,
|
||||
* field?: string,
|
||||
* type?: string,
|
||||
* dateTime?: string,
|
||||
* value?: mixed,
|
||||
* }>|null $filtersData
|
||||
* @return stdClass[]|null
|
||||
*/
|
||||
private function convertFiltersData(?array $filtersData): ?array
|
||||
{
|
||||
if (empty($filtersData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$arr = [];
|
||||
|
||||
foreach ($filtersData as $name => $defs) {
|
||||
$field = $name;
|
||||
|
||||
if (empty($defs)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($defs->where)) {
|
||||
$arr[] = $defs->where;
|
||||
} else {
|
||||
if (isset($defs->field)) {
|
||||
$field = $defs->field;
|
||||
}
|
||||
|
||||
$type = $defs->type ?? null;
|
||||
|
||||
if (!empty($defs->dateTime)) {
|
||||
$arr[] = $this->fixDateTimeWhere(
|
||||
$type,
|
||||
$field,
|
||||
$defs->value ?? null,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
$o = new stdClass();
|
||||
|
||||
$o->type = $type;
|
||||
$o->field = $field;
|
||||
$o->value = $defs->value ?? null;
|
||||
|
||||
$arr[] = $o;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object{
|
||||
* type?: string,
|
||||
* name?: ?string,
|
||||
* params?: object{
|
||||
* type?: string,
|
||||
* where?: mixed,
|
||||
* attribute?: string,
|
||||
* field?: string,
|
||||
* dateTime?: string,
|
||||
* value?: mixed,
|
||||
* function?: string,
|
||||
* expression?: string,
|
||||
* operator?: string,
|
||||
* },
|
||||
* }[] $filtersDataList
|
||||
* @return stdClass[]|null
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function convertFiltersDataList(array $filtersDataList, bool $useSystemTimeZone): ?array
|
||||
{
|
||||
if (empty($filtersDataList)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$arr = [];
|
||||
|
||||
foreach ($filtersDataList as $defs) {
|
||||
$field = null;
|
||||
|
||||
if (isset($defs->name)) {
|
||||
$field = $defs->name;
|
||||
}
|
||||
|
||||
if (empty($defs) || empty($defs->params)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$params = $defs->params;
|
||||
|
||||
$type = $defs->type ?? null;
|
||||
|
||||
if (
|
||||
in_array($type, [
|
||||
self::WHERE_TYPE_OR,
|
||||
self::WHERE_TYPE_AND,
|
||||
self::WHERE_TYPE_NOT,
|
||||
self::WHERE_TYPE_SUB_QUERY_IN,
|
||||
self::WHERE_TYPE_SUB_QUERY_NOT_IN,
|
||||
self::WHERE_TYPE_HAVING,
|
||||
])
|
||||
) {
|
||||
if (empty($params->value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$o = new stdClass();
|
||||
|
||||
$o->type = $params->type ?? null;
|
||||
|
||||
if ($o->type === self::WHERE_TYPE_NOT) {
|
||||
$o->type = self::WHERE_TYPE_SUB_QUERY_NOT_IN;
|
||||
}
|
||||
|
||||
if ($o->type === self::WHERE_TYPE_HAVING) {
|
||||
$o->type = self::WHERE_TYPE_AND;
|
||||
$o->attribute = self::ATTR_HAVING;
|
||||
}
|
||||
|
||||
$o->value = $this->convertFiltersDataList($params->value, $useSystemTimeZone);
|
||||
|
||||
$arr[] = $o;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type === 'complexExpression') {
|
||||
$o = (object) [];
|
||||
|
||||
$function = $params->function ?? null;
|
||||
|
||||
if ($function === 'custom') {
|
||||
if (empty($params->expression)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$o->attribute = $params->expression;
|
||||
$o->type = 'expression';
|
||||
} else if ($function === 'customWithOperator') {
|
||||
if (empty($params->expression)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($params->operator)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$o->attribute = $params->expression;
|
||||
$o->type = $params->operator;
|
||||
} else {
|
||||
if (empty($params->attribute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($params->operator)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$o->attribute = $params->attribute;
|
||||
|
||||
if ($function) {
|
||||
$o->attribute = $function . ':' . $o->attribute;
|
||||
}
|
||||
|
||||
$o->type = $params->operator;
|
||||
}
|
||||
|
||||
if (isset($params->value) && is_string($params->value) && strlen($params->value)) {
|
||||
try {
|
||||
$o->value = $this->runFormula($params->value);
|
||||
}
|
||||
catch (FormulaError $e) {
|
||||
throw new Error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$arr[] = $o;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($params->where)) {
|
||||
$arr[] = $params->where;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($params->field)) {
|
||||
$field = $params->field;
|
||||
}
|
||||
|
||||
if (empty($params->type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $params->type;
|
||||
|
||||
if (!empty($params->dateTime)) {
|
||||
$arr[] = $this->fixDateTimeWhere(
|
||||
$type,
|
||||
$field,
|
||||
$params->value ?? null,
|
||||
$useSystemTimeZone
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$o = new stdClass();
|
||||
|
||||
$o->type = $type;
|
||||
$o->field = $field;
|
||||
$o->attribute = $field;
|
||||
$o->value = $params->value ?? null;
|
||||
|
||||
$arr[] = $o;
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function fixDateTimeWhere(string $type, string $field, $value, bool $useSystemTimeZone): object
|
||||
{
|
||||
$timeZone = null;
|
||||
|
||||
if (!$useSystemTimeZone) {
|
||||
$timeZone = $this->preferences->get('timeZone');
|
||||
}
|
||||
|
||||
if (!$timeZone) {
|
||||
$timeZone = $this->config->get('timeZone') ?? 'UTC';
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'attribute' => $field,
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
'dateTime' => true,
|
||||
'timeZone' => $timeZone,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function checkRuntimeFilters(WhereItem $where, Report $report): void
|
||||
{
|
||||
$this->checkRuntimeFiltersItem($where, $report->getRuntimeFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $allowedFilterList
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkRuntimeFiltersItem(WhereItem $item, array $allowedFilterList): void
|
||||
{
|
||||
$type = $item->getType();
|
||||
|
||||
if ($type === self::WHERE_TYPE_AND || $type === self::WHERE_TYPE_OR) {
|
||||
foreach ($item->getItemList() as $subItem) {
|
||||
$this->checkRuntimeFiltersItem($subItem, $allowedFilterList);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$attribute = $item->getAttribute();
|
||||
|
||||
if (!$attribute) {
|
||||
throw new Forbidden("Not allowed runtime filter item.");
|
||||
}
|
||||
|
||||
if ($attribute === 'id') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_contains($attribute, ':')) {
|
||||
throw new Forbidden("Expressions are not allowed in runtime filter.");
|
||||
}
|
||||
|
||||
if (!str_contains($attribute, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isAllowed = in_array($attribute, $allowedFilterList);
|
||||
|
||||
if (!$isAllowed && str_ends_with($attribute, 'Id')) {
|
||||
$isAllowed = in_array(substr($attribute, 0, -2), $allowedFilterList);
|
||||
}
|
||||
|
||||
if (!$isAllowed) {
|
||||
throw new Forbidden("Not allowed runtime filter $attribute.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws Forbidden
|
||||
* @throws FormulaError
|
||||
*/
|
||||
private function runFormula(string $script)
|
||||
{
|
||||
$this->formulaChecker->check($script);
|
||||
|
||||
$script = $this->formulaChecker->sanitize($script);
|
||||
|
||||
return $this->formulaManager->run($script);
|
||||
}
|
||||
}
|
||||
889
custom/Espo/Modules/Advanced/Tools/Report/SelectHelper.php
Normal file
889
custom/Espo/Modules/Advanced/Tools/Report/SelectHelper.php
Normal file
@@ -0,0 +1,889 @@
|
||||
<?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 DateTime;
|
||||
use DateTimeZone;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Select\Where\Converter;
|
||||
use Espo\Core\Select\Where\ConverterFactory;
|
||||
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\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Helper as GridHelper;
|
||||
use Espo\Modules\Advanced\Tools\Report\GridType\Util;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\Part\Expression;
|
||||
use Espo\ORM\Query\Part\Order;
|
||||
use Espo\ORM\Query\Part\Selection;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\ORM\QueryComposer\Util as QueryComposerUtil;
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
|
||||
class SelectHelper
|
||||
{
|
||||
private const WHERE_TYPE_AND = 'and';
|
||||
private const WHERE_TYPE_OR = 'or';
|
||||
|
||||
private const ATTR_HAVING = '_having';
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private Metadata $metadata,
|
||||
private Util $gridUtil,
|
||||
private EntityManager $entityManager,
|
||||
private GridHelper $gridHelper,
|
||||
private FieldUtil $fieldUtil,
|
||||
private User $user,
|
||||
private ConverterFactory $converterFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{0: WhereItem, 1: WhereItem}
|
||||
*/
|
||||
public function splitHavingItem(WhereItem $andItem): array
|
||||
{
|
||||
$whereItemList = [];
|
||||
$havingItemList = [];
|
||||
|
||||
foreach ($andItem->getItemList() as $item) {
|
||||
if (
|
||||
$item->getType() === self::WHERE_TYPE_AND &&
|
||||
$item->getAttribute() === self::ATTR_HAVING
|
||||
) {
|
||||
foreach ($item->getItemList() as $subItem) {
|
||||
$havingItemList[] = $subItem;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$whereItemList[] = $item;
|
||||
}
|
||||
|
||||
$whereItem = WhereItemBuilder::create()
|
||||
->setType(self::WHERE_TYPE_AND)
|
||||
->setItemList($whereItemList)
|
||||
->build();
|
||||
|
||||
$havingItem = WhereItemBuilder::create()
|
||||
->setType(self::WHERE_TYPE_AND)
|
||||
->setItemList($havingItemList)
|
||||
->build();
|
||||
|
||||
return [$whereItem, $havingItem];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function handleOrderByForList(string $orderBy, string $order, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
if (!$entityType) {
|
||||
throw new LogicException("No from.");
|
||||
}
|
||||
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
$fieldType = $entityDefs->hasField($orderBy) ?
|
||||
$entityDefs->getField($orderBy)->getType() :
|
||||
null;
|
||||
|
||||
if (
|
||||
in_array($fieldType, ['link', 'file', 'image']) &&
|
||||
!$queryBuilder->hasLeftJoinAlias($orderBy)
|
||||
) {
|
||||
$queryBuilder->leftJoin($orderBy);
|
||||
}
|
||||
|
||||
if (str_contains($orderBy, '_')) {
|
||||
if (str_contains($orderBy, ':')) {
|
||||
throw new Forbidden("Functions are not allowed in orderBy.");
|
||||
}
|
||||
|
||||
$orderBy = $this->getRealForeignOrderColumn($entityType, $orderBy);
|
||||
|
||||
$this->addSelect($orderBy, $queryBuilder);
|
||||
|
||||
/** @var 'ASC'|'DESC' $order */
|
||||
|
||||
$queryBuilder
|
||||
->order([])
|
||||
->order($orderBy, $order)
|
||||
->order('id', $order);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->fieldUtil->getAttributeList($entityType, $orderBy) as $attribute) {
|
||||
if (!$entityDefs->hasAttribute($attribute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addSelect($attribute, $queryBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
private function getRealForeignOrderColumn(string $entityType, string $item): string
|
||||
{
|
||||
$item = str_replace('_', '.', $item);
|
||||
|
||||
$data = $this->gridHelper->getDataFromColumnName($entityType, $item);
|
||||
|
||||
if (!$data->entityType) {
|
||||
throw new RuntimeException("Bad foreign order by '$item'.");
|
||||
}
|
||||
|
||||
if (in_array($data->fieldType, ['link', 'linkParent', 'image', 'file'])) {
|
||||
return $item . 'Id';
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $groupBy
|
||||
*/
|
||||
public function handleGroupBy(array $groupBy, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
if (!$entityType) {
|
||||
throw new LogicException("No from.");
|
||||
}
|
||||
|
||||
foreach ($groupBy as $item) {
|
||||
$this->handleGroupByItem($item, $entityType, $queryBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleGroupByItem(string $item, string $entityType, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
$alias = $this->gridUtil->sanitizeSelectAlias($item);
|
||||
|
||||
$function = null;
|
||||
$argument = $item;
|
||||
|
||||
if (str_contains($item, ':')) {
|
||||
[$function, $argument] = explode(':', $item);
|
||||
}
|
||||
|
||||
if (str_contains($item, '(') && str_contains($item, ':')) {
|
||||
$this->handleLeftJoins($item, $entityType, $queryBuilder, true);
|
||||
|
||||
$queryBuilder
|
||||
->select($item, $alias)
|
||||
->group($item);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function === 'YEAR_FISCAL') {
|
||||
$fiscalYearShift = $this->config->get('fiscalYearShift', 0);
|
||||
|
||||
$function = $fiscalYearShift ?
|
||||
'YEAR_' . $fiscalYearShift :
|
||||
'YEAR';
|
||||
|
||||
$item = $function . ':' . $argument;
|
||||
}
|
||||
else if ($function === 'QUARTER_FISCAL') {
|
||||
$fiscalYearShift = $this->config->get('fiscalYearShift', 0);
|
||||
|
||||
$function = $fiscalYearShift ?
|
||||
'QUARTER_' . $fiscalYearShift :
|
||||
'QUARTER';
|
||||
|
||||
$item = $function . ':' . $argument;
|
||||
}
|
||||
else if ($function === 'WEEK') {
|
||||
$function = $this->config->get('weekStart') ?
|
||||
'WEEK_1' :
|
||||
'WEEK_0';
|
||||
|
||||
$item = $function . ':' . $argument;
|
||||
}
|
||||
|
||||
if (!str_contains($item, '.')) {
|
||||
$fieldType = $this->metadata->get(['entityDefs', $entityType, 'fields', $argument, 'type']);
|
||||
|
||||
if (in_array($fieldType, ['link', 'file', 'image'])) {
|
||||
if (!$queryBuilder->hasLeftJoinAlias($item)) {
|
||||
$queryBuilder->leftJoin($item);
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
->select($item . 'Id')
|
||||
->group($item . 'Id');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($fieldType === 'linkParent') {
|
||||
if (!$queryBuilder->hasLeftJoinAlias($item)) {
|
||||
// @todo Revise
|
||||
$queryBuilder->leftJoin($item);
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
->select($item . 'Id')
|
||||
->select($item . 'Type')
|
||||
->group($item . 'Id')
|
||||
->group($item . 'Type');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function && in_array($fieldType, ['datetime', 'datetimeOptional'])) {
|
||||
$tzOffset = (string) $this->getTimeZoneOffset();
|
||||
|
||||
if ($tzOffset) {
|
||||
$groupBy = "$function:TZ:($argument,$tzOffset)";
|
||||
|
||||
$queryBuilder
|
||||
->select($groupBy)
|
||||
->group($groupBy);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
->select($item)
|
||||
->group($item);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
->select($item)
|
||||
->group($item);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
[$link, $field] = explode('.', $argument);
|
||||
|
||||
$skipSelect = false;
|
||||
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
if ($entityDefs->hasRelation($link)) {
|
||||
$relationType = $entityDefs->getRelation($link)->getType();
|
||||
|
||||
$foreignEntityType = $entityDefs->getRelation($link)->hasForeignEntityType() ?
|
||||
$entityDefs->getRelation($link)->getForeignEntityType() : null;
|
||||
|
||||
$foreignEntityDefs = $this->entityManager->getDefs()->getEntity($foreignEntityType);
|
||||
|
||||
$foreignFieldType = $foreignEntityDefs->hasField($field) ?
|
||||
$foreignEntityDefs->getField($field)->getType() : null;
|
||||
|
||||
if ($foreignEntityDefs->hasRelation($field)) {
|
||||
$foreignRelationType = $foreignEntityDefs->getRelation($field)->getType();
|
||||
|
||||
if (
|
||||
(
|
||||
$relationType === Entity::BELONGS_TO ||
|
||||
$relationType === Entity::HAS_ONE
|
||||
) &&
|
||||
$foreignRelationType === Entity::BELONGS_TO
|
||||
) {
|
||||
$queryBuilder
|
||||
->select($item . 'Id')
|
||||
->group($item . 'Id');
|
||||
|
||||
$skipSelect = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($function && in_array($foreignFieldType, ['datetime', 'datetimeOptional'])) {
|
||||
$tzOffset = (string) $this->getTimeZoneOffset();
|
||||
|
||||
if ($tzOffset) {
|
||||
$skipSelect = true;
|
||||
|
||||
$groupBy = "$function:TZ:($link.$field,$tzOffset)";
|
||||
|
||||
$queryBuilder
|
||||
->select($groupBy)
|
||||
->group($groupBy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->handleLeftJoins($item, $entityType, $queryBuilder, true);
|
||||
|
||||
if ($skipSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
->select($item)
|
||||
->group($item);
|
||||
}
|
||||
|
||||
private function handleLeftJoins(
|
||||
string $item,
|
||||
string $entityType,
|
||||
SelectBuilder $queryBuilder,
|
||||
bool $skipDistinct = false
|
||||
): void {
|
||||
|
||||
if (str_contains($item, ':')) {
|
||||
$argumentList = QueryComposerUtil::getAllAttributesFromComplexExpression($item);
|
||||
|
||||
foreach ($argumentList as $argument) {
|
||||
$this->handleLeftJoins($argument, $entityType, $queryBuilder, $skipDistinct);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType);
|
||||
|
||||
if (str_contains($item, '.')) {
|
||||
[$relation,] = explode('.', $item);
|
||||
|
||||
if ($queryBuilder->hasLeftJoinAlias($relation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder->leftJoin($relation);
|
||||
|
||||
if (!$entityDefs->hasRelation($relation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relationType = $entityDefs->getRelation($relation)->getType();
|
||||
|
||||
if (
|
||||
!$skipDistinct &&
|
||||
in_array($relationType, [
|
||||
Entity::HAS_MANY,
|
||||
Entity::MANY_MANY,
|
||||
Entity::HAS_CHILDREN,
|
||||
])
|
||||
) {
|
||||
// @todo Remove when v8.5 is min. supported.
|
||||
$queryBuilder->distinct();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$entityDefs->hasAttribute($item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$attributeDefs = $entityDefs->getAttribute($item);
|
||||
|
||||
if ($attributeDefs->getType() === Entity::FOREIGN) {
|
||||
$relation = $attributeDefs->getParam('relation');
|
||||
|
||||
if ($relation && !$queryBuilder->hasLeftJoinAlias($relation)) {
|
||||
$queryBuilder->leftJoin($relation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $columns
|
||||
*/
|
||||
public function handleColumns(array $columns, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
if (!$entityType) {
|
||||
throw new LogicException("No from.");
|
||||
}
|
||||
|
||||
foreach ($columns as $item) {
|
||||
$this->handleColumnsItem($item, $entityType, $queryBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Use the selectDefs attribute dependency map.
|
||||
*/
|
||||
private function handleColumnsItem(
|
||||
string $item,
|
||||
string $entityType,
|
||||
SelectBuilder $queryBuilder
|
||||
): void {
|
||||
|
||||
$columnData = $this->gridHelper->getDataFromColumnName($entityType, $item);
|
||||
|
||||
if ($columnData->function && !$columnData->link && $columnData->field) {
|
||||
$this->addSelect($item, $queryBuilder);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($columnData->link) {
|
||||
$this->handleLeftJoins($item, $entityType, $queryBuilder);
|
||||
|
||||
if (in_array($columnData->fieldType, ['link', 'file', 'image'])) {
|
||||
$this->addSelect($item . 'Id', $queryBuilder);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addSelect($item, $queryBuilder);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_contains($item, ':') && str_contains($item, '.')) {
|
||||
$this->handleLeftJoins($item, $entityType, $queryBuilder);
|
||||
}
|
||||
|
||||
$type = $columnData->fieldType;
|
||||
|
||||
if (in_array($type, ['link', 'file', 'image', 'linkOne'])) {
|
||||
$this->addSelect($item . 'Name', $queryBuilder);
|
||||
$this->addSelect($item . 'Id', $queryBuilder);
|
||||
|
||||
if (!$queryBuilder->hasLeftJoinAlias($item)) {
|
||||
$queryBuilder->leftJoin($item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'linkParent') {
|
||||
$this->addSelect($item . 'Type', $queryBuilder);
|
||||
$this->addSelect($item . 'Id', $queryBuilder);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'currency') {
|
||||
$this->addSelect($item, $queryBuilder);
|
||||
$this->addSelect($item . 'Currency', $queryBuilder);
|
||||
$this->addSelect($item . 'Converted', $queryBuilder);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'duration') {
|
||||
$start = $this->metadata->get(['entityDefs', $entityType, 'fields', $item, 'start']);
|
||||
$end = $this->metadata->get(['entityDefs', $entityType , 'fields', $item, 'end']);
|
||||
|
||||
$this->addSelect($start, $queryBuilder);
|
||||
$this->addSelect($end, $queryBuilder);
|
||||
$this->addSelect($item, $queryBuilder);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'personName') {
|
||||
$this->addSelect($item, $queryBuilder);
|
||||
$this->addSelect('first' . ucfirst($item), $queryBuilder);
|
||||
$this->addSelect('last' . ucfirst($item), $queryBuilder);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'address') {
|
||||
$pList = ['city', 'country', 'postalCode', 'street', 'state'];
|
||||
|
||||
foreach ($pList as $p) {
|
||||
$this->addSelect($item . ucfirst($p), $queryBuilder);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'datetimeOptional') {
|
||||
$this->addSelect($item, $queryBuilder);
|
||||
$this->addSelect($item . 'Date', $queryBuilder);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === 'linkMultiple' || $type === 'attachmentMultiple') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addSelect($item, $queryBuilder);
|
||||
}
|
||||
|
||||
private function isInSelect(string $item, SelectBuilder $queryBuilder): bool
|
||||
{
|
||||
$currentList = array_map(
|
||||
function (Selection $selection): string {
|
||||
return $selection->getExpression()->getValue();
|
||||
},
|
||||
$queryBuilder->build()->getSelect()
|
||||
);
|
||||
|
||||
return in_array($item, $currentList);
|
||||
}
|
||||
|
||||
private function addSelect(string $item, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
if ($this->isInSelect($item, $queryBuilder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$alias = $this->gridUtil->sanitizeSelectAlias($item);
|
||||
|
||||
$queryBuilder->select($item, $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $orderBy
|
||||
*/
|
||||
public function handleOrderBy(array $orderBy, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
foreach ($orderBy as $item) {
|
||||
$this->handleOrderByItem($item, $entityType, $queryBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleOrderByItem(string $item, string $entityType, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
if (str_contains($item, 'LIST:')) {
|
||||
// @todo Check is actual as processed afterwards.
|
||||
|
||||
$orderBy = substr($item, 5);
|
||||
|
||||
if (str_contains($orderBy, '.')) {
|
||||
[$rel, $field] = explode('.', $orderBy);
|
||||
|
||||
if (!$entityDefs->hasRelation($rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relationDefs = $entityDefs->getRelation($rel);
|
||||
|
||||
$foreignEntityType = $relationDefs->hasForeignEntityType() ?
|
||||
$relationDefs->getForeignEntityType() : null;
|
||||
|
||||
if (!$foreignEntityType) {
|
||||
return;
|
||||
}
|
||||
|
||||
$optionList = $this->metadata
|
||||
->get(['entityDefs', $foreignEntityType, 'fields', $field, 'options']) ?? [];
|
||||
}
|
||||
else {
|
||||
$optionList = $this->metadata->get(['entityDefs', $entityType, 'fields', $orderBy, 'options']) ?? [];
|
||||
}
|
||||
|
||||
if (!$optionList) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder->order(
|
||||
Order::createByPositionInList(Expression::column($orderBy), $optionList)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_contains($item, 'ASC:')) {
|
||||
$orderBy = substr($item, 4);
|
||||
$order = 'ASC';
|
||||
}
|
||||
else if (str_contains($item, 'DESC:')) {
|
||||
$orderBy = substr($item, 5);
|
||||
$order = 'DESC';
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
$field = $orderBy;
|
||||
$orderEntityType = $entityType;
|
||||
$link = null;
|
||||
|
||||
if (str_contains($orderBy, '.')) {
|
||||
[$link, $field] = explode('.', $orderBy);
|
||||
|
||||
if (!$entityDefs->hasRelation($link)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relationDefs = $entityDefs->getRelation($link);
|
||||
|
||||
$orderEntityType = $relationDefs->hasForeignEntityType() ?
|
||||
$relationDefs->getForeignEntityType() : null;
|
||||
|
||||
if (!$orderEntityType) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($orderEntityType);
|
||||
|
||||
$fieldType = $entityDefs->hasField($field) ?
|
||||
$entityDefs->getField($field)->getType() : null;
|
||||
|
||||
if (in_array($fieldType, ['link', 'file', 'image'])) {
|
||||
/*if ($link) {
|
||||
continue;
|
||||
}*/
|
||||
|
||||
// MariaDB issue with ONLY_FULL_GROUP_BY.
|
||||
/*$orderBy = $orderBy . 'Name';
|
||||
|
||||
if (!in_array($orderBy, $params['select'])) {
|
||||
$params['select'][] = $orderBy;
|
||||
}*/
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($fieldType === 'linkParent') {
|
||||
if ($link) {
|
||||
return;
|
||||
}
|
||||
|
||||
$orderBy = $orderBy . 'Type';
|
||||
}
|
||||
|
||||
if (!$this->isInSelect($orderBy, $queryBuilder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryBuilder->order($orderBy, $order);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function handleFiltersWhere(
|
||||
WhereItem $whereItem,
|
||||
SelectBuilder $queryBuilder/*,
|
||||
bool $isGrid = false*/
|
||||
): void {
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
if (!$entityType) {
|
||||
throw new LogicException("No from.");
|
||||
}
|
||||
|
||||
// Supposed to be applied by the scanner.
|
||||
//$this->applyLeftJoinsFromWhere($whereItem, $queryBuilder);
|
||||
|
||||
$params = $this->supportsHasManySubQuery() ?
|
||||
new Converter\Params(useSubQueryIfMany: true) : null;
|
||||
|
||||
$whereClause = $this->createConverter($entityType)
|
||||
->convert($queryBuilder, $whereItem, $params);
|
||||
|
||||
$queryBuilder->where($whereClause);
|
||||
|
||||
/*if (!$isGrid) {
|
||||
// Distinct is already supposed to be applied by the scanner.
|
||||
$this->applyDistinctFromWhere($whereItem, $queryBuilder);
|
||||
}*/
|
||||
}
|
||||
|
||||
private function supportsHasManySubQuery(): bool
|
||||
{
|
||||
return class_exists("Espo\\Core\\Select\\Where\\Converter\\Params");
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function handleFiltersHaving(
|
||||
WhereItem $havingItem,
|
||||
SelectBuilder $queryBuilder,
|
||||
bool $isGrid = false
|
||||
): void {
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
if (!$entityType) {
|
||||
throw new LogicException("No from.");
|
||||
}
|
||||
|
||||
if ($havingItem->getItemList() === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$converter = $this->createConverter($entityType);
|
||||
|
||||
if ($isGrid) {
|
||||
// Supposed to be applied by the scanner.
|
||||
//$this->applyLeftJoinsFromWhere($havingItem, $queryBuilder);
|
||||
|
||||
$havingClause = $converter->convert($queryBuilder, $havingItem);
|
||||
|
||||
$queryBuilder->having($havingClause);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$subQueryBuilder = SelectBuilder::create()
|
||||
->from($entityType, lcfirst($entityType))
|
||||
->select('id')
|
||||
->group('id');
|
||||
|
||||
$havingClause = $converter->convert($subQueryBuilder, $havingItem);
|
||||
|
||||
$subQueryBuilder->having($havingClause);
|
||||
|
||||
// Supposed to be applied by the scanner.
|
||||
//$this->applyLeftJoinsFromWhere($havingItem, $subQueryBuilder);
|
||||
|
||||
$queryBuilder->where(['id=s' => $subQueryBuilder->build()->getRaw()]);
|
||||
}
|
||||
|
||||
/*public function applyLeftJoinsFromWhere(WhereItem $item, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
if (!$entityType) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
//if ($queryBuilder->build()->isDistinct()) {
|
||||
// return;
|
||||
//}
|
||||
|
||||
if (in_array($item->getType(), [self::WHERE_TYPE_OR, self::WHERE_TYPE_AND])) {
|
||||
foreach ($item->getItemList() as $listItem) {
|
||||
$this->applyLeftJoinsFromWhere($listItem, $queryBuilder);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$item->getAttribute()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->handleLeftJoins($item->getAttribute(), $entityType, $queryBuilder, true);
|
||||
}*/
|
||||
|
||||
/**
|
||||
* @deprecated As of v3.4.7.
|
||||
* @todo Remove when v8.5 is min. supported.
|
||||
*/
|
||||
public function applyDistinctFromWhere(WhereItem $item, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
if ($this->supportsHasManySubQuery()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($queryBuilder->build()->isDistinct()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
if (!$entityType) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
if (in_array($item->getType(), [self::WHERE_TYPE_OR, self::WHERE_TYPE_AND])) {
|
||||
foreach ($item->getItemList() as $listItem) {
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->applyDistinctFromWhere($listItem, $queryBuilder);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$item->getAttribute()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->handleDistinct($item->getAttribute(), $entityType, $queryBuilder);
|
||||
}
|
||||
|
||||
private function handleDistinct(string $item, string $entityType, SelectBuilder $queryBuilder): void
|
||||
{
|
||||
if (str_contains($item, ':')) {
|
||||
$argumentList = QueryComposerUtil::getAllAttributesFromComplexExpression($item);
|
||||
|
||||
foreach ($argumentList as $argument) {
|
||||
$this->handleDistinct($argument, $entityType, $queryBuilder);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!str_contains($item, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$relation,] = explode('.', $item);
|
||||
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
if (!$entityDefs->hasRelation($relation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relationsDefs = $entityDefs->getRelation($relation);
|
||||
|
||||
if (in_array($relationsDefs->getType(), [Entity::HAS_MANY, Entity::MANY_MANY])) {
|
||||
$queryBuilder->distinct();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|int
|
||||
*/
|
||||
private function getTimeZoneOffset()
|
||||
{
|
||||
$timeZone = $this->config->get('timeZone', 'UTC');
|
||||
|
||||
if ($timeZone === 'UTC') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$dateTimeZone = new DateTimeZone($timeZone);
|
||||
$dateTime = new DateTime('now', $dateTimeZone);
|
||||
|
||||
$dateTime->modify('first day of january');
|
||||
$tzOffset = $dateTimeZone->getOffset($dateTime) / 3600;
|
||||
}
|
||||
catch (Exception) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $tzOffset;
|
||||
}
|
||||
|
||||
private function createConverter(string $entityType): Converter
|
||||
{
|
||||
return $this->converterFactory->create($entityType, $this->user);
|
||||
}
|
||||
}
|
||||
435
custom/Espo/Modules/Advanced/Tools/Report/SendingService.php
Normal file
435
custom/Espo/Modules/Advanced/Tools/Report/SendingService.php
Normal file
@@ -0,0 +1,435 @@
|
||||
<?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\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\SearchParams;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Entities\Email;
|
||||
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\GridType\Result as GridResult;
|
||||
use Espo\Modules\Advanced\Tools\Report\Jobs\Send;
|
||||
use Espo\Modules\Advanced\Tools\Report\ListType\Result as ListResult;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Export\Export;
|
||||
use Espo\Tools\Export\Params as ExportToolParams;
|
||||
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use stdClass;
|
||||
|
||||
class SendingService
|
||||
{
|
||||
private const LIST_REPORT_MAX_SIZE = 3000;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private User $user,
|
||||
private Metadata $metadata,
|
||||
private Config $config,
|
||||
private FieldUtil $fieldUtil,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private EmailBuilder $emailBuilder
|
||||
) {}
|
||||
|
||||
private function getSendingListMaxCount(): int
|
||||
{
|
||||
return $this->config->get('reportSendingListMaxCount', self::LIST_REPORT_MAX_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function getEmailAttributes(string $id, ?WhereItem $where = null, ?User $user = null): array
|
||||
{
|
||||
/** @var ?ReportEntity $report */
|
||||
$report = $this->entityManager->getEntityById(ReportEntity::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$report) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$service = $this->injectableFactory->create(Service::class);
|
||||
|
||||
if ($report->getType() === ReportEntity::TYPE_LIST) {
|
||||
$searchParams = SearchParams::create()
|
||||
->withMaxSize($this->getSendingListMaxCount());
|
||||
|
||||
$orderByList = $report->getOrderByList();
|
||||
|
||||
if ($orderByList) {
|
||||
$arr = explode(':', $orderByList);
|
||||
|
||||
/**
|
||||
* @var 'ASC'|'DESC' $orderDirection
|
||||
* @noinspection PhpRedundantVariableDocTypeInspection
|
||||
*/
|
||||
$orderDirection = strtoupper($arr[0]);
|
||||
|
||||
$searchParams = $searchParams
|
||||
->withOrderBy($arr[1])
|
||||
->withOrder($orderDirection);
|
||||
}
|
||||
|
||||
if ($where) {
|
||||
$searchParams = $searchParams->withWhere($where);
|
||||
}
|
||||
|
||||
$result = $service->runList($id, $searchParams, $user);
|
||||
} else {
|
||||
$result = $service->runGrid($id, $where, $user);
|
||||
}
|
||||
|
||||
$reportResult = $result;
|
||||
|
||||
if ($result instanceof ListResult) {
|
||||
$reportResult = [];
|
||||
|
||||
foreach ($result->getCollection() as $e) {
|
||||
$reportResult[] = get_object_vars($e->getValueMap());
|
||||
}
|
||||
}
|
||||
|
||||
$data = (object) [
|
||||
'userId' => $user ? $user->getId() : $this->user->getId(),
|
||||
];
|
||||
|
||||
if ($reportResult instanceof ListResult) {
|
||||
// For static analysis.
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$this->emailBuilder->buildEmailData($data, $reportResult, $report);
|
||||
|
||||
$attachmentId = $this->getExportAttachmentId($report, $result, $where, $user);
|
||||
|
||||
if ($attachmentId) {
|
||||
$data->attachmentId = $attachmentId;
|
||||
|
||||
$attachment = $this->entityManager->getEntityById(Attachment::ENTITY_TYPE, $attachmentId);
|
||||
|
||||
if ($attachment) {
|
||||
$attachment->set([
|
||||
'role' => 'Attachment',
|
||||
'parentType' => Email::ENTITY_TYPE,
|
||||
'relatedId' => $id,
|
||||
'relatedType' => ReportEntity::ENTITY_TYPE,
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
}
|
||||
}
|
||||
|
||||
$userIdList = $report->getLinkMultipleIdList('emailSendingUsers');
|
||||
|
||||
$nameHash = (object) [];
|
||||
|
||||
$toArr = [];
|
||||
|
||||
if ($report->get('emailSendingInterval') && count($userIdList)) {
|
||||
$userList = $this
|
||||
->entityManager
|
||||
->getRDBRepositoryByClass(User::class)
|
||||
->where(['id' => $userIdList])
|
||||
->find();
|
||||
|
||||
foreach ($userList as $user) {
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
|
||||
if ($emailAddress) {
|
||||
$toArr[] = $emailAddress;
|
||||
$nameHash->$emailAddress = $user->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$attributes = [
|
||||
'isHtml' => true,
|
||||
'body' => $data->emailBody,
|
||||
'name' => $data->emailSubject,
|
||||
'nameHash' => $nameHash,
|
||||
'to' => implode(';', $toArr),
|
||||
];
|
||||
|
||||
if ($attachmentId) {
|
||||
$attributes['attachmentsIds'] = [$attachmentId];
|
||||
|
||||
$attachment = $this->entityManager->getEntityById(Attachment::ENTITY_TYPE, $attachmentId);
|
||||
|
||||
if ($attachment) {
|
||||
$attributes['attachmentsNames'] = [
|
||||
$attachmentId => $attachment->get('name')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GridResult|ListResult $result
|
||||
*/
|
||||
public function getExportAttachmentId(
|
||||
ReportEntity $report,
|
||||
$result,
|
||||
?WhereItem $where = null,
|
||||
?User $user = null
|
||||
): ?string {
|
||||
|
||||
$entityType = $report->getTargetEntityType();
|
||||
|
||||
if ($report->getType() === ReportEntity::TYPE_LIST) {
|
||||
if (!$result instanceof ListResult) {
|
||||
throw new RuntimeException("Bad result.");
|
||||
}
|
||||
|
||||
$fieldList = $report->getColumns();
|
||||
|
||||
foreach ($fieldList as $key => $field) {
|
||||
if (strpos($field, '.')) {
|
||||
$fieldList[$key] = 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;
|
||||
}
|
||||
}
|
||||
|
||||
$exportParams = ExportToolParams::create($entityType)
|
||||
->withFieldList($fieldList)
|
||||
->withAttributeList($attributeList)
|
||||
->withFormat('xlsx')
|
||||
->withName($report->getName())
|
||||
->withFileName($report->getName() . ' ' . date('Y-m-d'));
|
||||
|
||||
$export = $this->injectableFactory->create(Export::class);
|
||||
|
||||
try {
|
||||
return $export
|
||||
->setParams($exportParams)
|
||||
->setCollection($result->getCollection())
|
||||
->run()
|
||||
->getAttachmentId();
|
||||
} catch (Exception $e) {
|
||||
$GLOBALS['log']->error("Report export fail, {$report->getId()}: {$e->getMessage()}");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$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";
|
||||
|
||||
try {
|
||||
$service = $this->injectableFactory->create(GridExportService::class);
|
||||
|
||||
$contents = $service->buildXlsxContents($report->getId(), $where, $user);
|
||||
|
||||
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getNew();
|
||||
|
||||
$attachment
|
||||
->setName($fileName)
|
||||
->setType($mimeType)
|
||||
->setContents($contents)
|
||||
->setRole(Attachment::ROLE_ATTACHMENT);
|
||||
|
||||
$attachment->set('parentType', Email::ENTITY_TYPE);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
return $attachment->getId();
|
||||
} catch (Exception $e) {
|
||||
$GLOBALS['log']->error("Report export fail, {$report->getId()}: {$e->getMessage()}");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function scheduleEmailSending(): void
|
||||
{
|
||||
$reports = $this->entityManager
|
||||
->getRDBRepositoryByClass(ReportEntity::class)
|
||||
->where([[
|
||||
'AND' => [
|
||||
['emailSendingInterval!=' => ''],
|
||||
['emailSendingInterval!=' => NULL],
|
||||
]]
|
||||
])
|
||||
->find();
|
||||
|
||||
$utcTZ = new DateTimeZone('UTC');
|
||||
$now = new DateTime("now", $utcTZ);
|
||||
|
||||
$defaultTz = $this->config->get('timeZone');
|
||||
|
||||
$espoTimeZone = new DateTimeZone($defaultTz);
|
||||
|
||||
foreach ($reports as $report) {
|
||||
$scheduleSending = false;
|
||||
$check = false;
|
||||
|
||||
$nowCopy = clone $now;
|
||||
$nowCopy->setTimezone($espoTimeZone);
|
||||
|
||||
switch ($report->get('emailSendingInterval')) {
|
||||
case 'Daily':
|
||||
$check = true;
|
||||
|
||||
break;
|
||||
|
||||
case 'Weekly':
|
||||
$check = (strpos($report->get('emailSendingSettingWeekdays'), $nowCopy->format('w')) !== false);
|
||||
|
||||
break;
|
||||
|
||||
case 'Monthly':
|
||||
$check =
|
||||
$nowCopy->format('j') == $report->get('emailSendingSettingDay') ||
|
||||
$nowCopy->format('j') == $nowCopy->format('t') &&
|
||||
$nowCopy->format('t') < $report->get('emailSendingSettingDay');
|
||||
|
||||
break;
|
||||
|
||||
case 'Yearly':
|
||||
$check =
|
||||
(
|
||||
$nowCopy->format('j') == $report->get('emailSendingSettingDay') ||
|
||||
$nowCopy->format('j') == $nowCopy->format('t') &&
|
||||
$nowCopy->format('t') < $report->get('emailSendingSettingDay')
|
||||
) &&
|
||||
$nowCopy->format('n') == $report->get('emailSendingSettingMonth');
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($check) {
|
||||
if ($report->get('emailSendingLastDateSent')) {
|
||||
$lastSent = new DateTime($report->get('emailSendingLastDateSent'), $utcTZ);
|
||||
$lastSent->setTimezone($espoTimeZone);
|
||||
|
||||
$nowCopy->setTime(0, 0);
|
||||
$lastSent->setTime(0, 0);
|
||||
$diff = $lastSent->diff($nowCopy);
|
||||
|
||||
if (!empty($diff)) {
|
||||
$dayDiff = (int) ((($diff->invert) ? '-' : '') . $diff->days);
|
||||
|
||||
if ($dayDiff > 0) {
|
||||
$scheduleSending = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$scheduleSending = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$scheduleSending) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$report->loadLinkMultipleField('emailSendingUsers');
|
||||
$users = $report->get('emailSendingUsersIds');
|
||||
|
||||
if (empty($users)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$executeTime = clone $now;
|
||||
|
||||
if ($report->get('emailSendingTime')) {
|
||||
$time = explode(':', $report->get('emailSendingTime'));
|
||||
|
||||
if (empty($time[0]) || $time[0] < 0 || $time[0] > 23) {
|
||||
$time[0] = 0;
|
||||
}
|
||||
|
||||
if (empty($time[1]) || $time[1] < 0 || $time[1] > 59) {
|
||||
$time[1] = 0;
|
||||
}
|
||||
|
||||
$executeTime->setTimezone($espoTimeZone);
|
||||
$executeTime->setTime(intval($time[0]), intval($time[1]));
|
||||
$executeTime->setTimezone($utcTZ);
|
||||
}
|
||||
|
||||
$report->set('emailSendingLastDateSent', $executeTime->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT));
|
||||
|
||||
$this->entityManager->saveEntity($report);
|
||||
|
||||
foreach ($users as $userId) {
|
||||
$jobEntity = $this->entityManager->getEntity(Job::ENTITY_TYPE);
|
||||
|
||||
$data = (object) [
|
||||
'userId' => $userId,
|
||||
'reportId' => $report->getId(),
|
||||
];
|
||||
|
||||
$jobEntity->set([
|
||||
'name' => Send::class,
|
||||
'className' => Send::class,
|
||||
'executeTime' => $executeTime->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($jobEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass $data
|
||||
* @param GridResult|array<int, mixed> $result
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildData($data, $result, ReportEntity $report): void
|
||||
{
|
||||
$this->emailBuilder->buildEmailData($data, $result, $report, true);
|
||||
}
|
||||
}
|
||||
1058
custom/Espo/Modules/Advanced/Tools/Report/Service.php
Normal file
1058
custom/Espo/Modules/Advanced/Tools/Report/Service.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
<?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\Acl;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Modules\Advanced\Entities\Report;
|
||||
use Espo\Modules\Crm\Entities\TargetList;
|
||||
use Espo\Modules\Crm\Tools\TargetList\RecordService;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class TargetListSyncService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
private Metadata $metadata,
|
||||
private ServiceContainer $serviceContainer,
|
||||
private Service $service,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function syncTargetListWithReportsById(string $targetListId): void
|
||||
{
|
||||
/** @var ?TargetList $targetList */
|
||||
$targetList = $this->entityManager->getEntity(TargetList::ENTITY_TYPE, $targetListId);
|
||||
|
||||
if (!$targetList) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$targetList->get('syncWithReportsEnabled')) {
|
||||
throw new Error("Sync with reports not enabled for target list $targetListId.");
|
||||
}
|
||||
|
||||
$this->syncTargetListWithReports($targetList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function syncTargetListWithReports(TargetList $targetList): void
|
||||
{
|
||||
if (!$this->acl->checkEntityEdit($targetList)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
/** @var \Espo\Modules\Crm\Tools\TargetList\RecordService $targetListService */
|
||||
$targetListService = class_exists("Espo\\Modules\\Crm\\Tools\\TargetList\\RecordService") ?
|
||||
$this->injectableFactory->create(RecordService::class) :
|
||||
$this->serviceContainer->get(TargetList::ENTITY_TYPE);
|
||||
|
||||
if ($targetList->get('syncWithReportsUnlink')) {
|
||||
$linkList = $this->metadata->get(['scopes', 'TargetList', 'targetLinkList']) ??
|
||||
['contacts', 'leads', 'accounts', 'users'];
|
||||
|
||||
foreach ($linkList as $link) {
|
||||
$targetListService->unlinkAll($targetList->getId(), $link);
|
||||
}
|
||||
}
|
||||
|
||||
$reportList = $this->entityManager
|
||||
->getRDBRepository(TargetList::ENTITY_TYPE)
|
||||
->getRelation($targetList, 'syncWithReports')
|
||||
->find();
|
||||
|
||||
foreach ($reportList as $report) {
|
||||
$this->populateTargetList($report->getId(), $targetList->getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function populateTargetList(string $id, string $targetListId): void
|
||||
{
|
||||
/** @var ?Report $report */
|
||||
$report = $this->entityManager->getEntityById(Report::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$report) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityRead($report)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$targetList = $this->entityManager->getEntity(TargetList::ENTITY_TYPE, $targetListId);
|
||||
|
||||
if (!$targetList) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityEdit($targetList)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($report->getType() !== Report::TYPE_LIST) {
|
||||
throw new Error("Report is not of 'List' type.");
|
||||
}
|
||||
|
||||
$entityType = $report->getTargetEntityType();
|
||||
|
||||
$linkList = $this->metadata->get(['scopes', 'TargetList', 'targetLinkList']) ??
|
||||
['contacts', 'leads', 'accounts', 'users'];
|
||||
|
||||
$link = null;
|
||||
|
||||
foreach ($linkList as $itemLink) {
|
||||
if (
|
||||
$this->metadata->get(['entityDefs', 'TargetList', 'links', $itemLink, 'entity']) === $entityType
|
||||
) {
|
||||
$link = $itemLink;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$link) {
|
||||
throw new Error("Not supported entity type '$entityType' for target list sync.");
|
||||
}
|
||||
|
||||
$query = $this->service
|
||||
->prepareSelectBuilder($report)
|
||||
->build();
|
||||
|
||||
$this->entityManager
|
||||
->getRDBRepository(TargetList::ENTITY_TYPE)
|
||||
->getRelation($targetList, $link)
|
||||
->massRelate($query);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user