Initial commit
This commit is contained in:
43
custom/Espo/Modules/Advanced/Core/App/Job.php
Normal file
43
custom/Espo/Modules/Advanced/Core/App/Job.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Core\App;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Espo\Core\Job\JobDataLess;
|
||||
use Espo\Core\Job\JobSchedulerFactory;
|
||||
|
||||
class Job implements JobDataLess
|
||||
{
|
||||
public function __construct(
|
||||
private JobSchedulerFactory $jobSchedulerFactory
|
||||
) {}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$runAt = (new DateTimeImmutable())
|
||||
->modify('+ 1 day')
|
||||
->setTime(rand(0, 5), rand(0, 59));
|
||||
|
||||
$this->jobSchedulerFactory
|
||||
->create()
|
||||
->setClassName(JobRunner::class)
|
||||
->setTime($runAt)
|
||||
->schedule();
|
||||
}
|
||||
}
|
||||
165
custom/Espo/Modules/Advanced/Core/App/JobRunner.php
Normal file
165
custom/Espo/Modules/Advanced/Core/App/JobRunner.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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\Core\App;
|
||||
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\Extension;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
|
||||
class JobRunner implements Job
|
||||
{
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var string */
|
||||
private $fieldStatus;
|
||||
|
||||
/** @var string */
|
||||
private $fieldMessage;
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private EntityManager $entityManager
|
||||
) {
|
||||
$this->name = base64_decode('QWR2YW5jZWQgUGFjaw==');
|
||||
$this->fieldStatus = base64_decode('bGljZW5zZVN0YXR1cw==');
|
||||
$this->fieldMessage = base64_decode('bGljZW5zZVN0YXR1c01lc3NhZ2U=');
|
||||
}
|
||||
|
||||
public function run(Data $data): void
|
||||
{
|
||||
/** @var ?Extension $current */
|
||||
$current = $this->entityManager
|
||||
->getRDBRepository(Extension::ENTITY_TYPE)
|
||||
->where([
|
||||
'name' => $this->name,
|
||||
])
|
||||
->order('createdAt', true)
|
||||
->findOne();
|
||||
|
||||
$responseData = $this->validate($this->getData($current));
|
||||
|
||||
$status = $responseData['status'] ?? null;
|
||||
$statusMessage = $responseData['statusMessage'] ?? null;
|
||||
|
||||
if (!$status) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$current->has($this->fieldStatus)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$current->get($this->fieldStatus) == $status &&
|
||||
$current->get($this->fieldMessage) == $statusMessage
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current->set([
|
||||
$this->fieldStatus => $status,
|
||||
$this->fieldMessage => $statusMessage,
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($current, [
|
||||
'skipAll' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getData(?Extension $current): array
|
||||
{
|
||||
$query = SelectBuilder::create()
|
||||
->from(Extension::ENTITY_TYPE)
|
||||
->select(['createdAt'])
|
||||
->withDeleted()
|
||||
->build();
|
||||
|
||||
/** @var ?Extension $first */
|
||||
$first = $this->entityManager
|
||||
->getRDBRepository(Extension::ENTITY_TYPE)
|
||||
->clone($query)
|
||||
->where(['name' => $this->name])
|
||||
->order('createdAt')
|
||||
->findOne();
|
||||
|
||||
return [
|
||||
'id' => base64_decode('MTliYzg2YTY4YTdiYjAxZjQ1OGNiMzkxZDQzYTkyMTI='),
|
||||
'name' => $this->name,
|
||||
'version' => $current?->get('version'),
|
||||
'updatedAt' => $current?->get('createdAt'),
|
||||
'installedAt' => $first?->get('createdAt'),
|
||||
'site' => $this->config->get('siteUrl'),
|
||||
'instanceId' => $this->config->get('instanceId'),
|
||||
'espoVersion' => $this->config->get('version'),
|
||||
'applicationName' => $this->config->get('applicationName'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
private function validate(array $data): ?array
|
||||
{
|
||||
if (!function_exists('curl_version')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
/**
|
||||
* @var string $payload
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
$payload = json_encode($data);
|
||||
|
||||
/** @phpstan-ignore-next-line argument.type */
|
||||
curl_setopt($ch, CURLOPT_URL, base64_decode('aHR0cHM6Ly9zLmVzcG9jcm0uY29tL2xpY2Vuc2Uv'));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($payload),
|
||||
]);
|
||||
|
||||
/** @var string $result */
|
||||
$result = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode === 200) {
|
||||
return json_decode($result, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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\Core\AppParams;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Select\SelectBuilderFactory;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowchart;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class FlowchartEntityTypeList
|
||||
{
|
||||
private Acl $acl;
|
||||
private EntityManager $entityManager;
|
||||
private SelectBuilderFactory $selectBuilderFactory;
|
||||
|
||||
public function __construct(
|
||||
Acl $acl,
|
||||
EntityManager $entityManager,
|
||||
SelectBuilderFactory $selectBuilderFactory
|
||||
) {
|
||||
$this->acl = $acl;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->selectBuilderFactory = $selectBuilderFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function get(): array
|
||||
{
|
||||
if (!$this->acl->checkScope(BpmnProcess::ENTITY_TYPE, Acl\Table::ACTION_CREATE)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$this->acl->checkScope(BpmnFlowchart::ENTITY_TYPE, Acl\Table::ACTION_READ)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [];
|
||||
|
||||
$query = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from(BpmnFlowchart::ENTITY_TYPE)
|
||||
->withAccessControlFilter()
|
||||
->build();
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository(BpmnFlowchart::ENTITY_TYPE)
|
||||
->clone($query)
|
||||
->select(['targetType'])
|
||||
->group('targetType')
|
||||
->where(['isActive' => true])
|
||||
->find();
|
||||
|
||||
foreach ($collection as $item) {
|
||||
$list[] = $item->get('targetType');
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
2393
custom/Espo/Modules/Advanced/Core/Bpmn/BpmnManager.php
Normal file
2393
custom/Espo/Modules/Advanced/Core/Bpmn/BpmnManager.php
Normal file
File diff suppressed because it is too large
Load Diff
237
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Activity.php
Normal file
237
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Activity.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Utils\DateTime;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\ORM\Collection;
|
||||
use Throwable;
|
||||
|
||||
abstract class Activity extends Base
|
||||
{
|
||||
/** @var string[] */
|
||||
protected array $pendingBoundaryTypeList = [
|
||||
'eventIntermediateConditionalBoundary',
|
||||
'eventIntermediateTimerBoundary',
|
||||
'eventIntermediateSignalBoundary',
|
||||
'eventIntermediateMessageBoundary',
|
||||
];
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function beforeProcess(): void
|
||||
{
|
||||
$this->prepareBoundary();
|
||||
$this->refreshFlowNode();
|
||||
$this->refreshTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function prepareBoundary(): void
|
||||
{
|
||||
$boundaryFlowNodeList = [];
|
||||
|
||||
$attachedElementIdList = $this->getProcess()->getAttachedToFlowNodeElementIdList($this->getFlowNode());
|
||||
|
||||
foreach ($attachedElementIdList as $id) {
|
||||
$item = $this->getProcess()->getElementDataById($id);
|
||||
|
||||
if (!in_array($item->type, $this->pendingBoundaryTypeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$boundaryFlowNode = $this->getManager()->prepareFlow(
|
||||
$this->getTarget(),
|
||||
$this->getProcess(),
|
||||
$id,
|
||||
$this->getFlowNode()->get('id'),
|
||||
$this->getFlowNode()->getElementType()
|
||||
);
|
||||
|
||||
if ($boundaryFlowNode) {
|
||||
$boundaryFlowNodeList[] = $boundaryFlowNode;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($boundaryFlowNodeList as $boundaryFlowNode) {
|
||||
$this->getManager()->processPreparedFlowNode($this->getTarget(), $boundaryFlowNode, $this->getProcess());
|
||||
}
|
||||
}
|
||||
|
||||
public function isProcessable(): bool
|
||||
{
|
||||
return $this->getFlowNode()->getStatus() === BpmnFlowNode::STATUS_CREATED;
|
||||
}
|
||||
|
||||
protected function isInNormalFlow(): bool
|
||||
{
|
||||
return !$this->getFlowNode()->getElementDataItemValue('isForCompensation');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function setFailedWithError(?string $errorCode = null, ?string $errorMessage = null): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_FAILED);
|
||||
$flowNode->set([
|
||||
'processedAt' => date(DateTime::SYSTEM_DATE_TIME_FORMAT),
|
||||
]);
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$this->getManager()->endProcessWithError($this->getProcess(), $errorCode, $errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function setFailed(): void
|
||||
{
|
||||
$this->rejectPendingBoundaryFlowNodes();
|
||||
|
||||
$errorCode = $this->getFlowNode()->getDataItemValue('errorCode');
|
||||
$errorMessage = $this->getFlowNode()->getDataItemValue('errorMessage');
|
||||
|
||||
$boundaryErrorFlowNode = $this->getManager()
|
||||
->prepareBoundaryErrorFlowNode($this->getFlowNode(), $this->getProcess(), $errorCode);
|
||||
|
||||
if (!$boundaryErrorFlowNode) {
|
||||
$this->setFailedWithError($errorCode, $errorMessage);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$boundaryErrorFlowNode->setDataItemValue('code', $errorCode);
|
||||
$boundaryErrorFlowNode->setDataItemValue('message', $errorMessage);
|
||||
|
||||
$this->getEntityManager()->saveEntity($boundaryErrorFlowNode);
|
||||
|
||||
parent::setFailed();
|
||||
|
||||
$this->getManager()->processPreparedFlowNode($this->getTarget(), $boundaryErrorFlowNode, $this->getProcess());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function setFailedWithException(Throwable $e): void
|
||||
{
|
||||
$errorCode = (string) $e->getCode();
|
||||
|
||||
$this->rejectPendingBoundaryFlowNodes();
|
||||
|
||||
$boundaryErrorFlowNode = $this->getManager()
|
||||
->prepareBoundaryErrorFlowNode($this->getFlowNode(), $this->getProcess(), $errorCode);
|
||||
|
||||
if (!$boundaryErrorFlowNode) {
|
||||
$this->setFailedWithError($errorCode, $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$boundaryErrorFlowNode->setDataItemValue('code', $errorCode);
|
||||
$boundaryErrorFlowNode->setDataItemValue('message', $e->getMessage());
|
||||
|
||||
$this->getEntityManager()->saveEntity($boundaryErrorFlowNode);
|
||||
|
||||
parent::setFailed();
|
||||
|
||||
$this->getManager()->processPreparedFlowNode($this->getTarget(), $boundaryErrorFlowNode, $this->getProcess());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<BpmnFlowNode>
|
||||
*/
|
||||
protected function getPendingBoundaryFlowNodeList(): Collection
|
||||
{
|
||||
return $this->getEntityManager()
|
||||
->getRDBRepositoryByClass(BpmnFlowNode::class)
|
||||
->where([
|
||||
'elementType' => $this->pendingBoundaryTypeList,
|
||||
'processId' => $this->getProcess()->get('id'),
|
||||
'status' => [
|
||||
BpmnFlowNode::STATUS_CREATED,
|
||||
BpmnFlowNode::STATUS_PENDING,
|
||||
],
|
||||
'previousFlowNodeId' => $this->getFlowNode()->get('id'),
|
||||
])
|
||||
->find();
|
||||
}
|
||||
|
||||
protected function rejectPendingBoundaryFlowNodes(): void
|
||||
{
|
||||
$boundaryNodeList = $this->getPendingBoundaryFlowNodeList();
|
||||
|
||||
foreach ($boundaryNodeList as $boundaryNode) {
|
||||
$boundaryNode->set('status', BpmnFlowNode::STATUS_REJECTED);
|
||||
|
||||
$this->getEntityManager()->saveEntity($boundaryNode);
|
||||
}
|
||||
}
|
||||
|
||||
protected function setRejected(): void
|
||||
{
|
||||
$this->rejectPendingBoundaryFlowNodes();
|
||||
|
||||
parent::setRejected();
|
||||
}
|
||||
|
||||
protected function setProcessed(): void
|
||||
{
|
||||
$this->rejectPendingBoundaryFlowNodes();
|
||||
|
||||
parent::setProcessed();
|
||||
}
|
||||
|
||||
protected function setInterrupted(): void
|
||||
{
|
||||
$this->rejectPendingBoundaryFlowNodes();
|
||||
|
||||
parent::setInterrupted();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getReturnVariableList(): array
|
||||
{
|
||||
$newVariableList = [];
|
||||
|
||||
$variableList = $this->getAttributeValue('returnVariableList') ?? [];
|
||||
|
||||
foreach ($variableList as $variable) {
|
||||
if (!$variable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($variable[0] === '$') {
|
||||
$variable = substr($variable, 1);
|
||||
}
|
||||
|
||||
$newVariableList[] = $variable;
|
||||
}
|
||||
|
||||
return $newVariableList;
|
||||
}
|
||||
}
|
||||
611
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Base.php
Normal file
611
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Base.php
Normal file
@@ -0,0 +1,611 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Formula\Manager as FormulaManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\WebSocket\Submission;
|
||||
use Espo\Modules\Advanced\Core\Bpmn\BpmnManager;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Container;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\ORM\Entity;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
abstract class Base
|
||||
{
|
||||
/**
|
||||
* // Do not rename the parameters. Mapped by name.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Container $container,
|
||||
protected BpmnManager $manager,
|
||||
protected Entity $target,
|
||||
protected BpmnFlowNode $flowNode,
|
||||
protected BpmnProcess $process,
|
||||
) {}
|
||||
|
||||
protected function getContainer(): Container
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
protected function getLog(): Log
|
||||
{
|
||||
return $this->container->getByClass(Log::class);
|
||||
}
|
||||
|
||||
protected function getEntityManager(): EntityManager
|
||||
{
|
||||
return $this->container->getByClass(EntityManager::class);
|
||||
}
|
||||
|
||||
protected function getMetadata(): Metadata
|
||||
{
|
||||
return $this->container->getByClass(Metadata::class);
|
||||
}
|
||||
|
||||
protected function getFormulaManager(): FormulaManager
|
||||
{
|
||||
return $this->getContainer()->getByClass(FormulaManager::class);
|
||||
}
|
||||
|
||||
private function getWebSocketSubmission(): Submission
|
||||
{
|
||||
// Important: The service was not bound prior v9.0.0.
|
||||
/** @var Submission */
|
||||
return $this->getContainer()->get('webSocketSubmission');
|
||||
}
|
||||
|
||||
private function getConfig(): Config
|
||||
{
|
||||
return $this->getContainer()->getByClass(Config::class);
|
||||
}
|
||||
|
||||
protected function getProcess(): BpmnProcess
|
||||
{
|
||||
return $this->process;
|
||||
}
|
||||
|
||||
protected function getFlowNode(): BpmnFlowNode
|
||||
{
|
||||
return $this->flowNode;
|
||||
}
|
||||
|
||||
protected function getTarget(): Entity
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
protected function getManager(): BpmnManager
|
||||
{
|
||||
return $this->manager;
|
||||
}
|
||||
|
||||
protected function refresh(): void
|
||||
{
|
||||
$this->refreshFlowNode();
|
||||
$this->refreshProcess();
|
||||
$this->refreshTarget();
|
||||
}
|
||||
|
||||
protected function refreshFlowNode(): void
|
||||
{
|
||||
if (!$this->flowNode->hasId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$flowNode = $this->getEntityManager()->getEntityById(BpmnFlowNode::ENTITY_TYPE, $this->flowNode->getId());
|
||||
|
||||
if ($flowNode) {
|
||||
$this->flowNode->set($flowNode->getValueMap());
|
||||
$this->flowNode->setAsFetched();
|
||||
}
|
||||
}
|
||||
|
||||
protected function refreshProcess(): void
|
||||
{
|
||||
if (!$this->process->hasId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$process = $this->getEntityManager()->getEntityById(BpmnProcess::ENTITY_TYPE, $this->process->getId());
|
||||
|
||||
if ($process) {
|
||||
$this->process->set($process->getValueMap());
|
||||
$this->process->setAsFetched();
|
||||
}
|
||||
}
|
||||
|
||||
protected function saveProcess(): void
|
||||
{
|
||||
$this->getEntityManager()->saveEntity($this->getProcess(), ['silent' => true]);
|
||||
}
|
||||
|
||||
protected function saveFlowNode(): void
|
||||
{
|
||||
$this->getEntityManager()->saveEntity($this->getFlowNode());
|
||||
|
||||
$this->submitWebSocket();
|
||||
}
|
||||
|
||||
private function submitWebSocket(): void
|
||||
{
|
||||
if (!$this->getConfig()->get('useWebSocket')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->getProcess()->hasId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityType = $this->getProcess()->getEntityType();
|
||||
$id = $this->getProcess()->getId();
|
||||
|
||||
$topic = "recordUpdate.$entityType.$id";
|
||||
|
||||
$this->getWebSocketSubmission()->submit($topic);
|
||||
}
|
||||
|
||||
protected function refreshTarget(): void
|
||||
{
|
||||
if (!$this->target->hasId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = $this->getEntityManager()->getEntityById($this->target->getEntityType(), $this->target->getId());
|
||||
|
||||
if ($target) {
|
||||
$this->target->set($target->getValueMap());
|
||||
$this->target->setAsFetched();
|
||||
}
|
||||
}
|
||||
|
||||
public function isProcessable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function beforeProcess(): void
|
||||
{}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
abstract public function process(): void;
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
throw new Error("BPM Flow: Can't proceed element ". $flowNode->getElementType() . " " .
|
||||
$flowNode->get('elementId') . " in flowchart " . $flowNode->getFlowchartId() . ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getElementId(): string
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
$elementId = $flowNode->getElementId();
|
||||
|
||||
if (!$elementId) {
|
||||
throw new Error("BPM Flow: No id for element " . $flowNode->getElementType() .
|
||||
" in flowchart " . $flowNode->getFlowchartId() . ".");
|
||||
}
|
||||
|
||||
return $elementId;
|
||||
}
|
||||
|
||||
protected function isInNormalFlow(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function hasNextElementId(): bool
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$item = $flowNode->getElementData();
|
||||
$nextElementIdList = $item->nextElementIdList;
|
||||
|
||||
if (!count($nextElementIdList)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getNextElementId(): ?string
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
if (!$this->hasNextElementId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = $flowNode->getElementData();
|
||||
$nextElementIdList = $item->nextElementIdList;
|
||||
|
||||
return $nextElementIdList[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAttributeValue(string $name)
|
||||
{
|
||||
$item = $this->getFlowNode()->getElementData();
|
||||
|
||||
if (!property_exists($item, $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $item->$name;
|
||||
}
|
||||
|
||||
protected function getVariables(): stdClass
|
||||
{
|
||||
return $this->getProcess()->getVariables() ?? (object) [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Revise the need.
|
||||
*/
|
||||
protected function getClonedVariables(): stdClass
|
||||
{
|
||||
return clone $this->getVariables();
|
||||
}
|
||||
|
||||
protected function getVariablesForFormula(): stdClass
|
||||
{
|
||||
$variables = $this->getClonedVariables();
|
||||
|
||||
$variables->__createdEntitiesData = $this->getCreatedEntitiesData();
|
||||
$variables->__processEntity = $this->getProcess();
|
||||
$variables->__targetEntity = $this->getTarget();
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
protected function addCreatedEntityDataToVariables(stdClass $variables): void
|
||||
{
|
||||
$variables->__createdEntitiesData = $this->getCreatedEntitiesData();
|
||||
}
|
||||
|
||||
protected function sanitizeVariables(stdClass $variables): void
|
||||
{
|
||||
unset($variables->__createdEntitiesData);
|
||||
unset($variables->__processEntity);
|
||||
unset($variables->__targetEntity);
|
||||
}
|
||||
|
||||
protected function setProcessed(): void
|
||||
{
|
||||
$this->getFlowNode()->set([
|
||||
'status' => BpmnFlowNode::STATUS_PROCESSED,
|
||||
'processedAt' => date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)
|
||||
]);
|
||||
$this->saveFlowNode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function setInterrupted(): void
|
||||
{
|
||||
$this->getFlowNode()->set([
|
||||
'status' => BpmnFlowNode::STATUS_INTERRUPTED,
|
||||
]);
|
||||
$this->saveFlowNode();
|
||||
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function setFailed(): void
|
||||
{
|
||||
$this->getFlowNode()->set([
|
||||
'status' => BpmnFlowNode::STATUS_FAILED,
|
||||
'processedAt' => date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
|
||||
]);
|
||||
$this->saveFlowNode();
|
||||
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function setRejected(): void
|
||||
{
|
||||
$this->getFlowNode()->set([
|
||||
'status' => BpmnFlowNode::STATUS_REJECTED,
|
||||
]);
|
||||
$this->saveFlowNode();
|
||||
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function fail(): void
|
||||
{
|
||||
$this->setFailed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function interrupt(): void
|
||||
{
|
||||
$this->setInterrupted();
|
||||
}
|
||||
|
||||
public function cleanupInterrupted(): void
|
||||
{}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function complete(): void
|
||||
{
|
||||
throw new Error("Can't complete " . $this->getFlowNode()->getElementType() . ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|false|null $divergentFlowNodeId
|
||||
* @throws Error
|
||||
*/
|
||||
protected function prepareNextFlowNode(
|
||||
?string $nextElementId = null,
|
||||
$divergentFlowNodeId = false
|
||||
): ?BpmnFlowNode {
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
if (!$nextElementId) {
|
||||
if (!$this->isInNormalFlow()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->hasNextElementId()) {
|
||||
$this->endProcessFlow();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$nextElementId = $this->getNextElementId();
|
||||
}
|
||||
|
||||
if ($divergentFlowNodeId === false) {
|
||||
$divergentFlowNodeId = $flowNode->getDivergentFlowNodeId();
|
||||
}
|
||||
|
||||
return $this->getManager()->prepareFlow(
|
||||
$this->getTarget(),
|
||||
$this->getProcess(),
|
||||
$nextElementId,
|
||||
$flowNode->get('id'),
|
||||
$flowNode->getElementType(),
|
||||
$divergentFlowNodeId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|false|null $divergentFlowNodeId
|
||||
* @throws Error
|
||||
*/
|
||||
protected function processNextElement(
|
||||
?string $nextElementId = null,
|
||||
$divergentFlowNodeId = false,
|
||||
bool $dontSetProcessed = false
|
||||
): ?BpmnFlowNode {
|
||||
|
||||
$nextFlowNode = $this->prepareNextFlowNode($nextElementId, $divergentFlowNodeId);
|
||||
|
||||
if (!$dontSetProcessed) {
|
||||
$this->setProcessed();
|
||||
}
|
||||
|
||||
if ($nextFlowNode) {
|
||||
$this->getManager()->processPreparedFlowNode(
|
||||
$this->getTarget(),
|
||||
$nextFlowNode,
|
||||
$this->getProcess()
|
||||
);
|
||||
}
|
||||
|
||||
return $nextFlowNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function processPreparedNextFlowNode(BpmnFlowNode $flowNode): void
|
||||
{
|
||||
$this->getManager()->processPreparedFlowNode($this->getTarget(), $flowNode, $this->getProcess());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function endProcessFlow(): void
|
||||
{
|
||||
$this->getManager()->endProcessFlow($this->getFlowNode(), $this->getProcess());
|
||||
}
|
||||
|
||||
protected function getCreatedEntitiesData(): stdClass
|
||||
{
|
||||
$createdEntitiesData = $this->getProcess()->get('createdEntitiesData');
|
||||
|
||||
if (!$createdEntitiesData) {
|
||||
$createdEntitiesData = (object) [];
|
||||
}
|
||||
|
||||
return $createdEntitiesData;
|
||||
}
|
||||
|
||||
protected function getCreatedEntity(string $target): ?Entity
|
||||
{
|
||||
$createdEntitiesData = $this->getCreatedEntitiesData();
|
||||
|
||||
if (str_starts_with($target, 'created:')) {
|
||||
$alias = substr($target, 8);
|
||||
} else {
|
||||
$alias = $target;
|
||||
}
|
||||
|
||||
if (!property_exists($createdEntitiesData, $alias)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (empty($createdEntitiesData->$alias->entityId) || empty($createdEntitiesData->$alias->entityType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entityType = $createdEntitiesData->$alias->entityType;
|
||||
$entityId = $createdEntitiesData->$alias->entityId;
|
||||
|
||||
$entity = $this->getEntityManager()->getEntityById($entityType, $entityId);
|
||||
|
||||
if (!$entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$entity instanceof Entity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getSpecificTarget(?string $target): ?Entity
|
||||
{
|
||||
$entity = $this->getTarget();
|
||||
|
||||
if (!$target || $target == 'targetEntity') {
|
||||
return $entity;
|
||||
}
|
||||
|
||||
if (str_starts_with($target, 'created:')) {
|
||||
return $this->getCreatedEntity($target);
|
||||
}
|
||||
|
||||
if (str_starts_with($target, 'record:')) {
|
||||
$entityType = substr($target, 7);
|
||||
|
||||
$targetIdExpression = $this->getAttributeValue('targetIdExpression');
|
||||
|
||||
if (!$targetIdExpression) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_ends_with($targetIdExpression, ';')) {
|
||||
$targetIdExpression = substr($targetIdExpression, 0, -1);
|
||||
}
|
||||
|
||||
$id = $this->getFormulaManager()->run(
|
||||
$targetIdExpression,
|
||||
$this->getTarget(),
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if (!$id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_string($id)) {
|
||||
throw new Error("BPM: Target-ID evaluated not to string.");
|
||||
}
|
||||
|
||||
$entity = $this->getEntityManager()->getEntityById($entityType, $id);
|
||||
|
||||
if (!$entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$entity instanceof Entity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
if (str_starts_with($target, 'link:')) {
|
||||
$link = substr($target, 5);
|
||||
|
||||
$linkList = explode('.', $link);
|
||||
|
||||
$pointerEntity = $entity;
|
||||
|
||||
$notFound = false;
|
||||
|
||||
foreach ($linkList as $link) {
|
||||
$type = $this->getMetadata()
|
||||
->get(['entityDefs', $pointerEntity->getEntityType(), 'links', $link, 'type']);
|
||||
|
||||
if (empty($type)) {
|
||||
$notFound = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$pointerEntity = $this->getEntityManager()
|
||||
->getRDBRepository($pointerEntity->getEntityType())
|
||||
->getRelation($pointerEntity, $link)
|
||||
->findOne();
|
||||
|
||||
if (!$pointerEntity instanceof Entity) {
|
||||
$notFound = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$notFound) {
|
||||
if ($pointerEntity && !$pointerEntity instanceof Entity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return $pointerEntity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
779
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/CallActivity.php
Normal file
779
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/CallActivity.php
Normal file
@@ -0,0 +1,779 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaException;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowchart;
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Throwable;
|
||||
use stdClass;
|
||||
use Traversable;
|
||||
|
||||
class CallActivity extends Activity
|
||||
{
|
||||
protected const MAX_INSTANCE_COUNT = 20;
|
||||
protected const CALLABLE_TYPE_PROCESS = 'Process';
|
||||
|
||||
/**
|
||||
* @throws FormulaException
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(): void
|
||||
{
|
||||
if ($this->isMultiInstance()) {
|
||||
$this->processMultiInstance();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$callableType = $this->getAttributeValue('callableType');
|
||||
|
||||
if (!$callableType) {
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($callableType === self::CALLABLE_TYPE_PROCESS) {
|
||||
$this->processProcess();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->fail();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaException
|
||||
* @throws Error
|
||||
*/
|
||||
protected function processProcess(): void
|
||||
{
|
||||
$target = $this->getNewTargetEntity();
|
||||
|
||||
if (!$target) {
|
||||
$this->getLog()->notice("BPM Call Activity: Could not get target for sub-process.");
|
||||
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$flowchartId = $this->getAttributeValue('flowchartId');
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$variables = $this->getPrepareVariables();
|
||||
|
||||
/** @var BpmnProcess $subProcess */
|
||||
$subProcess = $this->getEntityManager()->createEntity(BpmnProcess::ENTITY_TYPE, [
|
||||
'status' => BpmnProcess::STATUS_CREATED,
|
||||
'flowchartId' => $flowchartId,
|
||||
'targetId' => $target->getId(),
|
||||
'targetType' => $target->getEntityType(),
|
||||
'parentProcessId' => $this->getProcess()->getId(),
|
||||
'parentProcessFlowNodeId' => $flowNode->getId(),
|
||||
'rootProcessId' => $this->getProcess()->getRootProcessId(),
|
||||
'assignedUserId' => $this->getProcess()->getAssignedUser()?->getId(),
|
||||
'teamsIds' => $this->getProcess()->getTeams()->getIdList(),
|
||||
'variables' => $variables,
|
||||
], [
|
||||
'skipCreatedBy' => true,
|
||||
'skipModifiedBy' => true,
|
||||
'skipStartProcessFlow' => true,
|
||||
]);
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
$flowNode->setDataItemValue('subProcessId', $subProcess->getId());
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
try {
|
||||
$this->getManager()->startCreatedProcess($subProcess);
|
||||
} catch (Throwable $e) {
|
||||
$message = "BPM Call Activity: Starting sub-process failure, {$subProcess->getId()}. {$e->getMessage()}";
|
||||
|
||||
$this->getLog()->error($message, ['exception' => $e]);
|
||||
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(): void
|
||||
{
|
||||
if (!$this->isInNormalFlow()) {
|
||||
$this->setProcessed();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$subProcessId = $this->getFlowNode()->getDataItemValue('subProcessId');
|
||||
|
||||
if (!$subProcessId) {
|
||||
$this->processNextElement();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$subProcess = $this->getEntityManager()->getEntityById(BpmnProcess::ENTITY_TYPE, $subProcessId);
|
||||
|
||||
if (!$subProcess) {
|
||||
$this->processNextElement();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$spCreatedEntitiesData = $subProcess->get('createdEntitiesData') ?? (object) [];
|
||||
$createdEntitiesData = $this->getCreatedEntitiesData();
|
||||
$spVariables = $subProcess->get('variables') ?? (object) [];
|
||||
$variables = $this->getVariables();
|
||||
|
||||
$isUpdated = false;
|
||||
|
||||
foreach (get_object_vars($spCreatedEntitiesData) as $key => $value) {
|
||||
if (!isset($createdEntitiesData->$key)) {
|
||||
$createdEntitiesData->$key = $value;
|
||||
|
||||
$isUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
$variableList = $this->getReturnVariableList();
|
||||
|
||||
if ($this->isMultiInstance()) {
|
||||
$variableList = [];
|
||||
|
||||
$returnCollectionVariable = $this->getReturnCollectionVariable();
|
||||
|
||||
if ($returnCollectionVariable !== null) {
|
||||
$variables->$returnCollectionVariable = $spVariables->outputCollection;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($variableList as $variable) {
|
||||
$variables->$variable = $spVariables->$variable ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
$isUpdated ||
|
||||
count($variableList) ||
|
||||
$this->isMultiInstance()
|
||||
) {
|
||||
$this->refreshProcess();
|
||||
|
||||
$this->getProcess()->set('createdEntitiesData', $createdEntitiesData);
|
||||
$this->getProcess()->set('variables', $variables);
|
||||
|
||||
$this->getEntityManager()->saveEntity($this->getProcess());
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
protected function getReturnCollectionVariable(): ?string
|
||||
{
|
||||
$variable = $this->getAttributeValue('returnCollectionVariable');
|
||||
|
||||
if (!$variable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($variable[0] === '$') {
|
||||
$variable = substr($variable, 1);
|
||||
}
|
||||
|
||||
return $variable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaException
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getNewTargetEntity(): ?Entity
|
||||
{
|
||||
$target = $this->getAttributeValue('target');
|
||||
|
||||
return $this->getSpecificTarget($target);
|
||||
}
|
||||
|
||||
protected function isMultiInstance(): bool
|
||||
{
|
||||
return (bool) $this->getAttributeValue('isMultiInstance');
|
||||
}
|
||||
|
||||
protected function isSequential(): bool
|
||||
{
|
||||
return (bool) $this->getAttributeValue('isSequential');
|
||||
}
|
||||
|
||||
protected function getLoopCollectionExpression(): ?string
|
||||
{
|
||||
$expression = $this->getAttributeValue('loopCollectionExpression');
|
||||
|
||||
if (!$expression) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$expression = trim($expression, " \t\n\r");
|
||||
|
||||
if (str_ends_with($expression, ';')) {
|
||||
$expression = substr($expression, 0, -1);
|
||||
}
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
protected function getConfig(): Config
|
||||
{
|
||||
return $this->getContainer()->getByClass(Config::class);
|
||||
}
|
||||
|
||||
protected function getMaxInstanceCount(): int
|
||||
{
|
||||
return $this->getConfig()->get('bpmnSubProcessInstanceMaxCount', self::MAX_INSTANCE_COUNT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaException
|
||||
* @throws Error
|
||||
*/
|
||||
protected function processMultiInstance(): void
|
||||
{
|
||||
$loopCollectionExpression = $this->getLoopCollectionExpression();
|
||||
|
||||
if (!$loopCollectionExpression) {
|
||||
throw new Error("BPM Sub-Process: No loop-collection-expression.");
|
||||
}
|
||||
|
||||
$loopCollection = $this->getFormulaManager()->run(
|
||||
$loopCollectionExpression,
|
||||
$this->getTarget(),
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if (!is_iterable($loopCollection)) {
|
||||
throw new Error("BPM Sub-Process: Loop-collection-expression evaluated to a non-iterable value.");
|
||||
}
|
||||
|
||||
if ($loopCollection instanceof Traversable) {
|
||||
$loopCollection = iterator_to_array($loopCollection);
|
||||
}
|
||||
|
||||
$maxCount = $this->getMaxInstanceCount();
|
||||
|
||||
$returnVariableList = $this->getReturnVariableList();
|
||||
|
||||
$outputCollection = [];
|
||||
|
||||
for ($i = 0; $i < count($loopCollection); $i++) {
|
||||
$outputItem = (object) [];
|
||||
|
||||
foreach ($returnVariableList as $variable) {
|
||||
$outputItem->$variable = null;
|
||||
}
|
||||
|
||||
$outputCollection[] = $outputItem;
|
||||
}
|
||||
|
||||
if ($maxCount < count($loopCollection)) {
|
||||
$loopCollection = array_slice($loopCollection, 0, $maxCount);
|
||||
}
|
||||
|
||||
$count = count($loopCollection);
|
||||
|
||||
$flowchart = $this->createMultiInstanceFlowchart($count);
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
$variables = $this->getClonedVariables();
|
||||
|
||||
$this->refreshProcess();
|
||||
|
||||
$variables->inputCollection = $loopCollection;
|
||||
$variables->outputCollection = $outputCollection;
|
||||
|
||||
/** @var BpmnProcess $subProcess */
|
||||
$subProcess = $this->getEntityManager()->createEntity(BpmnProcess::ENTITY_TYPE, [
|
||||
'status' => BpmnFlowNode::STATUS_CREATED,
|
||||
'targetId' => $this->getTarget()->getId(),
|
||||
'targetType' => $this->getTarget()->getEntityType(),
|
||||
'parentProcessId' => $this->getProcess()->getId(),
|
||||
'parentProcessFlowNodeId' => $flowNode->getId(),
|
||||
'rootProcessId' => $this->getProcess()->getRootProcessId(),
|
||||
'assignedUserId' => $this->getProcess()->getAssignedUser()?->getId(),
|
||||
'teamsIds' => $this->getProcess()->getTeams()->getIdList(),
|
||||
'variables' => $variables,
|
||||
'createdEntitiesData' => clone $this->getCreatedEntitiesData(),
|
||||
],
|
||||
[
|
||||
'skipCreatedBy' => true,
|
||||
'skipModifiedBy' => true,
|
||||
'skipStartProcessFlow' => true,
|
||||
]);
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
$flowNode->setDataItemValue('subProcessId', $subProcess->getId());
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
try {
|
||||
$this->getManager()->startCreatedProcess($subProcess, $flowchart);
|
||||
} catch (Throwable $e) {
|
||||
$message = "BPM Sub-Process: Starting sub-process failure, {$subProcess->getId()}. {$e->getMessage()}";
|
||||
|
||||
$this->getLog()->error($message, ['exception' => $e]);
|
||||
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected function createMultiInstanceFlowchart(int $count): BpmnFlowchart
|
||||
{
|
||||
/** @var BpmnFlowchart $flowchart */
|
||||
$flowchart = $this->getEntityManager()->getNewEntity(BpmnFlowchart::ENTITY_TYPE);
|
||||
|
||||
$dataList = $this->isSequential() ?
|
||||
$this->generateSequentialMultiInstanceDataList($count) :
|
||||
$this->generateParallelMultiInstanceDataList($count);
|
||||
|
||||
$eData = Helper::getElementsDataFromFlowchartData((object) [
|
||||
'list' => $dataList,
|
||||
]);
|
||||
|
||||
$name = $this->isSequential() ?
|
||||
'Sequential Multi-Instance' :
|
||||
'Parallel Multi-Instance';
|
||||
|
||||
$flowchart->set([
|
||||
'targetType' => $this->getTarget()->getEntityType(),
|
||||
'data' => (object) [
|
||||
'createdEntitiesData' => clone $this->getCreatedEntitiesData(),
|
||||
'list' => $dataList,
|
||||
],
|
||||
'elementsDataHash' => $eData['elementsDataHash'],
|
||||
'teamsIds' => $this->getProcess()->getLinkMultipleIdList('teams'),
|
||||
'assignedUserId' => $this->getProcess()->get('assignedUserId'),
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
return $flowchart;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function generateParallelMultiInstanceDataList(int $count): array
|
||||
{
|
||||
$dataList = [];
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$dataList = array_merge($dataList, $this->generateMultiInstanceIteration($i));
|
||||
}
|
||||
|
||||
return array_merge($dataList, $this->generateMultiInstanceCompensation($count, $dataList));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function generateSequentialMultiInstanceDataList(int $count): array
|
||||
{
|
||||
$dataList = [];
|
||||
$groupList = [];
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$groupList[] = $this->generateMultiInstanceIteration($i);
|
||||
}
|
||||
|
||||
foreach ($groupList as $i => $itemList) {
|
||||
$dataList = array_merge($dataList, $itemList);
|
||||
|
||||
if ($i == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$previousItemList = $groupList[$i - 1];
|
||||
|
||||
$dataList[] = (object) [
|
||||
'type' => 'flow',
|
||||
'id' => self::generateElementId(),
|
||||
'startId' => $previousItemList[2]->id,
|
||||
'endId' => $itemList[0]->id,
|
||||
'startDirection' => 'r',
|
||||
];
|
||||
}
|
||||
|
||||
return array_merge($dataList, $this->generateMultiInstanceCompensation(0, $dataList));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function generateMultiInstanceIteration(int $loopCounter): array
|
||||
{
|
||||
$dataList = [];
|
||||
|
||||
$x = 100;
|
||||
$y = ($loopCounter + 1) * 130;
|
||||
|
||||
if ($this->isSequential()) {
|
||||
$x = $x + ($loopCounter * 400);
|
||||
$y = 50;
|
||||
}
|
||||
|
||||
$initElement = (object) [
|
||||
'type' => 'taskScript',
|
||||
'id' => self::generateElementId(),
|
||||
'formula' =>
|
||||
"\$loopCounter = $loopCounter;\n" .
|
||||
"\$inputItem = array\\at(\$inputCollection, $loopCounter);\n",
|
||||
'center' => (object) [
|
||||
'x' => $x,
|
||||
'y' => $y,
|
||||
],
|
||||
'text' => $loopCounter . ' init',
|
||||
];
|
||||
|
||||
$subProcessElement = $this->generateSubProcessMultiInstance($loopCounter, $x, $y);
|
||||
|
||||
$endScript = "\$outputItem = array\\at(\$outputCollection, $loopCounter);\n";
|
||||
|
||||
foreach ($this->getReturnVariableList() as $variable) {
|
||||
$endScript .= "object\set(\$outputItem, '$variable', \$$variable);\n";
|
||||
}
|
||||
|
||||
$endElement = (object) [
|
||||
'type' => 'taskScript',
|
||||
'id' => self::generateElementId(),
|
||||
'formula' => $endScript,
|
||||
'center' => (object) [
|
||||
'x' => $x + 250,
|
||||
'y' => $y,
|
||||
],
|
||||
'text' => $loopCounter . ' out',
|
||||
];
|
||||
|
||||
$dataList[] = $initElement;
|
||||
$dataList[] = $subProcessElement;
|
||||
$dataList[] = $endElement;
|
||||
|
||||
$dataList[] = (object) [
|
||||
'type' => 'flow',
|
||||
'id' => self::generateElementId(),
|
||||
'startId' => $initElement->id,
|
||||
'endId' => $subProcessElement->id,
|
||||
'startDirection' => 'r',
|
||||
];
|
||||
|
||||
$dataList[] = (object) [
|
||||
'type' => 'flow',
|
||||
'id' => self::generateElementId(),
|
||||
'startId' => $subProcessElement->id,
|
||||
'endId' => $endElement->id,
|
||||
'startDirection' => 'r',
|
||||
];
|
||||
|
||||
foreach ($this->generateBoundaryMultiInstance($subProcessElement) as $item) {
|
||||
$dataList[] = $item;
|
||||
}
|
||||
|
||||
return $dataList;
|
||||
}
|
||||
|
||||
protected function generateSubProcessMultiInstance(int $loopCounter, int $x, int $y): stdClass
|
||||
{
|
||||
return (object) [
|
||||
'type' => $this->getAttributeValue('type'),
|
||||
'id' => self::generateElementId(),
|
||||
'center' => (object) [
|
||||
'x' => $x + 125,
|
||||
'y' => $y,
|
||||
],
|
||||
'callableType' => $this->getAttributeValue('callableType'),
|
||||
'flowchartId' => $this->getAttributeValue('flowchartId'),
|
||||
'flowchartName' => $this->getAttributeValue('flowchartName'),
|
||||
'returnVariableList' => $this->getAttributeValue('returnVariableList'),
|
||||
'target' => $this->getAttributeValue('target'),
|
||||
'targetType' => $this->getAttributeValue('targetType'),
|
||||
'targetIdExpression' => $this->getAttributeValue('targetIdExpression'),
|
||||
'isMultiInstance' => false,
|
||||
'isSequential' => false,
|
||||
'loopCollectionExpression' => null,
|
||||
'text' => (string) $loopCounter,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function generateBoundaryMultiInstance(stdClass $element): array
|
||||
{
|
||||
$dataList = [];
|
||||
|
||||
$attachedElementIdList = array_filter(
|
||||
$this->getProcess()->getAttachedToFlowNodeElementIdList($this->getFlowNode()),
|
||||
function (string $id): bool {
|
||||
$data = $this->getProcess()->getElementDataById($id);
|
||||
|
||||
return in_array(
|
||||
$data->type,
|
||||
[
|
||||
'eventIntermediateErrorBoundary',
|
||||
'eventIntermediateEscalationBoundary',
|
||||
'eventIntermediateCompensationBoundary',
|
||||
]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$compensationId = array_values(array_filter(
|
||||
$this->getProcess()->getElementIdList(),
|
||||
function (string $id): bool {
|
||||
$data = $this->getProcess()->getElementDataById($id);
|
||||
|
||||
return ($data->isForCompensation ?? null) === true;
|
||||
}
|
||||
))[0] ?? null;
|
||||
|
||||
foreach ($attachedElementIdList as $i => $id) {
|
||||
$boundaryElementId = self::generateElementId();
|
||||
$throwElementId = self::generateElementId();
|
||||
|
||||
$originalData = $this->getProcess()->getElementDataById($id);
|
||||
|
||||
$o1 = (object) [
|
||||
'type' => $originalData->type,
|
||||
'id' => $boundaryElementId,
|
||||
'attachedToId' => $element->id,
|
||||
'cancelActivity' => $originalData->cancelActivity ?? false,
|
||||
'center' => (object) [
|
||||
'x' => $element->center->x - 20 + $i * 25,
|
||||
'y' => $element->center->y - 35,
|
||||
],
|
||||
'attachPosition' => $originalData->attachPosition,
|
||||
];
|
||||
|
||||
if ($originalData->type === 'eventIntermediateCompensationBoundary') {
|
||||
if (!$compensationId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dataList[] = $o1;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$o2 = (object) [
|
||||
'type' => 'eventEndError',
|
||||
'id' => $throwElementId,
|
||||
'errorCode' => $originalData->errorCode ?? null,
|
||||
'center' => (object) [
|
||||
'x' => $element->center->x - 20 + $i * 25 + 80,
|
||||
'y' => $element->center->y - 35 - 25,
|
||||
],
|
||||
];
|
||||
|
||||
if ($originalData->type === 'eventIntermediateErrorBoundary') {
|
||||
$o2->type = 'eventEndError';
|
||||
$o1->errorCode = $originalData->errorCode ?? null;
|
||||
$o2->errorCode = $originalData->errorCode ?? null;
|
||||
$o1->cancelActivity = true;
|
||||
}
|
||||
else if ($originalData->type === 'eventIntermediateEscalationBoundary') {
|
||||
$o2->type = 'eventEndEscalation';
|
||||
$o1->escalationCode = $originalData->escalationCode ?? null;
|
||||
$o2->escalationCode = $originalData->escalationCode ?? null;
|
||||
}
|
||||
|
||||
$dataList[] = $o1;
|
||||
$dataList[] = $o2;
|
||||
|
||||
$dataList[] = (object) [
|
||||
'type' => 'flow',
|
||||
'id' => self::generateElementId(),
|
||||
'startId' => $boundaryElementId,
|
||||
'endId' => $throwElementId,
|
||||
'startDirection' => 'r',
|
||||
];
|
||||
}
|
||||
|
||||
return $dataList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $currentDataList
|
||||
* @return stdClass[]
|
||||
*/
|
||||
private function generateMultiInstanceCompensation(int $loopCounter, array $currentDataList): array
|
||||
{
|
||||
$x = 150;
|
||||
$y = ($loopCounter + 1) * 100 + 100;
|
||||
|
||||
$internalDataList = $this->generateMultiInstanceCompensationSubProcessDataList();
|
||||
|
||||
$dataList = [];
|
||||
|
||||
$dataList[] = (object) [
|
||||
'type' => 'eventSubProcess',
|
||||
'id' => self::generateElementId(),
|
||||
'center' => (object) [
|
||||
'x' => $x,
|
||||
'y' => $y,
|
||||
],
|
||||
'height' => 100,
|
||||
'width' => 205,
|
||||
'target' => null,
|
||||
'isExpanded' => true,
|
||||
'dataList' => $internalDataList,
|
||||
'eventStartData' => clone $internalDataList[0],
|
||||
];
|
||||
|
||||
$activity = $this->generateMultiInstanceBoundaryCompensationActivity();
|
||||
|
||||
if ($activity) {
|
||||
$activity->center = (object) [
|
||||
'x' => 350,
|
||||
'y' => $y,
|
||||
];
|
||||
|
||||
$dataList[] = $activity;
|
||||
|
||||
foreach ($currentDataList as $item) {
|
||||
if ($item->type === 'eventIntermediateCompensationBoundary') {
|
||||
$dataList[] = (object) [
|
||||
'type' => 'flow',
|
||||
'id' => self::generateElementId(),
|
||||
'startId' => $item->id,
|
||||
'endId' => $activity->id,
|
||||
'startDirection' => 'd',
|
||||
'isAssociation' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dataList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass[]
|
||||
*/
|
||||
private function generateMultiInstanceCompensationSubProcessDataList(): array
|
||||
{
|
||||
$x = 50;
|
||||
$y = 35;
|
||||
|
||||
$dataList = [];
|
||||
|
||||
$dataList[] = (object) [
|
||||
'type' => 'eventStartCompensation',
|
||||
'id' => self::generateElementId(),
|
||||
'center' => (object) [
|
||||
'x' => $x,
|
||||
'y' => $y,
|
||||
],
|
||||
];
|
||||
|
||||
$dataList[] = (object) [
|
||||
'type' => 'eventEndCompensation',
|
||||
'id' => self::generateElementId(),
|
||||
'center' => (object) [
|
||||
'x' => $x + 100,
|
||||
'y' => $y,
|
||||
],
|
||||
'activityId' => null,
|
||||
];
|
||||
|
||||
$dataList[] = (object) [
|
||||
'type' => 'flow',
|
||||
'id' => self::generateElementId(),
|
||||
'startId' => $dataList[0]->id,
|
||||
'endId' => $dataList[1]->id,
|
||||
'startDirection' => 'r',
|
||||
];
|
||||
|
||||
return $dataList;
|
||||
}
|
||||
|
||||
private function generateMultiInstanceBoundaryCompensationActivity(): ?stdClass
|
||||
{
|
||||
/** @var string[] $attachedElementIdList */
|
||||
$attachedElementIdList = array_values(array_filter(
|
||||
$this->getProcess()->getAttachedToFlowNodeElementIdList($this->getFlowNode()),
|
||||
function (string $id): bool {
|
||||
$data = $this->getProcess()->getElementDataById($id);
|
||||
|
||||
return $data->type === 'eventIntermediateCompensationBoundary';
|
||||
}
|
||||
));
|
||||
|
||||
if ($attachedElementIdList === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$boundaryId = $attachedElementIdList[0];
|
||||
|
||||
$compensationId = $this->getProcess()->getElementNextIdList($boundaryId)[0] ?? null;
|
||||
|
||||
if (!$compensationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = $this->getProcess()->getElementDataById($compensationId);
|
||||
|
||||
if (!$item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = ObjectUtil::clone($item);
|
||||
$item->id = $this->generateElementId();
|
||||
|
||||
if ($item->type === 'subProcess') {
|
||||
$item->isExpanded = false;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
protected static function generateElementId(): string
|
||||
{
|
||||
return Util::generateId();
|
||||
}
|
||||
|
||||
protected function getPrepareVariables(): stdClass
|
||||
{
|
||||
$variables = $this->getClonedVariables();
|
||||
|
||||
unset($variables->__caughtErrorCode);
|
||||
unset($variables->__caughtErrorMessage);
|
||||
|
||||
return $variables;
|
||||
}
|
||||
}
|
||||
49
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Event.php
Normal file
49
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Event.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
abstract class Event extends Base
|
||||
{
|
||||
protected function rejectConcurrentPendingFlows(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
if ($flowNode->getPreviousFlowNodeElementType() === 'gatewayEventBased') {
|
||||
/** @var iterable<BpmnFlowNode> $concurrentFlowNodeList */
|
||||
$concurrentFlowNodeList = $this->getEntityManager()
|
||||
->getRDBRepository(BpmnFlowNode::ENTITY_TYPE)
|
||||
->where([
|
||||
'previousFlowNodeElementType' => 'gatewayEventBased',
|
||||
'previousFlowNodeId' => $flowNode->getPreviousFlowNodeId(),
|
||||
'processId' => $flowNode->getProcessId(),
|
||||
'id!=' => $flowNode->get('id'),
|
||||
'status' => BpmnFlowNode::STATUS_PENDING,
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($concurrentFlowNodeList as $concurrentFlowNode) {
|
||||
$concurrentFlowNode->setStatus(BpmnFlowNode::STATUS_REJECTED);
|
||||
|
||||
$this->getEntityManager()->saveEntity($concurrentFlowNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/EventEnd.php
Normal file
31
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/EventEnd.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class EventEnd extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->setProcessed();
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
|
||||
class EventEndCompensation extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
/** @var ?string $activityId */
|
||||
$activityId = $this->getAttributeValue('activityId');
|
||||
|
||||
$process = $this->getProcess();
|
||||
|
||||
if (
|
||||
$this->getProcess()->getParentProcessFlowNodeId() &&
|
||||
$this->getProcess()->getParentProcessId()
|
||||
) {
|
||||
/** @var ?BpmnFlowNode $parentNode */
|
||||
$parentNode = $this->getEntityManager()
|
||||
->getEntityById(BpmnFlowNode::ENTITY_TYPE, $this->getProcess()->getParentProcessFlowNodeId());
|
||||
|
||||
if (!$parentNode) {
|
||||
throw new Error("No parent node.");
|
||||
}
|
||||
|
||||
if ($parentNode->getElementType() === 'eventSubProcess') {
|
||||
/** @var ?BpmnProcess $process */
|
||||
$process = $this->getEntityManager()
|
||||
->getEntityById(BpmnProcess::ENTITY_TYPE, $this->getProcess()->getParentProcessId());
|
||||
|
||||
if (!$process) {
|
||||
throw new Error("No parent process.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$compensationFlowNodeIdList = $this->getManager()->compensate($process, $activityId);
|
||||
|
||||
$this->getFlowNode()->setDataItemValue('compensationFlowNodeIdList', $compensationFlowNodeIdList);
|
||||
$this->saveFlowNode();
|
||||
|
||||
$this->refreshProcess();
|
||||
$this->refreshTarget();
|
||||
|
||||
if ($compensationFlowNodeIdList === [] || $this->isCompensated()) {
|
||||
$this->setProcessed();
|
||||
$this->processAfterProcessed();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getFlowNode()->set('status', BpmnFlowNode::STATUS_PENDING);
|
||||
$this->saveFlowNode();
|
||||
}
|
||||
|
||||
protected function processAfterProcessed(): void
|
||||
{
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
|
||||
public function proceedPending(): void
|
||||
{
|
||||
if (!$this->isCompensated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setProcessed();
|
||||
$this->processAfterProcessed();
|
||||
}
|
||||
|
||||
private function isCompensated(): bool
|
||||
{
|
||||
/** @var string[] $compensationFlowNodeIdList */
|
||||
$compensationFlowNodeIdList = $this->getFlowNode()->getDataItemValue('compensationFlowNodeIdList') ?? [];
|
||||
|
||||
$flowNodes = $this->getEntityManager()
|
||||
->getRDBRepositoryByClass(BpmnFlowNode::class)
|
||||
->where(['id' => $compensationFlowNodeIdList])
|
||||
->find();
|
||||
|
||||
foreach ($flowNodes as $flowNode) {
|
||||
if ($flowNode->getStatus() === BpmnFlowNode::STATUS_PROCESSED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventEndError extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->setProcessed();
|
||||
$this->getManager()->endProcessWithError($this->getProcess(), $this->getAttributeValue('errorCode'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
class EventEndEscalation extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->setProcessed();
|
||||
|
||||
$this->getManager()->escalate($this->getProcess(), $this->getAttributeValue('escalationCode'));
|
||||
|
||||
$this->refreshProcess();
|
||||
$this->refreshTarget();
|
||||
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
class EventEndSignal extends EventSignal
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->setProcessed();
|
||||
|
||||
$signal = $this->getSignal();
|
||||
|
||||
if ($signal) {
|
||||
if (mb_substr($signal, 0, 1) !== '@') {
|
||||
$this->getManager()->broadcastSignal($signal);
|
||||
}
|
||||
} else {
|
||||
$this->getLog()->warning("BPM: eventEndSignal, no signal");
|
||||
}
|
||||
|
||||
$this->refreshProcess();
|
||||
$this->refreshTarget();
|
||||
|
||||
$this->endProcessFlow();
|
||||
|
||||
if ($signal) {
|
||||
if (mb_substr($signal, 0, 1) !== '@') {
|
||||
$this->getSignalManager()->trigger($signal);
|
||||
} else {
|
||||
$this->getSignalManager()->trigger($signal, $this->getTarget());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventEndTerminate extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->setProcessed();
|
||||
$this->getManager()->endProcess($this->getProcess(), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventIntermediateBoundary extends Event
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventIntermediateCompensationThrow extends EventEndCompensation
|
||||
{
|
||||
protected function processAfterProcessed(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class EventIntermediateConditionalBoundary extends EventIntermediateConditionalCatch
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$result = $this->getConditionManager()->check(
|
||||
$this->getTarget(),
|
||||
$this->getAttributeValue('conditionsAll'),
|
||||
$this->getAttributeValue('conditionsAny'),
|
||||
$this->getAttributeValue('conditionsFormula'),
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$cancel = $this->getAttributeValue('cancelActivity');
|
||||
|
||||
if (!$cancel) {
|
||||
$this->createOppositeNode();
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
|
||||
if ($cancel) {
|
||||
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
}
|
||||
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$result = $this->getConditionManager()->check(
|
||||
$this->getTarget(),
|
||||
$this->getAttributeValue('conditionsAll'),
|
||||
$this->getAttributeValue('conditionsAny'),
|
||||
$this->getAttributeValue('conditionsFormula'),
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if ($this->getFlowNode()->getDataItemValue('isOpposite')) {
|
||||
if (!$result) {
|
||||
$this->setProcessed();
|
||||
$this->createOppositeNode(true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
$cancel = $this->getAttributeValue('cancelActivity');
|
||||
|
||||
if (!$cancel) {
|
||||
$this->createOppositeNode();
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
|
||||
if ($cancel) {
|
||||
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function createOppositeNode(bool $isNegative = false): void
|
||||
{
|
||||
/** @var BpmnFlowNode $flowNode */
|
||||
$flowNode = $this->getEntityManager()->getNewEntity(BpmnFlowNode::ENTITY_TYPE);
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
|
||||
|
||||
$flowNode->set([
|
||||
'elementId' => $this->getFlowNode()->getElementId(),
|
||||
'elementType' => $this->getFlowNode()->getElementType(),
|
||||
'elementData' => $this->getFlowNode()->getElementData(),
|
||||
'data' => [
|
||||
'isOpposite' => !$isNegative,
|
||||
],
|
||||
'flowchartId' => $this->getProcess()->getFlowchartId(),
|
||||
'processId' => $this->getProcess()->getId(),
|
||||
'previousFlowNodeElementType' => $this->getFlowNode()->getPreviousFlowNodeElementType(),
|
||||
'previousFlowNodeId' => $this->getFlowNode()->getPreviousFlowNodeId(),
|
||||
'divergentFlowNodeId' => $this->getFlowNode()->getDivergentFlowNodeId(),
|
||||
'targetType' => $this->getFlowNode()->getTargetType(),
|
||||
'targetId' => $this->getFlowNode()->getTargetId(),
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\ConditionManager;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class EventIntermediateConditionalCatch extends Event
|
||||
{
|
||||
/** @var string */
|
||||
protected $pendingStatus = BpmnFlowNode::STATUS_PENDING;
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
public function process(): void
|
||||
{
|
||||
$target = $this->getConditionsTarget();
|
||||
|
||||
if (!$target) {
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->getConditionManager()->check(
|
||||
$target,
|
||||
$this->getAttributeValue('conditionsAll'),
|
||||
$this->getAttributeValue('conditionsAny'),
|
||||
$this->getAttributeValue('conditionsFormula'),
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$this->rejectConcurrentPendingFlows();
|
||||
$this->processNextElement();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->set([
|
||||
'status' => $this->pendingStatus,
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$target = $this->getConditionsTarget();
|
||||
|
||||
if (!$target) {
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->getConditionManager()->check(
|
||||
$target,
|
||||
$this->getAttributeValue('conditionsAll'),
|
||||
$this->getAttributeValue('conditionsAny'),
|
||||
$this->getAttributeValue('conditionsFormula'),
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$this->rejectConcurrentPendingFlows();
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getConditionsTarget(): ?Entity
|
||||
{
|
||||
return $this->getTarget();
|
||||
}
|
||||
|
||||
protected function getConditionManager(): ConditionManager
|
||||
{
|
||||
$conditionManager = $this->getContainer()
|
||||
->getByClass(InjectableFactory::class)
|
||||
->create(ConditionManager::class);
|
||||
|
||||
$conditionManager->setCreatedEntitiesData($this->getCreatedEntitiesData());
|
||||
|
||||
return $conditionManager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
class EventIntermediateErrorBoundary extends EventIntermediateBoundary
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->storeErrorVariables();
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
private function storeErrorVariables(): void
|
||||
{
|
||||
$variables = $this->getVariables();
|
||||
|
||||
$variables->__caughtErrorCode = $this->getFlowNode()->getDataItemValue('code');
|
||||
$variables->__caughtErrorMessage = $this->getFlowNode()->getDataItemValue('message');
|
||||
|
||||
$this->getProcess()->setVariables($variables);
|
||||
$this->saveProcess();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class EventIntermediateEscalationBoundary extends Event
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->setProcessed();
|
||||
|
||||
$this->processNextElement();
|
||||
|
||||
if ($this->getAttributeValue('cancelActivity')) {
|
||||
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventIntermediateEscalationThrow extends Event
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->getManager()->escalate($this->getProcess(), $this->getAttributeValue('escalationCode'));
|
||||
|
||||
$this->refreshProcess();
|
||||
$this->refreshTarget();
|
||||
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class EventIntermediateMessageBoundary extends EventIntermediateMessageCatch
|
||||
{
|
||||
protected function proceedPendingFinal(): void
|
||||
{
|
||||
$cancel = $this->getAttributeValue('cancelActivity');
|
||||
|
||||
if (!$cancel) {
|
||||
$this->createCopy();
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
|
||||
if ($cancel) {
|
||||
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
|
||||
}
|
||||
}
|
||||
|
||||
protected function createCopy(): void
|
||||
{
|
||||
/** @var BpmnFlowNode $flowNode */
|
||||
$flowNode = $this->getEntityManager()->getNewEntity(BpmnFlowNode::ENTITY_TYPE);
|
||||
|
||||
$flowNode->set([
|
||||
'status' => BpmnFlowNode::STATUS_PENDING,
|
||||
'elementId' => $this->getFlowNode()->getElementId(),
|
||||
'elementType' => $this->getFlowNode()->getElementType(),
|
||||
'elementData' => $this->getFlowNode()->getElementData(),
|
||||
'data' => (object) [],
|
||||
'flowchartId' => $this->getProcess()->getFlowchartId(),
|
||||
'processId' => $this->getProcess()->get('id'),
|
||||
'previousFlowNodeElementType' => $this->getFlowNode()->getPreviousFlowNodeElementType(),
|
||||
'previousFlowNodeId' => $this->getFlowNode()->getPreviousFlowNodeId(),
|
||||
'divergentFlowNodeId' => $this->getFlowNode()->getDivergentFlowNodeId(),
|
||||
'targetType' => $this->getFlowNode()->getTargetType(),
|
||||
'targetId' => $this->getFlowNode()->getTargetId(),
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
class EventIntermediateMessageCatch extends Event
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$repliedToAliasId = $this->getAttributeValue('repliedTo');
|
||||
$messageType = $this->getAttributeValue('messageType') ?? 'Email';
|
||||
$relatedTo = $this->getAttributeValue('relatedTo');
|
||||
|
||||
$conditionsFormula = $this->getAttributeValue('conditionsFormula');
|
||||
$conditionsFormula = trim($conditionsFormula, " \t\n\r");
|
||||
|
||||
if (strlen($conditionsFormula) && str_ends_with($conditionsFormula, ';')) {
|
||||
$conditionsFormula = substr($conditionsFormula, 0, -1);
|
||||
}
|
||||
|
||||
$target = $this->getTarget();
|
||||
|
||||
$createdEntitiesData = $this->getCreatedEntitiesData();
|
||||
|
||||
$repliedToId = null;
|
||||
|
||||
if ($repliedToAliasId) {
|
||||
if (!isset($createdEntitiesData->$repliedToAliasId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$repliedToId = $createdEntitiesData->$repliedToAliasId->entityId ?? null;
|
||||
$repliedToType = $createdEntitiesData->$repliedToAliasId->entityType ?? null;
|
||||
|
||||
if (!$repliedToId || $messageType !== $repliedToType) {
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
if ($messageType === 'Email') {
|
||||
$from = $flowNode->getDataItemValue('checkedAt') ?? $flowNode->get('createdAt');
|
||||
|
||||
$whereClause = [
|
||||
'createdAt>=' => $from,
|
||||
'status' => Email::STATUS_ARCHIVED,
|
||||
'dateSent>=' => $flowNode->get('createdAt'),
|
||||
[
|
||||
'OR' => [
|
||||
'sentById' => null,
|
||||
'sentBy.type' => 'portal', // @todo Change to const.
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
if ($repliedToId) {
|
||||
$whereClause['repliedId'] = $repliedToId;
|
||||
|
||||
} else if ($relatedTo) {
|
||||
$relatedTarget = $this->getSpecificTarget($relatedTo);
|
||||
|
||||
if (!$relatedTarget) {
|
||||
$this->updateCheckedAt();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($relatedTarget->getEntityType() === 'Account') {
|
||||
$whereClause['accountId'] = $relatedTarget->getId();
|
||||
} else {
|
||||
$whereClause['parentId'] = $relatedTarget->getId();
|
||||
$whereClause['parentType'] = $relatedTarget->getEntityType();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$repliedToId && !$relatedTo) {
|
||||
if ($target->getEntityType() === 'Contact' && $target->get('accountId')) {
|
||||
$whereClause[] = [
|
||||
'OR' => [
|
||||
[
|
||||
'parentType' => 'Contact',
|
||||
'parentId' => $target->getId(),
|
||||
],
|
||||
[
|
||||
'parentType' => 'Account',
|
||||
'parentId' => $target->get('accountId'),
|
||||
],
|
||||
]
|
||||
];
|
||||
}
|
||||
else if ($target->getEntityType() === 'Account') {
|
||||
$whereClause['accountId'] = $target->getId();
|
||||
}
|
||||
else {
|
||||
$whereClause['parentId'] = $target->getId();
|
||||
$whereClause['parentType'] = $target->getEntityType();
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $this->getContainer()->get('config');
|
||||
|
||||
$limit = $config->get('bpmnMessageCatchLimit', 50);
|
||||
|
||||
$emailList = $this->getEntityManager()
|
||||
->getRDBRepository(Email::ENTITY_TYPE)
|
||||
->leftJoin('sentBy')
|
||||
->where($whereClause)
|
||||
->limit(0, $limit)
|
||||
->find();
|
||||
|
||||
if (!count($emailList)) {
|
||||
$this->updateCheckedAt();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($conditionsFormula) {
|
||||
$isFound = false;
|
||||
|
||||
foreach ($emailList as $email) {
|
||||
$formulaResult = $this->getFormulaManager()
|
||||
->run($conditionsFormula, $email, $this->getVariablesForFormula());
|
||||
|
||||
if ($formulaResult) {
|
||||
$isFound = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isFound) {
|
||||
$this->updateCheckedAt();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$this->proceedPendingFinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function proceedPendingFinal(): void
|
||||
{
|
||||
$this->rejectConcurrentPendingFlows();
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
protected function updateCheckedAt(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->setDataItemValue('checkedAt', date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT));
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class EventIntermediateSignalBoundary extends EventSignal
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$signal = $this->getSignal();
|
||||
|
||||
if (!$signal) {
|
||||
$this->fail();
|
||||
|
||||
$this->getLog()->warning("BPM: No signal for sub-process EventIntermediateSignalBoundary");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$this->getSignalManager()->subscribe($signal, $flowNode->getId());
|
||||
}
|
||||
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$cancel = $this->getAttributeValue('cancelActivity');
|
||||
|
||||
if (!$cancel) {
|
||||
$this->createCopy();
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
|
||||
if ($cancel) {
|
||||
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
|
||||
}
|
||||
}
|
||||
|
||||
protected function createCopy(): void
|
||||
{
|
||||
$data = $this->getFlowNode()->getData();
|
||||
|
||||
$data = clone $data;
|
||||
|
||||
/** @var BpmnFlowNode $flowNode */
|
||||
$flowNode = $this->getEntityManager()->getNewEntity(BpmnFlowNode::ENTITY_TYPE);
|
||||
|
||||
$flowNode->set([
|
||||
'status' => BpmnFlowNode::STATUS_PENDING,
|
||||
'elementId' => $this->getFlowNode()->getElementId(),
|
||||
'elementType' => $this->getFlowNode()->getElementType(),
|
||||
'elementData' => $this->getFlowNode()->getElementData(),
|
||||
'data' => $data,
|
||||
'flowchartId' => $this->getProcess()->getFlowchartId(),
|
||||
'processId' => $this->getProcess()->getId(),
|
||||
'previousFlowNodeElementType' => $this->getFlowNode()->getPreviousFlowNodeElementType(),
|
||||
'previousFlowNodeId' => $this->getFlowNode()->getPreviousFlowNodeId(),
|
||||
'divergentFlowNodeId' => $this->getFlowNode()->getDivergentFlowNodeId(),
|
||||
'targetType' => $this->getFlowNode()->getTargetType(),
|
||||
'targetId' => $this->getFlowNode()->getTargetId(),
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$this->getSignalManager()->subscribe($this->getSignal(), $flowNode->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
class EventIntermediateSignalCatch extends EventSignal
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$signal = $this->getSignal();
|
||||
|
||||
if (!$signal) {
|
||||
$this->fail();
|
||||
|
||||
$this->getLog()->warning("BPM: No signal for EventIntermediateSignalCatch");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_PENDING);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$this->getSignalManager()->subscribe($signal, $flowNode->getId());
|
||||
}
|
||||
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$this->rejectConcurrentPendingFlows();
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventIntermediateSignalThrow extends EventSignal
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$nextFlowNode = $this->prepareNextFlowNode();
|
||||
|
||||
$this->setProcessed();
|
||||
|
||||
$signal = $this->getSignal();
|
||||
|
||||
if ($signal) {
|
||||
if (mb_substr($signal, 0, 1) !== '@') {
|
||||
$this->getManager()->broadcastSignal($signal);
|
||||
}
|
||||
} else {
|
||||
$this->getLog()->warning("BPM: eventIntermediateSignalThrow, no signal");
|
||||
}
|
||||
|
||||
$this->refreshProcess();
|
||||
$this->refreshTarget();
|
||||
|
||||
if ($nextFlowNode) {
|
||||
$this->processPreparedNextFlowNode($nextFlowNode);
|
||||
}
|
||||
|
||||
if ($signal) {
|
||||
if (mb_substr($signal, 0, 1) !== '@') {
|
||||
$this->getSignalManager()->trigger($signal);
|
||||
} else {
|
||||
$this->getSignalManager()->trigger($signal, $this->getTarget());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class EventIntermediateTimerBoundary extends EventIntermediateTimerCatch
|
||||
{
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$this->processNextElement();
|
||||
|
||||
if ($this->getAttributeValue('cancelActivity')) {
|
||||
$this->getManager()->cancelActivityByBoundaryEvent($this->getFlowNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Exception;
|
||||
use DateTime;
|
||||
|
||||
class EventIntermediateTimerCatch extends Event
|
||||
{
|
||||
/** @var string */
|
||||
protected $pendingStatus = BpmnFlowNode::STATUS_PENDING;
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$timerBase = $this->getAttributeValue('timerBase');
|
||||
|
||||
if (!$timerBase || $timerBase === 'moment') {
|
||||
$dt = new DateTime();
|
||||
|
||||
$this->shiftDateTime($dt);
|
||||
} else if ($timerBase === 'formula') {
|
||||
$timerFormula = $this->getAttributeValue('timerFormula');
|
||||
|
||||
$formulaManager = $this->getFormulaManager();
|
||||
|
||||
if (!$timerFormula) {
|
||||
$this->setFailed();
|
||||
|
||||
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
|
||||
}
|
||||
|
||||
$value = $formulaManager->run($timerFormula, $this->getTarget(), $this->getVariablesForFormula());
|
||||
|
||||
if (!$value || !is_string($value)) {
|
||||
$this->setFailed();
|
||||
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new DateTime($value);
|
||||
} catch (Exception) {
|
||||
$this->setFailed();
|
||||
|
||||
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
|
||||
}
|
||||
}
|
||||
else if (str_starts_with($timerBase, 'field:')) {
|
||||
$field = substr($timerBase, 6);
|
||||
$entity = $this->getTarget();
|
||||
|
||||
if (strpos($field, '.') > 0) {
|
||||
[$link, $field] = explode('.', $field);
|
||||
|
||||
$target = $this->getTarget();
|
||||
|
||||
$entity = $this->getEntityManager()
|
||||
->getRDBRepository($target->getEntityType())
|
||||
->getRelation($target, $link)
|
||||
->findOne();
|
||||
|
||||
if (!$entity) {
|
||||
$this->setFailed();
|
||||
|
||||
throw new Error("Bpmn Flow: EventIntermediateTimer. Related entity doesn't exist.");
|
||||
}
|
||||
}
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
if (!$value || !is_string($value)) {
|
||||
$this->setFailed();
|
||||
|
||||
throw new Error('Bpmn Flow: EventIntermediateTimer.');
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new DateTime($value);
|
||||
}
|
||||
catch (Exception) {
|
||||
$this->setFailed();
|
||||
|
||||
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
|
||||
}
|
||||
|
||||
$this->shiftDateTime($dt);
|
||||
}
|
||||
else {
|
||||
$this->setFailed();
|
||||
|
||||
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->set([
|
||||
'status' => $this->pendingStatus,
|
||||
'proceedAt' => $dt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function shiftDateTime(DateTime $dt): void
|
||||
{
|
||||
$timerShiftOperator = $this->getAttributeValue('timerShiftOperator');
|
||||
$timerShift = $this->getAttributeValue('timerShift');
|
||||
$timerShiftUnits = $this->getAttributeValue('timerShiftUnits');
|
||||
|
||||
if (!in_array($timerShiftUnits, ['minutes', 'hours', 'days', 'months', 'seconds'])) {
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$this->setFailed();
|
||||
|
||||
throw new Error("Bpmn Flow: Bad shift in ". $flowNode->get('elementType') . " " .
|
||||
$flowNode->get('elementId') . " in flowchart " . $flowNode->get('flowchartId') . ".");
|
||||
}
|
||||
|
||||
if ($timerShift) {
|
||||
$modifyString = $timerShift . ' ' . $timerShiftUnits;
|
||||
|
||||
if ($timerShiftOperator === 'minus') {
|
||||
$modifyString = '-' . $modifyString;
|
||||
}
|
||||
|
||||
try {
|
||||
$dt->modify($modifyString);
|
||||
/** @phpstan-ignore-next-line */
|
||||
} catch (Exception) {
|
||||
$this->setFailed();
|
||||
|
||||
throw new Error('Bpmn Flow: EventIntermediateTimer error.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$this->getFlowNode()->set('status', BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
$this->saveFlowNode();
|
||||
|
||||
$this->rejectConcurrentPendingFlows();
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
|
||||
use Espo\Modules\Advanced\Core\SignalManager;
|
||||
|
||||
abstract class EventSignal extends Event
|
||||
{
|
||||
protected function getSignalManager(): SignalManager
|
||||
{
|
||||
return $this->getContainer()->getByClass(SignalManager::class);
|
||||
}
|
||||
|
||||
protected function getSignal(): ?string
|
||||
{
|
||||
$name = $this->getAttributeValue('signal');
|
||||
|
||||
if (!$name || !is_string($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Helper::applyPlaceholders($name, $this->getTarget(), $this->getVariables());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventStart extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventStartCompensation extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventStartConditional extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class EventStartConditionalEventSubProcess extends EventIntermediateConditionalCatch
|
||||
{
|
||||
/** @var string */
|
||||
protected $pendingStatus = BpmnFlowNode::STATUS_STANDBY;
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
protected function getConditionsTarget(): ?Entity
|
||||
{
|
||||
return $this->getSpecificTarget($this->getFlowNode()->getDataItemValue('subProcessTarget'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|bool|null $divergentFlowNodeId
|
||||
*/
|
||||
protected function processNextElement(
|
||||
?string $nextElementId = null,
|
||||
$divergentFlowNodeId = false,
|
||||
bool $dontSetProcessed = false
|
||||
): ?BpmnFlowNode {
|
||||
|
||||
return parent::processNextElement($this->getFlowNode()->getDataItemValue('subProcessElementId'));
|
||||
}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$target = $this->getConditionsTarget();
|
||||
|
||||
if (!$target) {
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->getConditionManager()->check(
|
||||
$target,
|
||||
$this->getAttributeValue('conditionsAll'),
|
||||
$this->getAttributeValue('conditionsAny'),
|
||||
$this->getAttributeValue('conditionsFormula'),
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$subProcessIsInterrupting = $this->getFlowNode()->getDataItemValue('subProcessIsInterrupting');
|
||||
|
||||
if (!$subProcessIsInterrupting) {
|
||||
$this->createOppositeNode();
|
||||
}
|
||||
|
||||
if ($subProcessIsInterrupting) {
|
||||
$this->getManager()->interruptProcessByEventSubProcess($this->getProcess(), $this->getFlowNode());
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getFlowNode()->set(['status' => $this->pendingStatus]);
|
||||
$this->saveFlowNode();
|
||||
}
|
||||
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$result = $this->getConditionManager()->check(
|
||||
$this->getTarget(),
|
||||
$this->getAttributeValue('conditionsAll'),
|
||||
$this->getAttributeValue('conditionsAny'),
|
||||
$this->getAttributeValue('conditionsFormula'),
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if ($this->getFlowNode()->getDataItemValue('isOpposite')) {
|
||||
if (!$result) {
|
||||
$this->setProcessed();
|
||||
$this->createOppositeNode(true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
$subProcessIsInterrupting = $this->getFlowNode()->getDataItemValue('subProcessIsInterrupting');
|
||||
|
||||
if (!$subProcessIsInterrupting) {
|
||||
$this->createOppositeNode();
|
||||
}
|
||||
|
||||
if ($subProcessIsInterrupting) {
|
||||
$this->getManager()->interruptProcessByEventSubProcess($this->getProcess(), $this->getFlowNode());
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
|
||||
protected function createOppositeNode(bool $isNegative = false): void
|
||||
{
|
||||
$data = $this->getFlowNode()->get('data') ?? (object) [];
|
||||
$data = clone $data;
|
||||
$data->isOpposite = !$isNegative;
|
||||
|
||||
$flowNode = $this->getEntityManager()->getEntity(BpmnFlowNode::ENTITY_TYPE);
|
||||
|
||||
$flowNode->set([
|
||||
'status' => BpmnFlowNode::STATUS_STANDBY,
|
||||
'elementType' => $this->getFlowNode()->get('elementType'),
|
||||
'elementData' => $this->getFlowNode()->get('elementData'),
|
||||
'data' => $data,
|
||||
'flowchartId' => $this->getProcess()->get('flowchartId'),
|
||||
'processId' => $this->getProcess()->get('id'),
|
||||
'targetType' => $this->getFlowNode()->get('targetType'),
|
||||
'targetId' => $this->getFlowNode()->get('targetId'),
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
class EventStartError extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->writeErrorData();
|
||||
$this->storeErrorVariables();
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
private function writeErrorData(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$parentFlowNodeId = $this->getProcess()->getParentProcessFlowNodeId();
|
||||
|
||||
if (!$parentFlowNodeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ?BpmnFlowNode $parentFlowNode */
|
||||
$parentFlowNode = $this->getEntityManager()->getEntityById(BpmnFlowNode::ENTITY_TYPE, $parentFlowNodeId);
|
||||
|
||||
if (!$parentFlowNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$code = $parentFlowNode->getDataItemValue('caughtErrorCode');
|
||||
$message = $parentFlowNode->getDataItemValue('caughtErrorMessage');
|
||||
|
||||
$flowNode->setDataItemValue('code', $code);
|
||||
$flowNode->setDataItemValue('message', $message);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
}
|
||||
|
||||
private function storeErrorVariables(): void
|
||||
{
|
||||
$variables = $this->getVariables();
|
||||
|
||||
$variables->__caughtErrorCode = $this->getFlowNode()->getDataItemValue('code');
|
||||
$variables->__caughtErrorMessage = $this->getFlowNode()->getDataItemValue('message');
|
||||
|
||||
$this->getProcess()->setVariables($variables);
|
||||
$this->saveProcess();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventStartEscalation extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventStartSignal extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
|
||||
use Espo\Modules\Advanced\Core\SignalManager;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class EventStartSignalEventSubProcess extends Event
|
||||
{
|
||||
/**
|
||||
* @param string|bool|null $divergentFlowNodeId
|
||||
*/
|
||||
protected function processNextElement(
|
||||
?string $nextElementId = null,
|
||||
$divergentFlowNodeId = false,
|
||||
bool $dontSetProcessed = false
|
||||
): ?BpmnFlowNode {
|
||||
|
||||
return parent::processNextElement($this->getFlowNode()->getDataItemValue('subProcessElementId'));
|
||||
}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$signal = $this->getSignal();
|
||||
|
||||
if (!$signal) {
|
||||
$this->fail();
|
||||
|
||||
$this->getLog()->warning("BPM: No signal for sub-process start event.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
$flowNode->set([
|
||||
'status' => BpmnFlowNode::STATUS_STANDBY,
|
||||
]);
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$this->getSignalManager()->subscribe($signal, $flowNode->get('id'));
|
||||
}
|
||||
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$subProcessIsInterrupting = $this->getFlowNode()->getDataItemValue('subProcessIsInterrupting');
|
||||
|
||||
if (!$subProcessIsInterrupting) {
|
||||
$this->createCopy();
|
||||
}
|
||||
|
||||
if ($subProcessIsInterrupting) {
|
||||
$this->getManager()->interruptProcessByEventSubProcess($this->getProcess(), $this->getFlowNode());
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
protected function createCopy(): void
|
||||
{
|
||||
$data = $this->getFlowNode()->get('data') ?? (object) [];
|
||||
$data = clone $data;
|
||||
|
||||
$flowNode = $this->getEntityManager()->getEntity(BpmnFlowNode::ENTITY_TYPE);
|
||||
|
||||
$flowNode->set([
|
||||
'status' => BpmnFlowNode::STATUS_STANDBY,
|
||||
'elementType' => $this->getFlowNode()->getElementType(),
|
||||
'elementData' => $this->getFlowNode()->get('elementData'),
|
||||
'data' => $data,
|
||||
'flowchartId' => $this->getProcess()->getFlowchartId(),
|
||||
'processId' => $this->getProcess()->get('id'),
|
||||
'targetType' => $this->getFlowNode()->getTargetType(),
|
||||
'targetId' => $this->getFlowNode()->getTargetId(),
|
||||
]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$this->getSignalManager()->subscribe($this->getSignal(), $flowNode->get('id'));
|
||||
}
|
||||
|
||||
protected function getSignal(): ?string
|
||||
{
|
||||
$subProcessStartData = $this->getFlowNode()->getDataItemValue('subProcessStartData') ?? (object) [];
|
||||
|
||||
$name = $subProcessStartData->signal ?? null;
|
||||
|
||||
if (!$name || !is_string($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Helper::applyPlaceholders($name, $this->getTarget(), $this->getVariables());
|
||||
}
|
||||
|
||||
protected function getSignalManager(): SignalManager
|
||||
{
|
||||
return $this->getContainer()->getByClass(SignalManager::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
class EventStartTimer extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class EventStartTimerEventSubProcess extends EventIntermediateTimerCatch
|
||||
{
|
||||
/** @var string */
|
||||
protected $pendingStatus = BpmnFlowNode::STATUS_STANDBY;
|
||||
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getConditionsTarget(): ?Entity
|
||||
{
|
||||
return $this->getSpecificTarget($this->getFlowNode()->getDataItemValue('subProcessTarget'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|bool|null $divergentFlowNodeId
|
||||
* @throws Error
|
||||
*/
|
||||
protected function processNextElement(
|
||||
?string $nextElementId = null,
|
||||
$divergentFlowNodeId = false,
|
||||
bool $dontSetProcessed = false
|
||||
): ?BpmnFlowNode {
|
||||
|
||||
return parent::processNextElement($this->getFlowNode()->getDataItemValue('subProcessElementId'));
|
||||
}
|
||||
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$subProcessIsInterrupting = $this->getFlowNode()->getDataItemValue('subProcessIsInterrupting');
|
||||
|
||||
if (!$subProcessIsInterrupting) {
|
||||
$standbyFlowNode = $this->getManager()->prepareStandbyFlow(
|
||||
$this->getTarget(),
|
||||
$this->getProcess(),
|
||||
$this->getFlowNode()->getDataItemValue('subProcessElementId')
|
||||
);
|
||||
|
||||
if ($standbyFlowNode) {
|
||||
$this->getManager()->processPreparedFlowNode(
|
||||
$this->getTarget(),
|
||||
$standbyFlowNode,
|
||||
$this->getProcess()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($subProcessIsInterrupting) {
|
||||
$this->getManager()->interruptProcessByEventSubProcess($this->getProcess(), $this->getFlowNode());
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
|
||||
class EventSubProcess extends SubProcess
|
||||
{
|
||||
protected function getSubProcessStartElementId(): ?string
|
||||
{
|
||||
$eventStartData = $this->getAttributeValue('eventStartData') ?? (object) [];
|
||||
|
||||
return $eventStartData->id ?? null;
|
||||
}
|
||||
|
||||
public function complete(): void
|
||||
{
|
||||
$this->setProcessed();
|
||||
|
||||
if ($this->getProcess()->getStatus() === BpmnProcess::STATUS_STARTED) {
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
}
|
||||
|
||||
protected function isMultiInstance(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
126
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Gateway.php
Normal file
126
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Gateway.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
abstract class Gateway extends Base
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
if ($this->isDivergent()) {
|
||||
$this->processDivergent();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isConvergent()) {
|
||||
$this->processConvergent();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setFailed();
|
||||
}
|
||||
|
||||
abstract protected function processDivergent(): void;
|
||||
|
||||
abstract protected function processConvergent(): void;
|
||||
|
||||
protected function isDivergent(): bool
|
||||
{
|
||||
$nextElementIdList = $this->getAttributeValue('nextElementIdList') ?? [];
|
||||
|
||||
return !$this->isConvergent() && count($nextElementIdList);
|
||||
}
|
||||
|
||||
protected function isConvergent(): bool
|
||||
{
|
||||
$previousElementIdList = $this->getAttributeValue('previousElementIdList') ?? [];
|
||||
|
||||
return is_array($previousElementIdList) && count($previousElementIdList) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $divergentElementId
|
||||
* @param string $forkElementId
|
||||
* @param string $currentElementId
|
||||
* @param string[] $metElementIdList
|
||||
*/
|
||||
private function checkElementsBelongSingleFlowRecursive(
|
||||
$divergentElementId,
|
||||
$forkElementId,
|
||||
$currentElementId,
|
||||
bool &$result,
|
||||
&$metElementIdList = null
|
||||
): void {
|
||||
|
||||
if ($divergentElementId === $currentElementId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($forkElementId === $currentElementId) {
|
||||
$result = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$metElementIdList) {
|
||||
$metElementIdList = [];
|
||||
}
|
||||
|
||||
$flowchartElementsDataHash = $this->getProcess()->get('flowchartElementsDataHash');
|
||||
|
||||
$elementData = $flowchartElementsDataHash->$currentElementId;
|
||||
|
||||
if (!isset($elementData->previousElementIdList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($elementData->previousElementIdList as $elementId) {
|
||||
if (in_array($elementId, $metElementIdList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->checkElementsBelongSingleFlowRecursive(
|
||||
$divergentElementId,
|
||||
$forkElementId,
|
||||
$elementId,
|
||||
$result,
|
||||
$metElementIdList
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $divergentElementId
|
||||
* @param string $forkElementId
|
||||
* @param string $elementId
|
||||
*/
|
||||
protected function checkElementsBelongSingleFlow(
|
||||
$divergentElementId,
|
||||
$forkElementId,
|
||||
$elementId
|
||||
): bool {
|
||||
|
||||
$result = false;
|
||||
|
||||
$this->checkElementsBelongSingleFlowRecursive($divergentElementId, $forkElementId, $elementId, $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GatewayEventBased extends Gateway
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function processDivergent(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$item = $flowNode->getElementData();
|
||||
$nextElementIdList = $item->nextElementIdList ?? [];
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
foreach ($nextElementIdList as $nextElementId) {
|
||||
$nextFlowNode = $this->processNextElement($nextElementId, false, true);
|
||||
|
||||
if ($nextFlowNode->getStatus() === BpmnFlowNode::STATUS_PROCESSED) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setProcessed();
|
||||
|
||||
$this->getManager()->tryToEndProcess($this->getProcess());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function processConvergent(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\ConditionManager;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GatewayExclusive extends Gateway
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws FormulaError
|
||||
*/
|
||||
protected function processDivergent(): void
|
||||
{
|
||||
$conditionManager = $this->getConditionManager();
|
||||
|
||||
$flowList = $this->getAttributeValue('flowList');
|
||||
|
||||
if (!is_array($flowList)) {
|
||||
$flowList = [];
|
||||
}
|
||||
|
||||
$defaultNextElementId = $this->getAttributeValue('defaultNextElementId');
|
||||
$nextElementId = null;
|
||||
|
||||
foreach ($flowList as $flowData) {
|
||||
$conditionsAll = $flowData->conditionsAll ?? null;
|
||||
$conditionsAny = $flowData->conditionsAny ?? null;
|
||||
$conditionsFormula = $flowData->conditionsFormula ?? null;
|
||||
|
||||
$result = $conditionManager->check(
|
||||
$this->getTarget(),
|
||||
$conditionsAll,
|
||||
$conditionsAny,
|
||||
$conditionsFormula,
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$nextElementId = $flowData->elementId;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$nextElementId && $defaultNextElementId) {
|
||||
$nextElementId = $defaultNextElementId;
|
||||
}
|
||||
|
||||
if ($nextElementId) {
|
||||
$this->processNextElement($nextElementId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaError
|
||||
*/
|
||||
protected function processConvergent(): void
|
||||
{
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
protected function getConditionManager(): ConditionManager
|
||||
{
|
||||
$conditionManager = $this->getContainer()
|
||||
->getByClass(InjectableFactory::class)
|
||||
->create(ConditionManager::class);
|
||||
|
||||
$conditionManager->setCreatedEntitiesData($this->getCreatedEntitiesData());
|
||||
|
||||
return $conditionManager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\ConditionManager;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GatewayInclusive extends Gateway
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws FormulaError
|
||||
*/
|
||||
protected function processDivergent(): void
|
||||
{
|
||||
$conditionManager = $this->getConditionManager();
|
||||
|
||||
$flowList = $this->getAttributeValue('flowList');
|
||||
|
||||
if (!is_array($flowList)) {
|
||||
$flowList = [];
|
||||
}
|
||||
|
||||
$defaultNextElementId = $this->getAttributeValue('defaultNextElementId');
|
||||
|
||||
$nextElementIdList = [];
|
||||
|
||||
foreach ($flowList as $flowData) {
|
||||
$conditionsAll = $flowData->conditionsAll ?? null;
|
||||
$conditionsAny = $flowData->conditionsAny ?? null;
|
||||
$conditionsFormula = $flowData->conditionsFormula ?? null;
|
||||
|
||||
$result = $conditionManager->check(
|
||||
$this->getTarget(),
|
||||
$conditionsAll,
|
||||
$conditionsAny,
|
||||
$conditionsFormula,
|
||||
$this->getVariablesForFormula()
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$nextElementIdList[] = $flowData->elementId;
|
||||
}
|
||||
}
|
||||
|
||||
//$isDefaultFlow = false;
|
||||
|
||||
if (!count($nextElementIdList) && $defaultNextElementId) {
|
||||
//$isDefaultFlow = true;
|
||||
|
||||
$nextElementIdList[] = $defaultNextElementId;
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$nextDivergentFlowNodeId = $flowNode->getId();
|
||||
|
||||
if (count($nextElementIdList)) {
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$nextFlowNodeList = [];
|
||||
|
||||
foreach ($nextElementIdList as $nextElementId) {
|
||||
$nextFlowNode = $this->prepareNextFlowNode($nextElementId, $nextDivergentFlowNodeId);
|
||||
|
||||
if ($nextFlowNode) {
|
||||
$nextFlowNodeList[] = $nextFlowNode;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setProcessed();
|
||||
|
||||
foreach ($nextFlowNodeList as $nextFlowNode) {
|
||||
if ($this->getProcess()->getStatus() !== BpmnProcess::STATUS_STARTED) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->getManager()->processPreparedFlowNode($this->getTarget(), $nextFlowNode, $this->getProcess());
|
||||
}
|
||||
|
||||
$this->getManager()->tryToEndProcess($this->getProcess());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaError
|
||||
*/
|
||||
protected function processConvergent(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$item = $flowNode->getElementData();
|
||||
$previousElementIdList = $item->previousElementIdList;
|
||||
|
||||
$nextDivergentFlowNodeId = null;
|
||||
$divergentFlowNode = null;
|
||||
|
||||
$convergingFlowCount = 1;
|
||||
|
||||
if ($flowNode->getDivergentFlowNodeId()) {
|
||||
/** @var ?BpmnFlowNode $divergentFlowNode */
|
||||
$divergentFlowNode = $this->getEntityManager()
|
||||
->getEntityById(BpmnFlowNode::ENTITY_TYPE, $flowNode->getDivergentFlowNodeId());
|
||||
|
||||
if ($divergentFlowNode) {
|
||||
$nextDivergentFlowNodeId = $divergentFlowNode->getDivergentFlowNodeId();
|
||||
|
||||
/** @var iterable<BpmnFlowNode> $forkFlowNodeList */
|
||||
$forkFlowNodeList = $this->getEntityManager()
|
||||
->getRDBRepository(BpmnFlowNode::ENTITY_TYPE)
|
||||
->where([
|
||||
'processId' => $flowNode->getProcessId(),
|
||||
'previousFlowNodeId' => $divergentFlowNode->getId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
$convergingFlowCount = 0;
|
||||
|
||||
foreach ($previousElementIdList as $previousElementId) {
|
||||
$isActual = false;
|
||||
|
||||
foreach ($forkFlowNodeList as $forkFlowNode) {
|
||||
if (
|
||||
$this->checkElementsBelongSingleFlow(
|
||||
$divergentFlowNode->getElementId(),
|
||||
$forkFlowNode->getElementId(),
|
||||
$previousElementId
|
||||
)
|
||||
) {
|
||||
$isActual = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isActual) {
|
||||
$convergingFlowCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$concurrentFlowNodeList = $this->getEntityManager()
|
||||
->getRDBRepository(BpmnFlowNode::ENTITY_TYPE)
|
||||
->where([
|
||||
'elementId' => $flowNode->getElementId(),
|
||||
'processId' => $flowNode->getProcessId(),
|
||||
'divergentFlowNodeId' => $flowNode->getDivergentFlowNodeId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
$concurrentCount = count(iterator_to_array($concurrentFlowNodeList));
|
||||
|
||||
if ($concurrentCount < $convergingFlowCount) {
|
||||
$this->setRejected();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$isBalancingDivergent = true;
|
||||
|
||||
if ($divergentFlowNode) {
|
||||
$divergentElementData = $divergentFlowNode->getElementData();
|
||||
|
||||
if (isset($divergentElementData->nextElementIdList)) {
|
||||
foreach ($divergentElementData->nextElementIdList as $forkId) {
|
||||
if (
|
||||
!$this->checkElementsBelongSingleFlow(
|
||||
$divergentFlowNode->getElementId(),
|
||||
$forkId,
|
||||
$flowNode->getElementId()
|
||||
)
|
||||
) {
|
||||
$isBalancingDivergent = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isBalancingDivergent) {
|
||||
if ($divergentFlowNode) {
|
||||
$nextDivergentFlowNodeId = $divergentFlowNode->getDivergentFlowNodeId();
|
||||
}
|
||||
|
||||
$this->processNextElement(null, $nextDivergentFlowNodeId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @noinspection PhpRedundantOptionalArgumentInspection */
|
||||
$this->processNextElement(null, false);
|
||||
}
|
||||
|
||||
protected function getConditionManager(): ConditionManager
|
||||
{
|
||||
$conditionManager = $this->getContainer()
|
||||
->getByClass(InjectableFactory::class)
|
||||
->create(ConditionManager::class);
|
||||
|
||||
$conditionManager->setCreatedEntitiesData($this->getCreatedEntitiesData());
|
||||
|
||||
return $conditionManager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GatewayParallel extends Gateway
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function processDivergent(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$item = $flowNode->getElementData();
|
||||
|
||||
$nextElementIdList = $item->nextElementIdList ?? [];
|
||||
|
||||
if (count($nextElementIdList)) {
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
$nextFlowNodeList = [];
|
||||
|
||||
foreach ($nextElementIdList as $nextElementId) {
|
||||
$nextFlowNode = $this->prepareNextFlowNode($nextElementId, $flowNode->getId());
|
||||
|
||||
if ($nextFlowNode) {
|
||||
$nextFlowNodeList[] = $nextFlowNode;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setProcessed();
|
||||
|
||||
foreach ($nextFlowNodeList as $nextFlowNode) {
|
||||
if ($this->getProcess()->getStatus() !== BpmnProcess::STATUS_STARTED) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->getManager()->processPreparedFlowNode($this->getTarget(), $nextFlowNode, $this->getProcess());
|
||||
}
|
||||
|
||||
$this->getManager()->tryToEndProcess($this->getProcess());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->endProcessFlow();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function processConvergent(): void
|
||||
{
|
||||
$flowNode = $this->getFlowNode();
|
||||
|
||||
$item = $flowNode->getElementData();
|
||||
|
||||
$previousElementIdList = $item->previousElementIdList;
|
||||
$convergingFlowCount = count($previousElementIdList);
|
||||
|
||||
//$nextDivergentFlowNodeId = null;
|
||||
$divergentFlowNode = null;
|
||||
|
||||
//$divergedFlowCount = 1;
|
||||
|
||||
if ($flowNode->getDivergentFlowNodeId()) {
|
||||
/** @var ?BpmnFlowNode $divergentFlowNode */
|
||||
$divergentFlowNode = $this->getEntityManager()
|
||||
->getEntityById(BpmnFlowNode::ENTITY_TYPE, $flowNode->getDivergentFlowNodeId());
|
||||
|
||||
/*if ($divergentFlowNode) {
|
||||
$divergentElementData = $divergentFlowNode->getElementData();
|
||||
|
||||
$divergedFlowCount = count($divergentElementData->nextElementIdList ?? []);
|
||||
}*/
|
||||
}
|
||||
|
||||
$concurrentFlowNodeList = $this->getEntityManager()
|
||||
->getRDBRepository(BpmnFlowNode::ENTITY_TYPE)
|
||||
->where([
|
||||
'elementId' => $flowNode->getElementId(),
|
||||
'processId' => $flowNode->getProcessId(),
|
||||
'divergentFlowNodeId' => $flowNode->getDivergentFlowNodeId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
$concurrentCount = count(iterator_to_array($concurrentFlowNodeList));
|
||||
|
||||
if ($concurrentCount < $convergingFlowCount) {
|
||||
$this->setRejected();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$isBalancingDivergent = true;
|
||||
|
||||
if ($divergentFlowNode) {
|
||||
$divergentElementData = $divergentFlowNode->getElementData();
|
||||
|
||||
if (isset($divergentElementData->nextElementIdList)) {
|
||||
foreach ($divergentElementData->nextElementIdList as $forkId) {
|
||||
if (
|
||||
!$this->checkElementsBelongSingleFlow(
|
||||
$divergentFlowNode->getElementId(),
|
||||
$forkId,
|
||||
$flowNode->getElementId()
|
||||
)
|
||||
) {
|
||||
$isBalancingDivergent = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isBalancingDivergent) {
|
||||
$nextDivergentFlowNodeId = $divergentFlowNode?->getDivergentFlowNodeId();
|
||||
|
||||
$this->processNextElement(null, $nextDivergentFlowNodeId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @noinspection PhpRedundantOptionalArgumentInspection */
|
||||
$this->processNextElement(null, false);
|
||||
}
|
||||
}
|
||||
144
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/SubProcess.php
Normal file
144
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/SubProcess.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowchart;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
|
||||
use Throwable;
|
||||
use stdClass;
|
||||
|
||||
class SubProcess extends CallActivity
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
if ($this->isMultiInstance()) {
|
||||
$this->processMultiInstance();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$target = $this->getNewTargetEntity();
|
||||
|
||||
if (!$target) {
|
||||
$this->getLog()->info("BPM Sub-Process: Could not get target for sub-process.");
|
||||
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$flowNode = $this->getFlowNode();
|
||||
$variables = $this->getPrepareVariables();
|
||||
|
||||
$this->refreshProcess();
|
||||
|
||||
$parentFlowchartData = $this->getProcess()->get('flowchartData') ?? (object) [];
|
||||
|
||||
$createdEntitiesData = clone $this->getCreatedEntitiesData();
|
||||
|
||||
$eData = Helper::getElementsDataFromFlowchartData((object) [
|
||||
'list' => $this->getAttributeValue('dataList') ?? [],
|
||||
]);
|
||||
|
||||
/** @var BpmnFlowchart $flowchart */
|
||||
$flowchart = $this->getEntityManager()->getNewEntity(BpmnFlowchart::ENTITY_TYPE);
|
||||
|
||||
$flowchart->set([
|
||||
'targetType' => $target->getEntityType(),
|
||||
'data' => (object) [
|
||||
'createdEntitiesData' => $parentFlowchartData->createdEntitiesData ?? (object) [],
|
||||
'list' => $this->getAttributeValue('dataList') ?? [],
|
||||
],
|
||||
'elementsDataHash' => $eData['elementsDataHash'],
|
||||
'hasNoneStartEvent' => count($eData['eventStartIdList']) > 0,
|
||||
'eventStartIdList'=> $eData['eventStartIdList'],
|
||||
'teamsIds' => $this->getProcess()->getTeams()->getIdList(),
|
||||
'assignedUserId' => $this->getProcess()->getAssignedUser()?->getId(),
|
||||
'name' => $this->getAttributeValue('title') ?? 'Sub-Process',
|
||||
]);
|
||||
|
||||
/** @var BpmnProcess $subProcess */
|
||||
$subProcess = $this->getEntityManager()->createEntity(BpmnProcess::ENTITY_TYPE, [
|
||||
'status' => BpmnFlowNode::STATUS_CREATED,
|
||||
'targetId' => $target->getId(),
|
||||
'targetType' => $target->getEntityType(),
|
||||
'parentProcessId' => $this->getProcess()->getId(),
|
||||
'parentProcessFlowNodeId' => $flowNode->getId(),
|
||||
'rootProcessId' => $this->getProcess()->getRootProcessId(),
|
||||
'assignedUserId' => $this->getProcess()->getAssignedUser()?->getId(),
|
||||
'teamsIds' => $this->getProcess()->getTeams()->getIdList(),
|
||||
'variables' => $variables,
|
||||
'createdEntitiesData' => $createdEntitiesData,
|
||||
'startElementId' => $this->getSubProcessStartElementId(),
|
||||
], [
|
||||
'skipCreatedBy' => true,
|
||||
'skipModifiedBy' => true,
|
||||
'skipStartProcessFlow' => true,
|
||||
]);
|
||||
|
||||
$flowNode->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
|
||||
$flowNode->setDataItemValue('subProcessId', $subProcess->getId());
|
||||
|
||||
$this->getEntityManager()->saveEntity($flowNode);
|
||||
|
||||
try {
|
||||
$this->getManager()->startCreatedProcess($subProcess, $flowchart);
|
||||
} catch (Throwable $e) {
|
||||
$message = "BPM Sub-Process: Starting sub-process failure, {$subProcess->getId()}. {$e->getMessage()}";
|
||||
|
||||
$this->getLog()->error($message, ['exception' => $e]);
|
||||
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getSubProcessStartElementId(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function generateSubProcessMultiInstance(int $loopCounter, int $x, int $y): stdClass
|
||||
{
|
||||
return (object) [
|
||||
'type' => $this->getAttributeValue('type'),
|
||||
'id' => self::generateElementId(),
|
||||
'center' => (object) [
|
||||
'x' => $x + 125,
|
||||
'y' => $y,
|
||||
],
|
||||
'dataList' => $this->getAttributeValue('dataList'),
|
||||
'returnVariableList' => $this->getAttributeValue('returnVariableList'),
|
||||
'isExpanded' => false,
|
||||
'target' => $this->getAttributeValue('target'),
|
||||
'targetType' => $this->getAttributeValue('targetType'),
|
||||
'targetIdExpression' => $this->getAttributeValue('targetIdExpression'),
|
||||
'isMultiInstance' => false,
|
||||
'triggeredByEvent' => false,
|
||||
'isSequential' => false,
|
||||
'loopCollectionExpression' => null,
|
||||
'text' => (string) $loopCounter,
|
||||
];
|
||||
}
|
||||
}
|
||||
157
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Task.php
Normal file
157
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/Task.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Actions\Base as BaseAction;
|
||||
|
||||
use Throwable;
|
||||
use stdClass;
|
||||
|
||||
class Task extends Activity
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $localVariableList = [
|
||||
'_lastHttpResponseBody',
|
||||
'__lastCreatedEntityId',
|
||||
];
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$actionList = $this->getAttributeValue('actionList');
|
||||
|
||||
if (!is_array($actionList)) {
|
||||
$actionList = [];
|
||||
}
|
||||
|
||||
$originalVariables = $this->getVariablesForFormula();
|
||||
|
||||
$variables = clone $originalVariables;
|
||||
|
||||
try {
|
||||
foreach ($actionList as $item) {
|
||||
if (empty($item->type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addCreatedEntityDataToVariables($variables);
|
||||
|
||||
$actionImpl = $this->getActionImplementation($item->type);
|
||||
|
||||
/** @var stdClass $item */
|
||||
$item = clone $item;
|
||||
$item->elementId = $this->getFlowNode()->getElementId();
|
||||
|
||||
$actionData = $item;
|
||||
|
||||
$actionImpl->process(
|
||||
entity: $this->getTarget(),
|
||||
actionData: $actionData,
|
||||
createdEntitiesData: $this->getCreatedEntitiesData(),
|
||||
variables: $variables,
|
||||
bpmnProcess: $this->getProcess(),
|
||||
);
|
||||
|
||||
if ($actionImpl->isCreatedEntitiesDataChanged()) {
|
||||
$this->getProcess()->setCreatedEntitiesData($actionImpl->getCreatedEntitiesData());
|
||||
|
||||
$this->getEntityManager()->saveEntity($this->getProcess(), ['silent' => true]);
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$message = "Process {$this->getProcess()->getId()}, element {$this->getFlowNode()->getId()}: " .
|
||||
"{$e->getMessage()}";
|
||||
|
||||
$this->getLog()->error($message, ['exception' => $e]);
|
||||
|
||||
$this->setFailedWithException($e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processStoreVariables($variables, $originalVariables);
|
||||
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @todo Use factory.
|
||||
*/
|
||||
private function getActionImplementation(string $name): BaseAction
|
||||
{
|
||||
$name = ucfirst($name);
|
||||
$name = str_replace("\\", "", $name);
|
||||
|
||||
$className = 'Espo\\Modules\\Advanced\\Core\\Workflow\\Actions\\' . $name;
|
||||
|
||||
if (!class_exists($className)) {
|
||||
$className .= 'Type';
|
||||
|
||||
if (!class_exists($className)) {
|
||||
throw new Error('Action class ' . $className . ' does not exist.');
|
||||
}
|
||||
}
|
||||
|
||||
/** @var class-string<BaseAction> $className */
|
||||
|
||||
$impl = $this->getContainer()
|
||||
->getByClass(InjectableFactory::class)
|
||||
->create($className);
|
||||
|
||||
$workflowId = $this->getProcess()->get('workflowId');
|
||||
|
||||
if ($workflowId) {
|
||||
$impl->setWorkflowId($workflowId);
|
||||
}
|
||||
|
||||
return $impl;
|
||||
}
|
||||
|
||||
private function processStoreVariables(stdClass $variables, stdClass $originalVariables): void
|
||||
{
|
||||
foreach ($this->localVariableList as $name) {
|
||||
unset($variables->$name);
|
||||
}
|
||||
|
||||
// The same in TaskScript.
|
||||
if ($this->getAttributeValue('isolateVariables')) {
|
||||
$variableList = array_keys(get_object_vars($variables));
|
||||
$returnVariableList = $this->getReturnVariableList();
|
||||
|
||||
foreach (array_diff($variableList, $returnVariableList) as $name) {
|
||||
unset($variables->$name);
|
||||
|
||||
if (property_exists($originalVariables, $name)) {
|
||||
$variables->$name = $originalVariables->$name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->sanitizeVariables($originalVariables);
|
||||
$this->sanitizeVariables($variables);
|
||||
|
||||
if (serialize($variables) !== serialize($originalVariables)) {
|
||||
$this->getProcess()->setVariables($variables);
|
||||
|
||||
$this->getEntityManager()->saveEntity($this->getProcess(), ['silent' => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class TaskScript extends Activity
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$formula = $this->getAttributeValue('formula');
|
||||
|
||||
if (!$formula) {
|
||||
$this->processNextElement();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_string($formula)) {
|
||||
$message = "Process {$this->getProcess()->getId()}, formula should be string.";
|
||||
|
||||
$this->getLog()->error($message);
|
||||
|
||||
$this->setFailed();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$originalVariables = $this->getVariablesForFormula();
|
||||
|
||||
$variables = clone $originalVariables;
|
||||
|
||||
try {
|
||||
$this->getFormulaManager()->run($formula, $this->getTarget(), $variables);
|
||||
|
||||
$this->getEntityManager()->saveEntity($this->getTarget(), [
|
||||
'skipWorkflow' => true,
|
||||
'skipModifiedBy' => true,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
$message = "Process {$this->getProcess()->getId()} formula error: {$e->getMessage()}";
|
||||
|
||||
$this->getLog()->error($message, ['exception' => $e]);
|
||||
|
||||
$this->setFailedWithException($e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processStoreVariables($variables, $originalVariables);
|
||||
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
private function processStoreVariables(stdClass $variables, stdClass $originalVariables): void
|
||||
{
|
||||
// The same in Task.
|
||||
if ($this->getAttributeValue('isolateVariables')) {
|
||||
$variableList = array_keys(get_object_vars($variables));
|
||||
$returnVariableList = $this->getReturnVariableList();
|
||||
|
||||
foreach (array_diff($variableList, $returnVariableList) as $name) {
|
||||
unset($variables->$name);
|
||||
|
||||
if (property_exists($originalVariables, $name)) {
|
||||
$variables->$name = $originalVariables->$name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->sanitizeVariables($variables);
|
||||
|
||||
$this->getProcess()->setVariables($variables);
|
||||
|
||||
$this->getEntityManager()->saveEntity($this->getProcess(), ['silent' => true]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\MessageSenders\EmailType;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class TaskSendMessage extends Activity
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$this->getFlowNode()->setStatus(BpmnFlowNode::STATUS_PENDING);
|
||||
$this->saveFlowNode();
|
||||
}
|
||||
|
||||
public function proceedPending(): void
|
||||
{
|
||||
$createdEntitiesData = $this->getCreatedEntitiesData();
|
||||
|
||||
try {
|
||||
$this->getImplementation()->process(
|
||||
$this->getTarget(),
|
||||
$this->getFlowNode(),
|
||||
$this->getProcess(),
|
||||
$createdEntitiesData,
|
||||
$this->getVariables()
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$message = "Process {$this->getProcess()->getId()}, element {$this->getFlowNode()->getElementId()}, " .
|
||||
"send message error: {$e->getMessage()}";
|
||||
|
||||
$this->getLog()->error($message, ['exception' => $e]);
|
||||
|
||||
$this->setFailedWithException($e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getProcess()->set('createdEntitiesData', $createdEntitiesData);
|
||||
$this->getEntityManager()->saveEntity($this->getProcess());
|
||||
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailType
|
||||
* @throws Error
|
||||
* @todo Use factory.
|
||||
*/
|
||||
private function getImplementation(): EmailType
|
||||
{
|
||||
$messageType = $this->getAttributeValue('messageType');
|
||||
|
||||
if (!$messageType) {
|
||||
throw new Error('Process ' . $this->getProcess()->getId() . ', no message type.');
|
||||
}
|
||||
|
||||
$messageType = str_replace('\\', '', $messageType);
|
||||
|
||||
/** @var class-string<EmailType> $className */
|
||||
$className = "Espo\\Modules\\Advanced\\Core\\Bpmn\\Utils\\MessageSenders\\{$messageType}Type";
|
||||
|
||||
if (!class_exists($className)) {
|
||||
throw new Error(
|
||||
'Process ' . $this->getProcess()->getId() . ' element ' .
|
||||
$this->getFlowNode()->get('elementId'). ' send message not found implementation class.');
|
||||
}
|
||||
|
||||
|
||||
return $this->getContainer()
|
||||
->getByClass(InjectableFactory::class)
|
||||
->create($className);
|
||||
}
|
||||
}
|
||||
311
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/TaskUser.php
Normal file
311
custom/Espo/Modules/Advanced/Core/Bpmn/Elements/TaskUser.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?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\Core\Bpmn\Elements;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Field\LinkMultiple;
|
||||
use Espo\Core\Field\LinkParent;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Modules\Advanced\Business\Workflow\AssignmentRules\LeastBusy;
|
||||
use Espo\Modules\Advanced\Business\Workflow\AssignmentRules\RoundRobin;
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\Helper;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use Espo\Modules\Advanced\Entities\BpmnUserTask;
|
||||
use Espo\ORM\Entity;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class TaskUser extends Activity
|
||||
{
|
||||
private const ASSIGNMENT_SPECIFIED_USER = 'specifiedUser';
|
||||
private const ASSIGNMENT_PROCESS_ASSIGNED_USER = 'processAssignedUser';
|
||||
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(): void
|
||||
{
|
||||
$this->getFlowNode()->setStatus(BpmnFlowNode::STATUS_IN_PROCESS);
|
||||
$this->saveFlowNode();
|
||||
|
||||
$target = $this->getSpecificTarget($this->getAttributeValue('target'));
|
||||
|
||||
if (!$target) {
|
||||
$this->getLog()->info("BPM TaskUser: Could not get target.");
|
||||
|
||||
$this->fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$userTask = $this->createUserTask($target);
|
||||
|
||||
$this->getFlowNode()->setDataItemValue('userTaskId', $userTask->getId());
|
||||
$this->saveFlowNode();
|
||||
|
||||
$createdEntitiesData = $this->getPreparedCreatedEntitiesData($userTask);
|
||||
|
||||
$this->getProcess()->setCreatedEntitiesData($createdEntitiesData);
|
||||
$this->saveProcess();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeastBusy|RoundRobin
|
||||
* @throws Error
|
||||
*/
|
||||
private function getAssignmentRuleImplementation(string $assignmentRule)
|
||||
{
|
||||
/** @var class-string<LeastBusy|RoundRobin> $className */
|
||||
$className = 'Espo\\Modules\\Advanced\\Business\\Workflow\\AssignmentRules\\' .
|
||||
str_replace('-', '', $assignmentRule);
|
||||
|
||||
if (!class_exists($className)) {
|
||||
throw new Error('Process TaskUser, Class ' . $className . ' not found.');
|
||||
}
|
||||
|
||||
$injectableFactory = $this->getContainer()->getByClass(InjectableFactory::class);
|
||||
|
||||
return $injectableFactory->createWith($className, [
|
||||
'entityType' => BpmnUserTask::ENTITY_TYPE,
|
||||
'actionId' => $this->getElementId(),
|
||||
'flowchartId' => $this->getFlowNode()->getFlowchartId(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function complete(): void
|
||||
{
|
||||
if (!$this->isInNormalFlow()) {
|
||||
$this->setProcessed();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processNextElement();
|
||||
}
|
||||
|
||||
protected function getLanguage(): Language
|
||||
{
|
||||
/** @var Language */
|
||||
return $this->getContainer()->get('defaultLanguage');
|
||||
}
|
||||
|
||||
protected function setInterrupted(): void
|
||||
{
|
||||
$this->cancelUserTask();
|
||||
|
||||
parent::setInterrupted();
|
||||
}
|
||||
|
||||
public function cleanupInterrupted(): void
|
||||
{
|
||||
parent::cleanupInterrupted();
|
||||
|
||||
$this->cancelUserTask();
|
||||
}
|
||||
|
||||
private function cancelUserTask(): void
|
||||
{
|
||||
$userTaskId = $this->getFlowNode()->getDataItemValue('userTaskId');
|
||||
|
||||
if ($userTaskId) {
|
||||
/** @var ?BpmnUserTask $userTask */
|
||||
$userTask = $this->getEntityManager()->getEntityById(BpmnUserTask::ENTITY_TYPE, $userTaskId);
|
||||
|
||||
if ($userTask && !$userTask->get('isResolved')) {
|
||||
$userTask->set(['isCanceled' => true]);
|
||||
|
||||
$this->getEntityManager()->saveEntity($userTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getTaskName(): ?string
|
||||
{
|
||||
$name = $this->getAttributeValue('name');
|
||||
|
||||
if (!is_string($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Helper::applyPlaceholders($name, $this->getTarget(), $this->getVariables());
|
||||
}
|
||||
|
||||
private function getInstructionsText(): ?string
|
||||
{
|
||||
$text = $this->getAttributeValue('instructions');
|
||||
|
||||
if (!is_string($text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Helper::applyPlaceholders($text, $this->getTarget(), $this->getVariables());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function getAssignmentAttributes(BpmnUserTask $userTask): array
|
||||
{
|
||||
$assignmentType = $this->getAttributeValue('assignmentType');
|
||||
$targetTeamId = $this->getAttributeValue('targetTeamId');
|
||||
$targetUserPosition = $this->getAttributeValue('targetUserPosition') ?: null;
|
||||
|
||||
$assignmentAttributes = [];
|
||||
|
||||
if (str_starts_with($assignmentType, 'rule:')) {
|
||||
$assignmentRule = substr($assignmentType, 5);
|
||||
$ruleImpl = $this->getAssignmentRuleImplementation($assignmentRule);
|
||||
|
||||
$whereClause = null;
|
||||
|
||||
if ($assignmentRule === 'Least-Busy') {
|
||||
$whereClause = ['isResolved' => false];
|
||||
}
|
||||
|
||||
$assignmentAttributes = $ruleImpl->getAssignmentAttributes(
|
||||
$userTask,
|
||||
$targetTeamId,
|
||||
$targetUserPosition,
|
||||
null,
|
||||
$whereClause
|
||||
);
|
||||
} else if (str_starts_with($assignmentType, 'link:')) {
|
||||
$link = substr($assignmentType, 5);
|
||||
$e = $this->getTarget();
|
||||
|
||||
if (str_contains($link, '.')) {
|
||||
[$firstLink, $link] = explode('.', $link);
|
||||
|
||||
$target = $this->getTarget();
|
||||
|
||||
$e = $this->getEntityManager()
|
||||
->getRDBRepository($target->getEntityType())
|
||||
->getRelation($target, $firstLink)
|
||||
->findOne();
|
||||
}
|
||||
|
||||
if ($e instanceof Entity) {
|
||||
$field = $link . 'Id';
|
||||
$userId = $e->get($field);
|
||||
|
||||
if ($userId) {
|
||||
$assignmentAttributes['assignedUserId'] = $userId;
|
||||
}
|
||||
}
|
||||
} else if ($assignmentType === self::ASSIGNMENT_PROCESS_ASSIGNED_USER) {
|
||||
$userId = $this->getProcess()->getAssignedUser()?->getId();
|
||||
|
||||
if ($userId) {
|
||||
$assignmentAttributes['assignedUserId'] = $userId;
|
||||
}
|
||||
} else if ($assignmentType === self::ASSIGNMENT_SPECIFIED_USER) {
|
||||
$userId = $this->getAttributeValue('targetUserId');
|
||||
|
||||
if ($userId) {
|
||||
$assignmentAttributes['assignedUserId'] = $userId;
|
||||
}
|
||||
}
|
||||
|
||||
return $assignmentAttributes;
|
||||
}
|
||||
|
||||
private function getTaskNameFinal(): string
|
||||
{
|
||||
$name = $this->getTaskName();
|
||||
|
||||
$actionType = $this->getAttributeValue('actionType');
|
||||
|
||||
if (!$name) {
|
||||
$name = $this->getLanguage()->translateOption($actionType, 'actionType', BpmnUserTask::ENTITY_TYPE);
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
private function getPreparedCreatedEntitiesData(BpmnUserTask $userTask): stdClass
|
||||
{
|
||||
$createdEntitiesData = $this->getCreatedEntitiesData();
|
||||
|
||||
$alias = $this->getFlowNode()->getElementId();
|
||||
|
||||
if ($alias) {
|
||||
$createdEntitiesData->$alias = (object)[
|
||||
'entityId' => $userTask->getId(),
|
||||
'entityType' => $userTask->getEntityType(),
|
||||
];
|
||||
}
|
||||
|
||||
return $createdEntitiesData;
|
||||
}
|
||||
|
||||
private function createUserTask(CoreEntity $target): BpmnUserTask
|
||||
{
|
||||
$userTask = $this->getEntityManager()->getRDBRepositoryByClass(BpmnUserTask::class)->getNew();
|
||||
|
||||
$userTask
|
||||
->setProcessId($this->getProcess()->getId())
|
||||
->setActionType($this->getAttributeValue('actionType'))
|
||||
->setFlowNodeId($this->getFlowNode()->getId())
|
||||
->setTarget(LinkParent::create($target->getEntityType(), $target->getId()))
|
||||
->setDescription($this->getAttributeValue('description'))
|
||||
->setInstructions($this->getInstructionsText());
|
||||
|
||||
$userTask->set($this->getAssignmentAttributes($userTask));
|
||||
|
||||
$userTask
|
||||
->setName($this->getTaskNameFinal())
|
||||
->setTeams(LinkMultiple::create()->withAddedIdList($this->getTeamIdList()));
|
||||
|
||||
$this->getEntityManager()->saveEntity($userTask, ['createdById' => 'system']);
|
||||
|
||||
return $userTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getTeamIdList(): array
|
||||
{
|
||||
$teamIdList = $this->getProcess()->getTeams()->getIdList();
|
||||
$targetTeamId = $this->getAttributeValue('targetTeamId');
|
||||
|
||||
if ($targetTeamId && !in_array($targetTeamId, $teamIdList)) {
|
||||
$teamIdList[] = $targetTeamId;
|
||||
}
|
||||
|
||||
return $teamIdList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?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\Core\Bpmn\Utils;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Container;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Formula\Manager as FormulaManager;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Conditions\Base;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
class ConditionManager
|
||||
{
|
||||
private ?stdClass $createdEntitiesData = null;
|
||||
|
||||
private const TYPE_AND = 'and';
|
||||
private const TYPE_OR = 'or';
|
||||
|
||||
/** @var string[] */
|
||||
private array $requiredOptionList = [
|
||||
'comparison',
|
||||
'fieldToCompare',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
private InjectableFactory $injectableFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param ?stdClass[] $conditionsAll
|
||||
* @param ?stdClass[] $conditionsAny
|
||||
* @throws Error
|
||||
* @throws FormulaError
|
||||
*/
|
||||
public function check(
|
||||
Entity $entity,
|
||||
?array $conditionsAll = null,
|
||||
?array $conditionsAny = null,
|
||||
?string $conditionsFormula = null,
|
||||
?stdClass $variables = null,
|
||||
): bool {
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$result = true;
|
||||
|
||||
if (!is_null($conditionsAll)) {
|
||||
$result &= $this->checkConditionsAll($entity, $conditionsAll);
|
||||
}
|
||||
|
||||
if (!is_null($conditionsAny)) {
|
||||
$result &= $this->checkConditionsAny($entity, $conditionsAny);
|
||||
}
|
||||
|
||||
if (!empty($conditionsFormula)) {
|
||||
$result &= $this->checkConditionsFormula($entity, $conditionsFormula, $variables);
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $items
|
||||
* @throws Error
|
||||
*/
|
||||
public function checkConditionsAll(CoreEntity $entity, array $items): bool
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
if (!$this->processCheck($entity, $item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $items
|
||||
* @throws Error
|
||||
*/
|
||||
public function checkConditionsAny(CoreEntity $entity, array $items): bool
|
||||
{
|
||||
if ($items === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($this->processCheck($entity, $item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FormulaError
|
||||
*/
|
||||
public function checkConditionsFormula(Entity $entity, ?string $formula, ?stdClass $variables = null): bool
|
||||
{
|
||||
if (empty($formula)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$formula = trim($formula, " \t\n\r");
|
||||
|
||||
if (str_ends_with($formula, ';')) {
|
||||
$formula = substr($formula, 0, -1);
|
||||
}
|
||||
|
||||
if (empty($formula)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$o = (object) [];
|
||||
|
||||
$o->__targetEntity = $entity;
|
||||
|
||||
if ($variables) {
|
||||
foreach (get_object_vars($variables) as $name => $value) {
|
||||
$o->$name = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->createdEntitiesData) {
|
||||
$o->__createdEntitiesData = $this->createdEntitiesData;
|
||||
}
|
||||
|
||||
return $this->getFormulaManager()->run($formula, $entity, $o);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
private function processCheck(CoreEntity $entity, stdClass $item): bool
|
||||
{
|
||||
if (!$this->validate($item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = $item->type ?? null;
|
||||
|
||||
if ($type === self::TYPE_AND || $type === self::TYPE_OR) {
|
||||
/** @var stdClass[] $value */
|
||||
$value = $item->value ?? [];
|
||||
|
||||
if ($type === self::TYPE_OR) {
|
||||
return $this->checkConditionsAny($entity, $value);
|
||||
}
|
||||
|
||||
return $this->checkConditionsAll($entity, $value);
|
||||
}
|
||||
|
||||
$impl = $this->getConditionImplementation($item->comparison);
|
||||
|
||||
return $impl->process($entity, $item, $this->createdEntitiesData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
private function getConditionImplementation(string $name): Base
|
||||
{
|
||||
$name = ucfirst($name);
|
||||
$name = str_replace("\\", "", $name);
|
||||
|
||||
$className = 'Espo\\Modules\\Advanced\\Core\\Workflow\\Conditions\\' . $name;
|
||||
|
||||
if (!class_exists($className)) {
|
||||
$className .= 'Type';
|
||||
|
||||
if (!class_exists($className)) {
|
||||
throw new Error('ConditionManager: Class ' . $className . ' does not exist.');
|
||||
}
|
||||
}
|
||||
|
||||
/** @var class-string<Base> $className */
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
public function setCreatedEntitiesData(stdClass $createdEntitiesData): void
|
||||
{
|
||||
$this->createdEntitiesData = $createdEntitiesData;
|
||||
}
|
||||
|
||||
private function validate(stdClass $item): bool
|
||||
{
|
||||
if (
|
||||
isset($item->type) &&
|
||||
in_array($item->type, [self::TYPE_OR, self::TYPE_AND])
|
||||
) {
|
||||
if (!isset($item->value) || !is_array($item->value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->requiredOptionList as $optionName) {
|
||||
if (!property_exists($item, $optionName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getContainer(): Container
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
private function getFormulaManager(): FormulaManager
|
||||
{
|
||||
return $this->getContainer()->getByClass(FormulaManager::class);
|
||||
}
|
||||
}
|
||||
195
custom/Espo/Modules/Advanced/Core/Bpmn/Utils/Helper.php
Normal file
195
custom/Espo/Modules/Advanced/Core/Bpmn/Utils/Helper.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?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\Core\Bpmn\Utils;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use stdClass;
|
||||
|
||||
class Helper
|
||||
{
|
||||
/**
|
||||
* @return array{
|
||||
* elementsDataHash: stdClass,
|
||||
* eventStartIdList: string[],
|
||||
* eventStartAllIdList: string[]
|
||||
* }
|
||||
*/
|
||||
public static function getElementsDataFromFlowchartData(stdClass $data): array
|
||||
{
|
||||
$elementsDataHash = (object) [];
|
||||
$eventStartIdList = [];
|
||||
$eventStartAllIdList = [];
|
||||
|
||||
if (isset($data->list) && is_array($data->list)) {
|
||||
foreach ($data->list as $item) {
|
||||
if (!is_object($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$itType = $item->type ?? null;
|
||||
$itId = $item->id ?? null;
|
||||
|
||||
if ($itType === 'flow') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nextElementIdList = [];
|
||||
$previousElementIdList = [];
|
||||
|
||||
foreach ($data->list as $itemAnother) {
|
||||
if ($itemAnother->type !== 'flow') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($itemAnother->startId) || !isset($itemAnother->endId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($itemAnother->startId === $itId) {
|
||||
$nextElementIdList[] = $itemAnother->endId;
|
||||
} else if ($itemAnother->endId === $itId) {
|
||||
$previousElementIdList[] = $itemAnother->startId;
|
||||
}
|
||||
}
|
||||
|
||||
usort($nextElementIdList, function ($id1, $id2) use ($data) {
|
||||
$item1 = self::getItemById($data, $id1);
|
||||
$item2 = self::getItemById($data, $id2);
|
||||
|
||||
if (isset($item1->center) && isset($item2->center)) {
|
||||
if ($item1->center->y > $item2->center->y) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($item1->center->y == $item2->center->y) {
|
||||
if ($item1->center->x > $item2->center->x) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
});
|
||||
|
||||
$id = $item->id ?? null;
|
||||
|
||||
/** @var stdClass $o */
|
||||
$o = clone $item;
|
||||
|
||||
$o->nextElementIdList = $nextElementIdList;
|
||||
$o->previousElementIdList = $previousElementIdList;
|
||||
|
||||
if (isset($item->flowList)) {
|
||||
$o->flowList = [];
|
||||
|
||||
foreach ($item->flowList as $nextFlowData) {
|
||||
$nextFlowDataCloned = clone $nextFlowData;
|
||||
|
||||
foreach ($data->list as $itemAnother) {
|
||||
if ($itemAnother->id !== $nextFlowData->id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nextFlowDataCloned->elementId = $itemAnother->endId;
|
||||
break;
|
||||
}
|
||||
|
||||
$o->flowList[] = $nextFlowDataCloned;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($item->defaultFlowId)) {
|
||||
foreach ($data->list as $itemAnother) {
|
||||
if ($itemAnother->id !== $item->defaultFlowId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$o->defaultNextElementId = $itemAnother->endId;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($itType === 'eventStart') {
|
||||
$eventStartIdList[] = $id;
|
||||
}
|
||||
|
||||
if (is_string($itType) && str_starts_with($itType, 'eventStart')) {
|
||||
$eventStartAllIdList[] = $id;
|
||||
}
|
||||
|
||||
$elementsDataHash->$id = $o;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'elementsDataHash' => $elementsDataHash,
|
||||
'eventStartIdList' => $eventStartIdList,
|
||||
'eventStartAllIdList' => $eventStartAllIdList,
|
||||
];
|
||||
}
|
||||
|
||||
private static function getItemById(stdClass $data, string $id): ?stdClass
|
||||
{
|
||||
foreach ($data->list as $item) {
|
||||
if ($item->id === $id) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function applyPlaceholders(string $text, Entity $target, stdClass $variables = null): string
|
||||
{
|
||||
foreach ($target->getAttributeList() as $attribute) {
|
||||
$value = $target->get($attribute);
|
||||
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$text = str_replace('{$' . $attribute . '}', $value, $text);
|
||||
}
|
||||
|
||||
$variables = $variables ?? (object) [];
|
||||
|
||||
foreach (get_object_vars($variables) as $key => $value) {
|
||||
if (is_numeric($value)) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$text = str_replace('{$$' . $key . '}', $value, $text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?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\Core\Bpmn\Utils\MessageSenders;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Actions\SendEmail;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowNode;
|
||||
use stdClass;
|
||||
|
||||
class EmailType
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(
|
||||
Entity $target,
|
||||
BpmnFlowNode $flowNode,
|
||||
BpmnProcess $process,
|
||||
stdClass $createdEntitiesData,
|
||||
stdClass $variables
|
||||
): void {
|
||||
|
||||
$elementData = $flowNode->getElementData();
|
||||
|
||||
if (empty($elementData->from)) {
|
||||
throw new Error("No 'from'.");
|
||||
}
|
||||
|
||||
$from = $elementData->from;
|
||||
|
||||
if (empty($elementData->to)) {
|
||||
throw new Error("No 'to'.");
|
||||
}
|
||||
|
||||
$to = $elementData->to;
|
||||
|
||||
$replyTo = null;
|
||||
|
||||
if (!empty($elementData->replyTo)) {
|
||||
$replyTo = $elementData->replyTo;
|
||||
}
|
||||
|
||||
$cc = null;
|
||||
|
||||
if (!empty($elementData->cc)) {
|
||||
$cc = $elementData->cc;
|
||||
}
|
||||
|
||||
if (empty($elementData->emailTemplateId)) {
|
||||
throw new Error("No 'emailTemplateId'.");
|
||||
}
|
||||
|
||||
$emailTemplateId = $elementData->emailTemplateId;
|
||||
|
||||
$doNotStore = false;
|
||||
|
||||
if (isset($elementData->doNotStore)) {
|
||||
$doNotStore = $elementData->doNotStore;
|
||||
}
|
||||
|
||||
$actionData = (object) [
|
||||
'type' => 'SendEmail',
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'cc' => $cc,
|
||||
'replyTo' => $replyTo,
|
||||
'emailTemplateId' => $emailTemplateId,
|
||||
'doNotStore' => $doNotStore,
|
||||
'processImmediately' => true,
|
||||
'elementId' => $flowNode->get('elementId'),
|
||||
'optOutLink' => $elementData->optOutLink ?? false,
|
||||
'attachmentsVariable' => $elementData->attachmentsVariable ?? null,
|
||||
];
|
||||
|
||||
if (property_exists($elementData, 'toEmailAddress')) {
|
||||
$actionData->toEmail = $elementData->toEmailAddress;
|
||||
}
|
||||
|
||||
if (property_exists($elementData, 'fromEmailAddress')) {
|
||||
$actionData->fromEmail = $elementData->fromEmailAddress;
|
||||
}
|
||||
|
||||
if (property_exists($elementData, 'replyToEmailAddress')) {
|
||||
$actionData->replyToEmail = $elementData->replyToEmailAddress;
|
||||
}
|
||||
|
||||
if (property_exists($elementData, 'ccEmailAddress')) {
|
||||
$actionData->ccEmail = $elementData->ccEmailAddress;
|
||||
}
|
||||
|
||||
if (in_array($to, ['specifiedContacts', 'specifiedUsers', 'specifiedTeams'])) {
|
||||
$actionData->toSpecifiedEntityIds = $elementData->{'to' . ucfirst($to) . 'Ids'};
|
||||
}
|
||||
|
||||
// Not used. Not available on UI.
|
||||
if (in_array($replyTo, ['specifiedContacts', 'specifiedUsers', 'specifiedTeams'])) {
|
||||
$actionData->replyToSpecifiedEntityIds = $elementData->{'replyTo' . ucfirst($replyTo) . 'Ids'};
|
||||
}
|
||||
|
||||
// Not used. Not available on UI.
|
||||
if (in_array($cc, ['specifiedContacts', 'specifiedUsers', 'specifiedTeams'])) {
|
||||
$actionData->ccSpecifiedEntityIds = $elementData->{'cc' . ucfirst($cc) . 'Ids'};
|
||||
}
|
||||
|
||||
$this->getActionImplementation()->process(
|
||||
entity: $target,
|
||||
actionData: $actionData,
|
||||
createdEntitiesData: $createdEntitiesData,
|
||||
variables: $variables,
|
||||
bpmnProcess: $process,
|
||||
);
|
||||
}
|
||||
|
||||
private function getActionImplementation(): SendEmail
|
||||
{
|
||||
return $this->injectableFactory->create(SendEmail::class);
|
||||
}
|
||||
}
|
||||
166
custom/Espo/Modules/Advanced/Core/Cleanup/Integrity.php
Normal file
166
custom/Espo/Modules/Advanced/Core/Cleanup/Integrity.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?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\Core\Cleanup;
|
||||
|
||||
use Exception;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\Extension;
|
||||
use Espo\Core\Cleanup\Cleanup;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\File\ZipArchive;
|
||||
use Espo\Core\Job\Job\Data as JobData;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
|
||||
class Integrity implements Cleanup
|
||||
{
|
||||
private string $name;
|
||||
private string $file;
|
||||
private string $class;
|
||||
private string $fieldStatus;
|
||||
|
||||
private string $hash = '1a0e1cce45d9abacc6e8c4a40222b29f';
|
||||
private string $packagePath = 'data/upload/extensions';
|
||||
|
||||
public function __construct(
|
||||
private FileManager $fileManager,
|
||||
private EntityManager $entityManager,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {
|
||||
$this->name = base64_decode('QWR2YW5jZWQgUGFjaw==');
|
||||
$this->file = base64_decode('Y3VzdG9tL0VzcG8vTW9kdWxlcy9BZHZhbmNlZC9Db3JlL0FwcC9Kb2JSdW5uZXIucGhw');
|
||||
$this->class = base64_decode('RXNwb1xNb2R1bGVzXEFkdmFuY2VkXENvcmVcQXBwXEpvYlJ1bm5lcg==');
|
||||
$this->fieldStatus = base64_decode('bGljZW5zZVN0YXR1cw==');
|
||||
}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$this->check();
|
||||
$this->checkRun();
|
||||
$this->scheduleRun();
|
||||
}
|
||||
|
||||
private function getExtension(): ?Extension
|
||||
{
|
||||
/** @var ?Extension */
|
||||
return $this->entityManager
|
||||
->getRDBRepository(Extension::ENTITY_TYPE)
|
||||
->where([
|
||||
'name' => $this->name,
|
||||
])
|
||||
->order('createdAt', true)
|
||||
->findOne();
|
||||
}
|
||||
|
||||
private function check(): void
|
||||
{
|
||||
if (!file_exists($this->file)) {
|
||||
$this->restore($this->file);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->hash !== hash_file('md5', $this->file)) {
|
||||
$this->restore($this->file);
|
||||
}
|
||||
}
|
||||
|
||||
private function restore(string $filePath): void
|
||||
{
|
||||
$current = $this->getExtension();
|
||||
|
||||
if (!$current) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $this->packagePath . '/' . $current->getId();
|
||||
|
||||
if (!file_exists($path . 'z')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$zip = new ZipArchive($this->fileManager);
|
||||
$zip->unzip($path . 'z', $path);
|
||||
|
||||
$file = $path . '/files/' . $filePath;
|
||||
|
||||
if (!file_exists($file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->fileManager->copy($file, dirname($filePath), false, null, true);
|
||||
} catch (Exception) {}
|
||||
|
||||
$this->fileManager->removeInDir($path, true);
|
||||
}
|
||||
|
||||
private function checkRun(): void
|
||||
{
|
||||
$current = $this->getExtension();
|
||||
|
||||
if (!$current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$current->has($this->fieldStatus)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($current->get($this->fieldStatus)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var class-string $class */
|
||||
$class = $this->class;
|
||||
|
||||
$service = $this->injectableFactory->create($class);
|
||||
|
||||
if (!method_exists($service, 'run')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service->run(JobData::create());
|
||||
}
|
||||
|
||||
private function scheduleRun(): void
|
||||
{
|
||||
$class = str_replace('Runner', '', $this->class);
|
||||
$file = str_replace('Runner', '', $this->file);
|
||||
|
||||
if (!file_exists($file)) {
|
||||
$this->restore($file);
|
||||
}
|
||||
|
||||
if (!file_exists($file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!class_exists($class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = $this->injectableFactory->create($class);
|
||||
|
||||
if (!method_exists($service, 'run')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service->run();
|
||||
}
|
||||
}
|
||||
52
custom/Espo/Modules/Advanced/Core/Cleanup/WorkflowLog.php
Normal file
52
custom/Espo/Modules/Advanced/Core/Cleanup/WorkflowLog.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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\Core\Cleanup;
|
||||
|
||||
//use Espo\Core\Cleanup\Cleanup;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\ORM\EntityManager;
|
||||
use DateTime;
|
||||
|
||||
class WorkflowLog /*implements Cleanup*/
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$period = '-' . $this->config->get('cleanupWorkflowLogPeriod', '2 months');
|
||||
$datetime = new DateTime();
|
||||
$datetime->modify($period);
|
||||
|
||||
$deleteQuery = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from('WorkflowLogRecord')
|
||||
->where(['createdAt<' => $datetime->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)])
|
||||
->build();
|
||||
|
||||
$this->entityManager
|
||||
->getQueryExecutor()
|
||||
->execute($deleteQuery);
|
||||
}
|
||||
}
|
||||
34
custom/Espo/Modules/Advanced/Core/Loaders/SignalManager.php
Normal file
34
custom/Espo/Modules/Advanced/Core/Loaders/SignalManager.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Core\Loaders;
|
||||
|
||||
use Espo\Core\Container\Loader;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Core\SignalManager as Service;
|
||||
|
||||
class SignalManager implements Loader
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function load(): Service
|
||||
{
|
||||
return $this->injectableFactory->create(Service::class);
|
||||
}
|
||||
}
|
||||
34
custom/Espo/Modules/Advanced/Core/Loaders/WorkflowHelper.php
Normal file
34
custom/Espo/Modules/Advanced/Core/Loaders/WorkflowHelper.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Core\Loaders;
|
||||
|
||||
use Espo\Core\Container\Loader;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Helper as Service;
|
||||
|
||||
class WorkflowHelper implements Loader
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function load(): Service
|
||||
{
|
||||
return $this->injectableFactory->create(Service::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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\Core\Loaders;
|
||||
|
||||
use Espo\Core\Container\Loader;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Core\WorkflowManager as Service;
|
||||
|
||||
class WorkflowManager implements Loader
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function load(): Service
|
||||
{
|
||||
return $this->injectableFactory->create(Service::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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\Core\ORM;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Creates entities with custom defs. Need for supporting foreign fields like `linkName.attribute`.
|
||||
*
|
||||
* Not used as of Espo v9.1.7.
|
||||
*/
|
||||
class CustomEntityFactory
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>> $attributeDefs
|
||||
*/
|
||||
public function create(string $entityType, array $attributeDefs): Entity
|
||||
{
|
||||
return $this->createImplementation($entityType, $attributeDefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>> $attributeDefs
|
||||
*/
|
||||
private function createImplementation(string $entityType, array $attributeDefs): Entity
|
||||
{
|
||||
$seed = $this->entityManager->getEntityFactory()->create($entityType);
|
||||
|
||||
$className = get_class($seed);
|
||||
|
||||
$defs = $this->entityManager->getMetadata()->get($entityType);
|
||||
|
||||
$defs['attributes'] = array_merge($defs['attributes'], $attributeDefs);
|
||||
|
||||
$entity = $this->injectableFactory->createWith($className, [
|
||||
'entityType' => $entityType,
|
||||
'defs' => $defs,
|
||||
'valueAccessorFactory' => null,
|
||||
]);
|
||||
|
||||
if (!$entity instanceof Entity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
174
custom/Espo/Modules/Advanced/Core/ORM/SthCollection.php
Normal file
174
custom/Espo/Modules/Advanced/Core/ORM/SthCollection.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?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\Core\ORM;
|
||||
|
||||
use Espo\Core\FieldProcessing\ListLoadProcessor;
|
||||
use Espo\Core\FieldProcessing\Loader\Params as LoaderParams;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\ORM\EntityFactory;
|
||||
use Espo\ORM\Collection;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use PDOStatement;
|
||||
use PDO;
|
||||
use IteratorAggregate;
|
||||
use stdClass;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @implements IteratorAggregate<int, Entity>
|
||||
* @implements Collection<Entity>
|
||||
*/
|
||||
class SthCollection implements IteratorAggregate, Collection
|
||||
{
|
||||
/**
|
||||
* @param array<string, array<string, mixed>> $attributeDefs Additional attributes.
|
||||
* @param string[] $linkMultipleFieldList
|
||||
* @param object{entityType: string, name: string}[] $foreignLinkFieldDataList
|
||||
*/
|
||||
public function __construct(
|
||||
private PDOStatement $sth,
|
||||
private string $entityType,
|
||||
private EntityManager $entityManager,
|
||||
private $attributeDefs,
|
||||
private $linkMultipleFieldList,
|
||||
private $foreignLinkFieldDataList,
|
||||
private CustomEntityFactory $customEntityFactory,
|
||||
private ListLoadProcessor $listLoadProcessor,
|
||||
) {}
|
||||
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return (function () {
|
||||
while ($row = $this->sth->fetch(PDO::FETCH_ASSOC)) {
|
||||
$rowData = [];
|
||||
|
||||
foreach ($row as $key => $value) {
|
||||
/** @var string $attribute */
|
||||
$attribute = str_replace('.', '_', $key);
|
||||
|
||||
$rowData[$attribute] = $value;
|
||||
}
|
||||
|
||||
$select = array_keys($rowData);
|
||||
|
||||
foreach ($this->linkMultipleFieldList as $it) {
|
||||
$select[] = $it . 'Ids';
|
||||
$select[] = $it . 'Names';
|
||||
}
|
||||
|
||||
$entity = $this->prepareEntity();
|
||||
|
||||
$entity->set($rowData);
|
||||
$entity->setAsFetched();
|
||||
|
||||
$fieldLoaderParams = LoaderParams::create()->withSelect($select);
|
||||
|
||||
$this->listLoadProcessor->process($entity, $fieldLoaderParams);
|
||||
|
||||
$this->loadNoLoadLinkMultiple($entity);
|
||||
|
||||
foreach ($this->foreignLinkFieldDataList as $item) {
|
||||
$foreignId = $entity->get($item->name . 'Id');
|
||||
|
||||
if (!$foreignId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$foreignEntity = $this->entityManager
|
||||
->getRDBRepository($item->entityType)
|
||||
->where(['id' => $foreignId])
|
||||
->select(['name'])
|
||||
->findOne();
|
||||
|
||||
if (!$foreignEntity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity->set($item->name . 'Name', $foreignEntity->get('name'));
|
||||
}
|
||||
|
||||
$entity->setAsFetched();
|
||||
|
||||
yield $entity;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass[]
|
||||
*/
|
||||
public function getValueMapList(): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($this as $entity) {
|
||||
$list[] = $entity->getValueMap();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function prepareEntity(): Entity
|
||||
{
|
||||
$factory = $this->entityManager->getEntityFactory();
|
||||
|
||||
/** @noinspection PhpConditionAlreadyCheckedInspection */
|
||||
if (
|
||||
$factory instanceof EntityFactory &&
|
||||
/** @phpstan-ignore-next-line function.alreadyNarrowedType */
|
||||
method_exists($factory, 'createWithAdditionalAttributes')
|
||||
) {
|
||||
/** @noinspection PhpInternalEntityUsedInspection */
|
||||
return $factory->createWithAdditionalAttributes($this->entityType, $this->attributeDefs);
|
||||
}
|
||||
|
||||
// Before Espo v9.1.7.
|
||||
return $this->customEntityFactory->create($this->entityType, $this->attributeDefs);
|
||||
}
|
||||
|
||||
private function loadNoLoadLinkMultiple(Entity $entity): void
|
||||
{
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($this->entityType);
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->linkMultipleFieldList as $field) {
|
||||
$fieldDefs = $entityDefs->tryGetField($field);
|
||||
|
||||
if (!$fieldDefs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$fieldDefs->getParam('noLoad')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$entity->hasLinkMultipleField($field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @noinspection PhpInternalEntityUsedInspection */
|
||||
$entity->loadLinkMultipleField($field);
|
||||
}
|
||||
}
|
||||
}
|
||||
1197
custom/Espo/Modules/Advanced/Core/Report/ExportXlsx.php
Normal file
1197
custom/Espo/Modules/Advanced/Core/Report/ExportXlsx.php
Normal file
File diff suppressed because it is too large
Load Diff
114
custom/Espo/Modules/Advanced/Core/ReportFilter.php
Normal file
114
custom/Espo/Modules/Advanced/Core/ReportFilter.php
Normal file
@@ -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\Core;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Entities\Report;
|
||||
use Espo\Modules\Advanced\Entities\ReportFilter as ReportFilterEntity;
|
||||
use Espo\Modules\Advanced\Tools\Report\Service;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @deprecated As of v7.5 PrimaryFilter is used.
|
||||
*/
|
||||
class ReportFilter
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Metadata $metadata,
|
||||
private User $user,
|
||||
private Service $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
public function applyFilter(string $entityType, string $filterName, &$result, $selectManger)
|
||||
{
|
||||
$reportFilterId = $this->metadata
|
||||
->get(['entityDefs', $entityType, 'collection', 'filters', $filterName, 'id']);
|
||||
|
||||
if (!$reportFilterId) {
|
||||
throw new Error('Report Filter error.');
|
||||
}
|
||||
|
||||
$reportFilter = $this->entityManager
|
||||
->getRDBRepositoryByClass(ReportFilterEntity::class)
|
||||
->getById($reportFilterId);
|
||||
|
||||
if (!$reportFilter) {
|
||||
throw new Error('Report Filter not found.');
|
||||
}
|
||||
|
||||
$teamIdList = $reportFilter->getLinkMultipleIdList('teams');
|
||||
|
||||
if (count($teamIdList) && !$this->user->isAdmin()) {
|
||||
$isInTeam = false;
|
||||
$userTeamIdList = $this->user->getLinkMultipleIdList('teams');
|
||||
|
||||
foreach ($userTeamIdList as $teamId) {
|
||||
if (in_array($teamId, $teamIdList)) {
|
||||
$isInTeam = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isInTeam) {
|
||||
throw new Forbidden("Access denied to Report Filter.");
|
||||
}
|
||||
}
|
||||
|
||||
$reportId = $reportFilter->get('reportId');
|
||||
|
||||
if (!$reportId) {
|
||||
throw new Error('Report Filter error.');
|
||||
}
|
||||
|
||||
/** @var ?Report $report */
|
||||
$report = $this->entityManager->getEntity(Report::ENTITY_TYPE, $reportId);
|
||||
|
||||
if (!$report) {
|
||||
throw new Error('Report Filter error. Report not found.');
|
||||
}
|
||||
|
||||
$selectParams = $this->service
|
||||
->prepareSelectBuilder($report)
|
||||
->build()
|
||||
->getRaw();
|
||||
|
||||
$result['whereClause'][] = $selectParams['whereClause'];
|
||||
|
||||
foreach ($selectParams['joins'] ?? [] as $join) {
|
||||
$selectManger->addJoin($join, $result);
|
||||
}
|
||||
|
||||
foreach ($selectParams['leftJoins'] ?? [] as $join) {
|
||||
$selectManger->addLeftJoin($join, $result);
|
||||
}
|
||||
|
||||
if (!empty($selectParams['distinct'])) {
|
||||
$selectManger->setDistinct(true, $result);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
custom/Espo/Modules/Advanced/Core/SignalManager.php
Normal file
110
custom/Espo/Modules/Advanced/Core/SignalManager.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?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\Core;
|
||||
|
||||
use Espo\Core\Utils\DateTime;
|
||||
use Espo\Modules\Advanced\Entities\BpmnSignalListener;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
class SignalManager
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private WorkflowManager $workflowManager;
|
||||
private Config $config;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
WorkflowManager $workflowManager,
|
||||
Config $config
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->workflowManager = $workflowManager;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|string $signal A signal.
|
||||
* @param array<string, mixed> $options Save options.
|
||||
* @param ?array<string, mixed> $params Signal params.
|
||||
*/
|
||||
public function trigger($signal, ?Entity $entity = null, array $options = [], ?array $params = null): void
|
||||
{
|
||||
if (is_array($signal)) {
|
||||
$signal = implode('.', $signal);
|
||||
}
|
||||
|
||||
if ($this->config->get('signalsDisabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity) {
|
||||
$this->workflowManager->processSignal($entity, $signal, $params, $options);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->config->get('signalsRegularDisabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$listenerList = $this->entityManager
|
||||
->getRDBRepository(BpmnSignalListener::ENTITY_TYPE)
|
||||
->select(['id'])
|
||||
->order('number')
|
||||
->where([
|
||||
'name' => $signal,
|
||||
'isTriggered' => false,
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($listenerList as $item) {
|
||||
$item->set('isTriggered', true);
|
||||
$item->set('triggeredAt', date(DateTime::SYSTEM_DATE_TIME_FORMAT));
|
||||
|
||||
$this->entityManager->saveEntity($item);
|
||||
}
|
||||
}
|
||||
|
||||
public function subscribe(string $signal, string $flowNodeId): ?string
|
||||
{
|
||||
if ($this->config->get('signalsDisabled')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->config->get('signalsRegularDisabled')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = $this->entityManager->createEntity(BpmnSignalListener::ENTITY_TYPE, [
|
||||
'name' => $signal,
|
||||
'flowNodeId' => $flowNodeId,
|
||||
]);
|
||||
|
||||
return $item->getId();
|
||||
}
|
||||
|
||||
public function unsubscribe(string $id): void
|
||||
{
|
||||
$this->entityManager
|
||||
->getRDBRepositoryByClass(BpmnSignalListener::class)
|
||||
->deleteFromDb($id);
|
||||
}
|
||||
}
|
||||
189
custom/Espo/Modules/Advanced/Core/TemplateHelpers/Report.php
Normal file
189
custom/Espo/Modules/Advanced/Core/TemplateHelpers/Report.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?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\Core\TemplateHelpers;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Htmlizer\Helper;
|
||||
use Espo\Core\Htmlizer\Helper\Data;
|
||||
use Espo\Core\Htmlizer\Helper\Result;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Modules\Advanced\Entities\Report as ReportEntity;
|
||||
use Espo\Modules\Advanced\Tools\Report\Service;
|
||||
use Espo\ORM\EntityManager;
|
||||
use RuntimeException;
|
||||
|
||||
class Report implements Helper
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private InjectableFactory $injectableFactory;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
InjectableFactory $injectableFactory
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function render(Data $data): Result
|
||||
{
|
||||
$color = $data->getOption('color');
|
||||
$fontSize = $data->getOption('fontSize');
|
||||
$border = $data->getOption('border') ?? 1;
|
||||
$borderColor = $data->getOption('borderColor');
|
||||
$cellpadding = $data->getOption('cellpadding') ?? 2;
|
||||
$column = $data->getOption('column');
|
||||
$width = $data->getOption('width') ?? '100%';
|
||||
$flip = (bool) $data->getOption('flip');
|
||||
|
||||
$id = $data->getRootContext()['id'] ?? null;
|
||||
$where = $data->getRootContext()['reportWhere'] ?? null;
|
||||
$user = $data->getRootContext()['user'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
throw new RuntimeException("No ID.");
|
||||
}
|
||||
|
||||
/** @var ?ReportEntity $report */
|
||||
$report = $this->entityManager->getEntityById(ReportEntity::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$report) {
|
||||
throw new RuntimeException("Report $id not found.");
|
||||
}
|
||||
|
||||
if (
|
||||
$report->getType() === ReportEntity::TYPE_GRID ||
|
||||
$report->getType() === ReportEntity::TYPE_JOINT_GRID
|
||||
) {
|
||||
if ($report->getGroupBy() && count($report->getGroupBy()) == 2) {
|
||||
if ($column && $report->getColumns() && count($report->getColumns())) {
|
||||
$column = $report->getColumns()[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$tableStyle = '';
|
||||
|
||||
$service = $this->injectableFactory->create(Service::class);
|
||||
|
||||
$data = $service->getReportResultsTableData($id, $where, $column, $user);
|
||||
|
||||
if ($flip) {
|
||||
$flipped = [];
|
||||
|
||||
foreach ($data as $key => $row) {
|
||||
foreach ($row as $subKey => $value) {
|
||||
$flipped[$subKey][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$data = $flipped;
|
||||
}
|
||||
|
||||
if ($borderColor) {
|
||||
$tableStyle .= "border-color: $borderColor;";
|
||||
}
|
||||
|
||||
$tableStyle .= "border-collapse: collapse; width: $width";
|
||||
|
||||
$html = "<table border=\"$border\" cellpadding=\"$cellpadding\" style=\"$tableStyle\">";
|
||||
|
||||
foreach ($data as $i => $row) {
|
||||
$html .= '<tr>';
|
||||
|
||||
foreach ($row as $item) {
|
||||
$attributes = $item['attrs'] ?? [];
|
||||
$align = $attributes['align'] ?? 'left';
|
||||
$isBold = $item['isBold'] ?? false;
|
||||
|
||||
$cellStyle = "";
|
||||
|
||||
$width = $attributes['width'] ?? null;
|
||||
$widthPart = '';
|
||||
|
||||
if ($i == 0) {
|
||||
$widthLeft = 100;
|
||||
$noWidthCount = count($row);
|
||||
|
||||
foreach ($row as $item2) {
|
||||
$attributes2 = $item2['attrs'] ?? [];
|
||||
$width2 = $attributes2['width'] ?? null;
|
||||
|
||||
if ($width2) {
|
||||
$widthLeft -= intval(substr($width2, 0, -1));
|
||||
$noWidthCount --;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$width) {
|
||||
$width = ($widthLeft / $noWidthCount) . '%';
|
||||
}
|
||||
|
||||
$widthPart = 'width = "'.(string) $width.'"';
|
||||
}
|
||||
|
||||
$value = $item['value'] ?? '';
|
||||
|
||||
if (is_array($value)) {
|
||||
$value = implode(', ', $value);
|
||||
}
|
||||
|
||||
$value = htmlspecialchars($value);
|
||||
|
||||
if ($isBold) {
|
||||
$value = '<strong>' . $value . '</strong>';
|
||||
}
|
||||
|
||||
$style = "";
|
||||
|
||||
if ($fontSize) {
|
||||
$style .= "font-size: {$fontSize}px;";
|
||||
}
|
||||
|
||||
if ($color) {
|
||||
$style .= "color: {$color};";
|
||||
}
|
||||
|
||||
if ($borderColor) {
|
||||
$style .= "border-color: {$borderColor};";
|
||||
}
|
||||
|
||||
$value = "<span style=\"{$style}\">{$value}</span>";
|
||||
|
||||
$html .= "<td align=\"{$align}\" {$widthPart} style=\"{$cellStyle}\">" . $value . '</td>';
|
||||
|
||||
}
|
||||
|
||||
$html .= '</tr>';
|
||||
}
|
||||
|
||||
$html .= '</table>';
|
||||
|
||||
return Result::createSafeString($html);
|
||||
}
|
||||
}
|
||||
120
custom/Espo/Modules/Advanced/Core/Workflow/ActionManager.php
Normal file
120
custom/Espo/Modules/Advanced/Core/Workflow/ActionManager.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?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\Core\Workflow;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Modules\Advanced\Core\Workflow\Actions\Base;
|
||||
use Exception;
|
||||
use stdClass;
|
||||
|
||||
class ActionManager extends BaseManager
|
||||
{
|
||||
protected string $dirName = 'Actions';
|
||||
protected array $requiredOptions = ['type'];
|
||||
|
||||
/**
|
||||
* @param stdClass[] $actions Actions.
|
||||
* @param array<string, mixed> $variables Formula variables to pass.
|
||||
* @param array<string, mixed> $options Save options.
|
||||
* @throws Error
|
||||
*/
|
||||
public function runActions(array $actions, array $variables = [], array $options = []): void
|
||||
{
|
||||
$this->log->debug("Workflow {$this->getWorkflowId()}: Start actions.");
|
||||
|
||||
$actualVariables = (object) [];
|
||||
|
||||
foreach ($variables as $key => $value) {
|
||||
$actualVariables->$key = $value;
|
||||
}
|
||||
|
||||
// Should be initialized before the loop.
|
||||
$processId = $this->getProcessId();
|
||||
|
||||
foreach ($actions as $action) {
|
||||
$this->runAction(
|
||||
actionData: $action,
|
||||
processId: $processId,
|
||||
variables: $actualVariables,
|
||||
options: $options,
|
||||
);
|
||||
}
|
||||
|
||||
$this->log->debug("Workflow {$this->getWorkflowId()}: End actions.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
* @throws Error
|
||||
*/
|
||||
private function runAction(
|
||||
stdClass $actionData,
|
||||
?string $processId,
|
||||
stdClass $variables,
|
||||
array $options = [],
|
||||
): void {
|
||||
|
||||
$entity = $this->getEntity($processId);
|
||||
|
||||
if (!$this->validate($actionData)) {
|
||||
$workflowId = $this->getWorkflowId($processId);
|
||||
|
||||
$this->log->warning("Workflow $workflowId: Invalid action data.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$actionImpl = $this->createConditionOrAction($actionData->type, $processId);
|
||||
|
||||
if (!$actionImpl instanceof Base) {
|
||||
throw new Error("Not action class.");
|
||||
}
|
||||
|
||||
try {
|
||||
$actionImpl->process(
|
||||
entity: $entity,
|
||||
actionData: $actionData,
|
||||
variables: $variables,
|
||||
options: $options,
|
||||
);
|
||||
|
||||
$this->copyVariables($actionImpl->getVariablesBack(), $variables);
|
||||
} catch (Exception $e) {
|
||||
$workflowId = $this->getWorkflowId($processId);
|
||||
$type = $actionData->type;
|
||||
$cid = $actionData->cid;
|
||||
|
||||
$this->log->error("Workflow $workflowId: Action failed, $type $cid, {$e->getMessage()}.");
|
||||
|
||||
throw new Error("Workflow action failed.", 500, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function copyVariables(object $source, object $destination): void
|
||||
{
|
||||
foreach (get_object_vars($destination) as $k => $v) {
|
||||
unset($destination->$k);
|
||||
}
|
||||
|
||||
foreach (get_object_vars($source) as $k => $v) {
|
||||
$destination->$k = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Modules\Advanced\Business\Workflow\AssignmentRules\LeastBusy;
|
||||
use Espo\Modules\Advanced\Business\Workflow\AssignmentRules\RoundRobin;
|
||||
use stdClass;
|
||||
|
||||
class ApplyAssignmentRule extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$entityManager = $this->entityManager;
|
||||
|
||||
$target = null;
|
||||
|
||||
if (!empty($actionData->target)) {
|
||||
$target = $actionData->target;
|
||||
}
|
||||
|
||||
if ($target === 'process') {
|
||||
$entity = $this->bpmnProcess;
|
||||
}
|
||||
else if (str_starts_with($target, 'created:')) {
|
||||
$entity = $this->getCreatedEntity($target);
|
||||
}
|
||||
|
||||
if (!$entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$entity->hasAttribute('assignedUserId') || !$entity->hasRelation('assignedUser')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$reloadedEntity = $entityManager->getEntity($entity->getEntityType(), $entity->get('id'));
|
||||
|
||||
if (empty($actionData->targetTeamId) || empty($actionData->assignmentRule)) {
|
||||
throw new Error('AssignmentRule: Not enough parameters.');
|
||||
}
|
||||
|
||||
$targetTeamId = $actionData->targetTeamId;
|
||||
$assignmentRule = $actionData->assignmentRule;
|
||||
|
||||
$targetUserPosition = null;
|
||||
|
||||
if (!empty($actionData->targetUserPosition)) {
|
||||
$targetUserPosition = $actionData->targetUserPosition;
|
||||
}
|
||||
|
||||
$listReportId = null;
|
||||
|
||||
if (!empty($actionData->listReportId)) {
|
||||
$listReportId = $actionData->listReportId;
|
||||
}
|
||||
|
||||
if (
|
||||
!in_array(
|
||||
$assignmentRule,
|
||||
$this->metadata->get('entityDefs.Workflow.assignmentRuleList', [])
|
||||
)
|
||||
) {
|
||||
throw new Error('AssignmentRule: ' . $assignmentRule . ' is not supported.');
|
||||
}
|
||||
|
||||
// @todo Use factory and interface.
|
||||
|
||||
/** @var class-string<LeastBusy|RoundRobin> $className */
|
||||
$className = 'Espo\\Modules\\Advanced\\Business\\Workflow\\AssignmentRules\\' .
|
||||
str_replace('-', '', $assignmentRule);
|
||||
|
||||
if (!class_exists($className)) {
|
||||
throw new Error('AssignmentRule: Class ' . $className . ' not found.');
|
||||
}
|
||||
|
||||
$actionId = $this->getActionData()->id ?? null;
|
||||
|
||||
if (!$actionId) {
|
||||
throw new Error("No action ID.");
|
||||
}
|
||||
|
||||
$workflowId = $this->getWorkflowId();
|
||||
|
||||
$flowchartId = null;
|
||||
|
||||
if ($this->bpmnProcess) {
|
||||
$flowchartId = $this->bpmnProcess->getFlowchartId();
|
||||
|
||||
$workflowId = null;
|
||||
}
|
||||
|
||||
$rule = $this->injectableFactory->createWith($className, [
|
||||
'actionId' => $actionId,
|
||||
'workflowId' => $workflowId,
|
||||
'flowchartId' => $flowchartId,
|
||||
'entityType' => $entity->getEntityType(),
|
||||
]);
|
||||
|
||||
$attributes = $rule->getAssignmentAttributes($entity, $targetTeamId, $targetUserPosition, $listReportId);
|
||||
|
||||
$entity->set($attributes);
|
||||
$reloadedEntity->set($attributes);
|
||||
|
||||
$entityManager->saveEntity($reloadedEntity, [
|
||||
'skipWorkflow' => true,
|
||||
'modifiedById' => 'system',
|
||||
'skipCreatedBy' => true,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
327
custom/Espo/Modules/Advanced/Core/Workflow/Actions/Base.php
Normal file
327
custom/Espo/Modules/Advanced/Core/Workflow/Actions/Base.php
Normal file
@@ -0,0 +1,327 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Formula\Manager as FormulaManager;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\ServiceFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\EntityHelper;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\FieldValueHelper;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Helper;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Utils;
|
||||
use Espo\Modules\Advanced\Entities\BpmnProcess;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\RecipientIds;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\RecipientProvider;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\TargetProvider;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
abstract class Base
|
||||
{
|
||||
private ?string $workflowId = null;
|
||||
protected ?CoreEntity $entity = null;
|
||||
protected ?stdClass $action = null;
|
||||
protected ?stdClass $createdEntitiesData = null;
|
||||
protected bool $createdEntitiesDataIsChanged = false;
|
||||
protected ?stdClass $variables = null;
|
||||
protected ?stdClass $preparedVariables = null;
|
||||
protected ?BpmnProcess $bpmnProcess = null;
|
||||
|
||||
public function __construct(
|
||||
protected EntityManager $entityManager,
|
||||
protected InjectableFactory $injectableFactory,
|
||||
protected ServiceFactory $serviceFactory,
|
||||
protected Metadata $metadata,
|
||||
protected Config $config,
|
||||
protected FormulaManager $formulaManager,
|
||||
protected User $user,
|
||||
protected Helper $workflowHelper,
|
||||
protected EntityHelper $entityHelper,
|
||||
protected FieldValueHelper $fieldValueHelper,
|
||||
protected Log $log,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options Save options.
|
||||
*/
|
||||
abstract protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function process(
|
||||
Entity $entity,
|
||||
stdClass $actionData,
|
||||
?stdClass $createdEntitiesData = null,
|
||||
?stdClass $variables = null,
|
||||
?BpmnProcess $bpmnProcess = null,
|
||||
array $options = [],
|
||||
): void {
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$this->entity = $entity;
|
||||
$this->action = $actionData;
|
||||
$this->createdEntitiesData = $createdEntitiesData;
|
||||
$this->variables = $variables;
|
||||
$this->bpmnProcess = $bpmnProcess;
|
||||
|
||||
if (!property_exists($actionData, 'cid')) {
|
||||
$actionData->cid = 0;
|
||||
}
|
||||
|
||||
$cid = $actionData->cid ?? 0;
|
||||
$actionType = $actionData->type;
|
||||
|
||||
$this->debugLog('Start', $actionType, $cid, $entity);
|
||||
|
||||
$result = $this->run($entity, $actionData, $options);
|
||||
|
||||
$this->debugLog('End', $actionType, $cid, $entity);
|
||||
|
||||
if (!$result) {
|
||||
$this->debugLog('Failed', $actionType, $cid, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
private function debugLog(string $type, string $actionType, int $cid, Entity $entity): void
|
||||
{
|
||||
$id = $entity->hasId() ? $entity->getId() : '(new)';
|
||||
|
||||
$message = "Workflow {$this->getWorkflowId()}, $actionType, $type, cid $cid, {$entity->getEntityType()} $id";
|
||||
|
||||
$GLOBALS['log']->debug($message);
|
||||
}
|
||||
|
||||
public function isCreatedEntitiesDataChanged(): bool
|
||||
{
|
||||
return $this->createdEntitiesDataIsChanged;
|
||||
}
|
||||
|
||||
public function getCreatedEntitiesData(): stdClass
|
||||
{
|
||||
return $this->createdEntitiesData;
|
||||
}
|
||||
|
||||
public function setWorkflowId(?string $workflowId): void
|
||||
{
|
||||
$this->workflowId = $workflowId;
|
||||
}
|
||||
|
||||
protected function getWorkflowId(): ?string
|
||||
{
|
||||
return $this->workflowId;
|
||||
}
|
||||
|
||||
protected function getEntity(): CoreEntity
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
protected function getActionData(): stdClass
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
protected function clearSystemVariables(stdClass $variables): void
|
||||
{
|
||||
unset($variables->__targetEntity);
|
||||
unset($variables->__processEntity);
|
||||
unset($variables->__createdEntitiesData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return variables. Can be changed after action is processed.
|
||||
*/
|
||||
public function getVariablesBack(): stdClass
|
||||
{
|
||||
$variables = clone $this->variables;
|
||||
|
||||
$this->clearSystemVariables($variables);
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get variables for usage within an action.
|
||||
*/
|
||||
public function getVariables(): stdClass
|
||||
{
|
||||
$variables = clone $this->getFormulaVariables();
|
||||
|
||||
$this->clearSystemVariables($variables);
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
protected function hasVariables(): bool
|
||||
{
|
||||
return !!$this->variables;
|
||||
}
|
||||
|
||||
protected function updateVariables(stdClass $variables): void
|
||||
{
|
||||
if (!$this->hasVariables()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$variables = clone $variables;
|
||||
|
||||
$this->clearSystemVariables($variables);
|
||||
|
||||
foreach (get_object_vars($variables) as $k => $v) {
|
||||
$this->variables->$k = $v;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFormulaVariables(): stdClass
|
||||
{
|
||||
if (!$this->preparedVariables) {
|
||||
$o = (object) [];
|
||||
|
||||
$o->__targetEntity = $this->getEntity();
|
||||
|
||||
if ($this->bpmnProcess) {
|
||||
$o->__processEntity = $this->bpmnProcess;
|
||||
}
|
||||
|
||||
if ($this->createdEntitiesData) {
|
||||
$o->__createdEntitiesData = $this->createdEntitiesData;
|
||||
}
|
||||
|
||||
if ($this->variables) {
|
||||
foreach (get_object_vars($this->variables) as $k => $v) {
|
||||
$o->$k = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$this->preparedVariables = $o;
|
||||
}
|
||||
|
||||
return $this->preparedVariables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get execute time defined in workflow
|
||||
*
|
||||
* @param stdClass $data
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getExecuteTime($data): string
|
||||
{
|
||||
$executeTime = date(DateTime::SYSTEM_DATE_TIME_FORMAT);
|
||||
|
||||
if (!property_exists($data, 'execution')) {
|
||||
return $executeTime;
|
||||
}
|
||||
|
||||
$execution = $data->execution;
|
||||
|
||||
switch ($execution->type) {
|
||||
case 'immediately':
|
||||
return $executeTime;
|
||||
|
||||
case 'later':
|
||||
$field = $execution->field ?? null;
|
||||
|
||||
if ($field) {
|
||||
$executeTime = $this->fieldValueHelper->getValue($this->getEntity(), $field);
|
||||
|
||||
$attributeType = Utils::getAttributeType($this->getEntity(), $field);
|
||||
$timezone = $this->config->get('timeZone') ?? 'UTC';
|
||||
|
||||
if ($attributeType === 'date') {
|
||||
$executeTime = (new DateTimeImmutable($executeTime))
|
||||
->setTimezone(new DateTimeZone($timezone))
|
||||
->setTime(0, 0)
|
||||
->setTimezone(new DateTimeZone('UTC'))
|
||||
->format(DateTime::SYSTEM_DATE_TIME_FORMAT);
|
||||
}
|
||||
}
|
||||
|
||||
$execution->shiftDays = $execution->shiftDays ?? 0;
|
||||
$shiftUnit = $execution->shiftUnit ?? 'days';
|
||||
|
||||
$executeTime = Utils::shiftDays(
|
||||
$execution->shiftDays,
|
||||
$executeTime,
|
||||
'datetime',
|
||||
$shiftUnit
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Workflow[{$this->getWorkflowId()}]: Unknown execution type [$execution->type]");
|
||||
}
|
||||
|
||||
return $executeTime;
|
||||
}
|
||||
|
||||
protected function getCreatedEntity(string $target): ?Entity
|
||||
{
|
||||
$provider = $this->injectableFactory->create(TargetProvider::class);
|
||||
|
||||
return $provider->getCreated($target, $this->createdEntitiesData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getFirstTargetFromTargetItem(Entity $entity, ?string $target): ?Entity
|
||||
{
|
||||
foreach ($this->getTargetsFromTargetItem($entity, $target) as $it) {
|
||||
return $it;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<Entity>
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getTargetsFromTargetItem(Entity $entity, ?string $target): iterable
|
||||
{
|
||||
$provider = $this->injectableFactory->create(TargetProvider::class);
|
||||
|
||||
return $provider->get($entity, $target, $this->createdEntitiesData);
|
||||
}
|
||||
|
||||
protected function getRecipients(Entity $entity, string $target): RecipientIds
|
||||
{
|
||||
$provider = $this->injectableFactory->create(RecipientProvider::class);
|
||||
|
||||
return $provider->get($entity, $target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\DateTime;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Utils;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Repositories\Attachment as AttachmentRepository;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
abstract class BaseEntity extends Base
|
||||
{
|
||||
/**
|
||||
* Get value of a field by a field name.
|
||||
*
|
||||
* @return stdClass|string|null
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getValue(string $field, Entity $targetEntity): mixed
|
||||
{
|
||||
$actionData = $this->getActionData();
|
||||
$entity = $this->getEntity();
|
||||
|
||||
if (!$targetEntity instanceof CoreEntity) {
|
||||
throw new RuntimeException("No Core Entity.");
|
||||
}
|
||||
|
||||
if (!isset($actionData->fields->$field)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fieldParams = $actionData->fields->$field;
|
||||
|
||||
/** @var stdClass|null $values */
|
||||
$values = null;
|
||||
|
||||
switch ($fieldParams->subjectType) {
|
||||
case 'value':
|
||||
if (isset($fieldParams->attributes) && $fieldParams->attributes instanceof stdClass) {
|
||||
$values = $fieldParams->attributes;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'field':
|
||||
$values = $this->entityHelper->getFieldValues(
|
||||
fromEntity: $entity,
|
||||
toEntity: $targetEntity,
|
||||
fromField: $fieldParams->field,
|
||||
toField: $field,
|
||||
);
|
||||
|
||||
$toShift = isset($fieldParams->shiftDays) || isset($fieldParams->shiftUnit);
|
||||
|
||||
if ($toShift) {
|
||||
$shiftDays = $fieldParams->shiftDays ?? 0;
|
||||
$shiftUnit = $fieldParams->shiftUnit ?? null;
|
||||
$timezone = $this->config->get('timeZone');
|
||||
|
||||
foreach (get_object_vars($values) as $attribute => $value) {
|
||||
$attributeType = $targetEntity->getAttributeType($attribute) ?? 'datetime';
|
||||
|
||||
if (!in_array($attributeType, ['date', 'datetime'])) {
|
||||
$attributeType = 'date';
|
||||
}
|
||||
|
||||
/** @var 'date'|'datetime' $attributeType */
|
||||
|
||||
$values->$attribute = Utils::shiftDays(
|
||||
shiftDays: $shiftDays,
|
||||
input: $value,
|
||||
type: $attributeType,
|
||||
unit: $shiftUnit,
|
||||
timezone: $timezone,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'today':
|
||||
$attributeType = Utils::getAttributeType($targetEntity, $field);
|
||||
$shiftUnit = $fieldParams->shiftUnit ?? 'days';
|
||||
$timezone = $this->config->get('timeZone');
|
||||
|
||||
if (!in_array($attributeType, ['date', 'datetime'])) {
|
||||
$attributeType = 'datetime';
|
||||
}
|
||||
|
||||
/** @var 'date'|'datetime' $attributeType */
|
||||
|
||||
return Utils::shiftDays(
|
||||
shiftDays: $fieldParams->shiftDays,
|
||||
type: $attributeType,
|
||||
unit: $shiftUnit,
|
||||
timezone: $timezone,
|
||||
);
|
||||
|
||||
default:
|
||||
throw new Error( "Workflow[{$this->getWorkflowId()}]: Unknown fieldName for a field '$field'.");
|
||||
}
|
||||
|
||||
$fieldType = $this->entityHelper->getFieldType($targetEntity, $field);
|
||||
|
||||
$actionType = $fieldParams->actionType ?? null;
|
||||
|
||||
if (
|
||||
($actionType === 'add' || $actionType === 'remove') &&
|
||||
$values instanceof stdClass &&
|
||||
in_array($fieldType, [
|
||||
FieldType::LINK_MULTIPLE,
|
||||
FieldType::ARRAY,
|
||||
FieldType::MULTI_ENUM,
|
||||
FieldType::CHECKLIST,
|
||||
])
|
||||
) {
|
||||
if ($fieldType === FieldType::LINK_MULTIPLE) {
|
||||
$attr = $field . 'Ids';
|
||||
$setIds = $targetEntity->getLinkMultipleIdList($field);
|
||||
} else {
|
||||
$attr = $field;
|
||||
$setIds = $targetEntity->get($attr) ?? [];
|
||||
}
|
||||
|
||||
$ids = $values->$attr ?? [];
|
||||
|
||||
if ($actionType === 'remove') {
|
||||
$values->$attr = array_values(array_unique(array_diff($setIds, $ids)));
|
||||
} else {
|
||||
$values->$attr = array_values(array_unique(array_merge($setIds, $ids)));
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data to fill.
|
||||
*
|
||||
* @param array<string, mixed>|stdClass|null $fields
|
||||
* @return array<string, mixed>
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getDataToFill(Entity $entity, $fields): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
if (empty($fields)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$metadataFields = $this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields']);
|
||||
$metadataFieldList = array_keys($metadataFields);
|
||||
|
||||
if ($fields instanceof stdClass) {
|
||||
$fields = get_object_vars($fields);
|
||||
}
|
||||
|
||||
foreach ($fields as $field => $fieldParams) {
|
||||
$fieldType = $this->entityHelper->getFieldType($entity, $field);
|
||||
|
||||
if ($fieldType === FieldType::ATTACHMENT_MULTIPLE) {
|
||||
$data = $this->getDataToFillAttachmentMultiple($field, $entity, $data);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$fieldType === FieldType::FILE ||
|
||||
$fieldType === FieldType::IMAGE
|
||||
) {
|
||||
$data = $this->getDataToFillFile($field, $entity, $data);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$entity->hasRelation($field) ||
|
||||
$entity->hasAttribute($field) ||
|
||||
in_array($field, $metadataFieldList)
|
||||
) {
|
||||
$fieldValue = $this->getValue($field, $entity);
|
||||
|
||||
if (is_object($fieldValue)) {
|
||||
$data = array_merge($data, get_object_vars($fieldValue));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[$field] = $fieldValue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($fields as $field => $fieldParams) {
|
||||
$fieldType = $this->entityHelper->getFieldType($entity, $field);
|
||||
|
||||
if ($fieldType === 'duration') {
|
||||
$this->fillDataDuration($field, $entity, $data);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
* @throws Error
|
||||
*/
|
||||
private function getDataToFillAttachmentMultiple(string $field, CoreEntity $entity, array $data): array
|
||||
{
|
||||
if (!$entity->hasLinkMultipleField($field)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$attachmentData = $this->getValue($field, $entity);
|
||||
|
||||
if (!$attachmentData instanceof stdClass) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$copiedIdList = [];
|
||||
$idListFieldName = $field . 'Ids';
|
||||
|
||||
/** @var AttachmentRepository $repository */
|
||||
$repository = $this->entityManager->getRepository(Attachment::ENTITY_TYPE);
|
||||
|
||||
if (is_array($attachmentData->$idListFieldName)) {
|
||||
foreach ($attachmentData->$idListFieldName as $attachmentId) {
|
||||
$attachment = $this->entityManager
|
||||
->getRDBRepositoryByClass(Attachment::class)
|
||||
->getById($attachmentId);
|
||||
|
||||
if (!$attachment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attachment = $repository->getCopiedAttachment($attachment);
|
||||
$attachment->setTargetField($field);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
$copiedIdList[] = $attachment->getId();
|
||||
}
|
||||
}
|
||||
|
||||
$attachmentData->$idListFieldName = $copiedIdList;
|
||||
|
||||
return array_merge($data, get_object_vars($attachmentData));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
* @throws Error
|
||||
*/
|
||||
private function getDataToFillFile(string $field, CoreEntity $entity, array $data): array
|
||||
{
|
||||
$idAttr = $field . 'Id';
|
||||
$nameAttr = $field . 'Name';
|
||||
|
||||
$inputData = $this->getValue($field, $entity) ?? (object) [];
|
||||
|
||||
$id = $inputData->$idAttr ?? null;
|
||||
|
||||
if (!$id) {
|
||||
return array_merge($data, [
|
||||
$idAttr => null,
|
||||
$nameAttr => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @var AttachmentRepository $repository */
|
||||
$repository = $this->entityManager->getRepository(Attachment::ENTITY_TYPE);
|
||||
|
||||
$attachment = $this->entityManager
|
||||
->getRDBRepositoryByClass(Attachment::class)
|
||||
->getById($id);
|
||||
|
||||
if (!$attachment) {
|
||||
return array_merge($data, [
|
||||
$idAttr => null,
|
||||
$nameAttr => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$copy = $repository->getCopiedAttachment($attachment);
|
||||
$copy->setTargetField($field);
|
||||
|
||||
$this->entityManager->saveEntity($copy);
|
||||
|
||||
return array_merge($data, [
|
||||
$idAttr => $copy->getId(),
|
||||
$nameAttr => $copy->getName(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function fillDataDuration(string $field, CoreEntity $entity, array &$data): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$duration = $data[$field];
|
||||
|
||||
if (!is_int($duration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$startField = $this->metadata->get("entityDefs.$entityType.fields.$field.start");
|
||||
$endField = $this->metadata->get("entityDefs.$entityType.fields.$field.end");
|
||||
|
||||
$startDateAttribute = $startField . 'Date';
|
||||
$endDateAttribute = $endField . 'Date';
|
||||
|
||||
$start = $data[$startField] ?? null;
|
||||
$startDate =$data[$startDateAttribute] ?? null;
|
||||
|
||||
if ($start) {
|
||||
$dateEnd = (new DateTimeImmutable($start))
|
||||
->modify("+$duration seconds")
|
||||
->format(DateTime::SYSTEM_DATE_TIME_FORMAT);
|
||||
|
||||
/** @phpstan-ignore-next-line parameterByRef.type */
|
||||
$data[$endField] = $dateEnd;
|
||||
}
|
||||
|
||||
if ($startDate) {
|
||||
$days = floor($duration / (3600 * 24));
|
||||
|
||||
$dateEndDate = (new DateTimeImmutable($startDate))
|
||||
->modify("+$days days")
|
||||
->format(DateTime::SYSTEM_DATE_FORMAT);
|
||||
|
||||
/** @phpstan-ignore-next-line parameterByRef.type */
|
||||
$data[$endDateAttribute] = $dateEndDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\ORM\Entity;
|
||||
use stdClass;
|
||||
|
||||
class CreateEntity extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$entityType = $actionData->link;
|
||||
|
||||
$newEntity = $this->entityManager->getNewEntity($entityType);
|
||||
|
||||
$data = $this->getDataToFill($newEntity, $actionData->fields);
|
||||
$newEntity->set($data);
|
||||
|
||||
$formula = $actionData->formula ?? null;
|
||||
|
||||
if ($formula) {
|
||||
$this->formulaManager->run($formula, $newEntity, $this->getFormulaVariables());
|
||||
}
|
||||
|
||||
if (isset($actionData->linkList) && count($actionData->linkList)) {
|
||||
foreach ($actionData->linkList as $link) {
|
||||
if ($newEntity->getRelationType($link) === Entity::BELONGS_TO) {
|
||||
$newEntity->set($link . 'Id', $entity->getId());
|
||||
} else if ($newEntity->getRelationType($link) === Entity::BELONGS_TO_PARENT) {
|
||||
$newEntity->set($link . 'Id', $entity->getId());
|
||||
$newEntity->set($link . 'Type', $entity->getEntityType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$saveOptions = [
|
||||
'workflowId' => $this->getWorkflowId(),
|
||||
'createdById' => $newEntity->get('createdById') ?? 'system',
|
||||
];
|
||||
|
||||
$this->entityManager->saveEntity($newEntity, $saveOptions);
|
||||
|
||||
if (isset($actionData->linkList) && count($actionData->linkList)) {
|
||||
foreach ($actionData->linkList as $link) {
|
||||
if (
|
||||
!in_array(
|
||||
$newEntity->getRelationType($link),
|
||||
[$newEntity::BELONGS_TO, Entity::BELONGS_TO_PARENT]
|
||||
)
|
||||
) {
|
||||
$this->entityManager
|
||||
->getRDBRepository($newEntity->getEntityType())
|
||||
->getRelation($newEntity, $link)
|
||||
->relate($entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->createdEntitiesData && !empty($actionData->elementId) && !empty($actionData->id)) {
|
||||
$this->createdEntitiesDataIsChanged = true;
|
||||
|
||||
$alias = $actionData->elementId . '_' . $actionData->id;
|
||||
|
||||
$this->createdEntitiesData->$alias = (object) [
|
||||
'entityType' => $newEntity->getEntityType(),
|
||||
'entityId' => $newEntity->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->variables) {
|
||||
$this->variables->__lastCreatedEntityId = $newEntity->getId();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Entities\Notification;
|
||||
use Espo\Entities\User;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class CreateNotification extends Base
|
||||
{
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
if (empty($actionData->recipient)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($actionData->messageTemplate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userList = [];
|
||||
|
||||
switch ($actionData->recipient) {
|
||||
case 'specifiedUsers':
|
||||
if (empty($actionData->userIdList) || !is_array($actionData->userIdList)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userIds = $actionData->userIdList;
|
||||
|
||||
break;
|
||||
|
||||
case 'specifiedTeams':
|
||||
$userIds = $this->workflowHelper->getUserIdsByTeamIds($actionData->specifiedTeamsIds);
|
||||
|
||||
break;
|
||||
|
||||
case 'teamUsers':
|
||||
$entity->loadLinkMultipleField('teams');
|
||||
$userIds = $this->workflowHelper->getUserIdsByTeamIds($entity->get('teamsIds'));
|
||||
|
||||
break;
|
||||
|
||||
case 'followers':
|
||||
$userIds = $this->workflowHelper->getFollowerUserIds($entity);
|
||||
|
||||
break;
|
||||
|
||||
case 'followersExcludingAssignedUser':
|
||||
$userIds = $this->workflowHelper->getFollowerUserIdsExcludingAssignedUser($entity);
|
||||
break;
|
||||
|
||||
case 'currentUser':
|
||||
$userIds = [$this->user->getId()];
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$userIds = $this->getRecipients($this->getEntity(), $actionData->recipient)->getIds();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
$userList[] = $user;
|
||||
}
|
||||
|
||||
$message = $actionData->messageTemplate;
|
||||
|
||||
$variables = $this->getVariables();
|
||||
|
||||
foreach (get_object_vars($variables) as $key => $value) {
|
||||
if (is_string($value) || is_int($value) || is_float($value)) {
|
||||
if (is_int($value) || is_float($value)) {
|
||||
$value = strval($value);
|
||||
} else {
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$message = str_replace('{$$' . $key . '}', $value, $message);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($userList as $user) {
|
||||
$notification = $this->entityManager->getNewEntity(Notification::ENTITY_TYPE);
|
||||
|
||||
$notification->set([
|
||||
'type' => Notification::TYPE_MESSAGE,
|
||||
'data' => [
|
||||
'entityId' => $entity->getId(),
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'entityName' => $entity->get('name'),
|
||||
'userId' => $this->user->getId(),
|
||||
'userName' => $this->user->getName(),
|
||||
],
|
||||
'userId' => $user->getId(),
|
||||
'message' => $message,
|
||||
'relatedId' => $entity->getId(),
|
||||
'relatedType' => $entity->getEntityType(),
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($notification);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Utils\Util;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class CreateRelatedEntity extends CreateEntity
|
||||
{
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$linkEntityType = $this->getLinkEntityType($entity, $actionData->link);
|
||||
|
||||
if (!isset($linkEntityType)) {
|
||||
$GLOBALS['log']->error(
|
||||
'Workflow\Actions\\'.$actionData->type.': ' .
|
||||
'Cannot find an entity type of the link ['.$actionData->link.'] in the entity ' .
|
||||
'[' . $entity->getEntityType() . '].');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$newEntity = $this->entityManager->getNewEntity($linkEntityType);
|
||||
|
||||
$data = $this->getDataToFill($newEntity, $actionData->fields);
|
||||
|
||||
$newEntity->set($data);
|
||||
|
||||
$link = $actionData->link;
|
||||
|
||||
$linkType = $entity->getRelationParam($link, 'type');
|
||||
|
||||
$isRelated = false;
|
||||
|
||||
if ($foreignLink = $entity->getRelationParam($link, 'foreign')) {
|
||||
$foreignRelationType = $newEntity->getRelationType($foreignLink);
|
||||
|
||||
if (in_array($foreignRelationType, [Entity::BELONGS_TO, Entity::BELONGS_TO_PARENT])) {
|
||||
if ($foreignRelationType === Entity::BELONGS_TO) {
|
||||
$newEntity->set($foreignLink. 'Id', $entity->getId());
|
||||
|
||||
$isRelated = true;
|
||||
} else if ($foreignRelationType === 'belongsToParent') {
|
||||
$newEntity->set($foreignLink. 'Id', $entity->getId());
|
||||
$newEntity->set($foreignLink. 'Type', $entity->getEntityType());
|
||||
|
||||
$isRelated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$newEntity->set('id', $this->generateId());
|
||||
|
||||
if (!empty($actionData->formula)) {
|
||||
$this->formulaManager->run($actionData->formula, $newEntity, $this->getFormulaVariables());
|
||||
}
|
||||
|
||||
$saveOptions = [
|
||||
'workflowId' => $this->getWorkflowId(),
|
||||
'createdById' => $newEntity->get('createdById') ?? 'system',
|
||||
];
|
||||
|
||||
$this->entityManager->saveEntity($newEntity, $saveOptions);
|
||||
|
||||
$newEntityId = $newEntity->getId();
|
||||
|
||||
$newEntity = $this->entityManager->getEntityById($newEntity->getEntityType(), $newEntityId);
|
||||
|
||||
if (!$isRelated && $linkType === Entity::BELONGS_TO) {
|
||||
$entity->set($link . 'Id', $newEntity->getId());
|
||||
$entity->set($link . 'Name', $newEntity->get('name'));
|
||||
|
||||
$this->entityManager->saveEntity($entity, [
|
||||
'skipWorkflow' => true,
|
||||
'noStream' => true,
|
||||
'noNotifications' => true,
|
||||
'skipModifiedBy' => true,
|
||||
'skipCreatedBy' => true,
|
||||
'skipHooks' => true,
|
||||
'silent' => true,
|
||||
]);
|
||||
|
||||
$isRelated = true;
|
||||
}
|
||||
|
||||
if (!$isRelated) {
|
||||
$this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $actionData->link)
|
||||
->relate($newEntity);
|
||||
}
|
||||
|
||||
if ($this->createdEntitiesData && !empty($actionData->elementId) && !empty($actionData->id)) {
|
||||
$this->createdEntitiesDataIsChanged = true;
|
||||
|
||||
$alias = $actionData->elementId . '_' . $actionData->id;
|
||||
|
||||
$this->createdEntitiesData->$alias = (object) [
|
||||
'entityType' => $newEntity->getEntityType(),
|
||||
'entityId' => $newEntity->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->variables) {
|
||||
$this->variables->__lastCreatedEntityId = $newEntity->getId();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function generateId(): string
|
||||
{
|
||||
if (
|
||||
interface_exists('Espo\\Core\\Utils\\Id\\RecordIdGenerator') &&
|
||||
method_exists($this->injectableFactory, 'createResolved') /** @phpstan-ignore-line */
|
||||
) {
|
||||
return $this->injectableFactory->createResolved('Espo\\Core\\Utils\\Id\\RecordIdGenerator')
|
||||
->generate();
|
||||
}
|
||||
|
||||
return Util::generateId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an Entity name of a link.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
protected function getLinkEntityType(CoreEntity $entity, string $linkName)
|
||||
{
|
||||
if ($entity->hasRelation($linkName)) {
|
||||
return $entity->getRelationParam($linkName, 'entity');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\ORM\Repository\Option\SaveContext;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\SaveContextHelper;
|
||||
use stdClass;
|
||||
|
||||
class ExecuteFormula extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
if (empty($actionData->formula)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$reloadedEntity = $this->entityManager->getEntityById($entity->getEntityType(), $entity->getId());
|
||||
|
||||
$variables = $this->getFormulaVariables();
|
||||
|
||||
$this->formulaManager->run($actionData->formula, $reloadedEntity, $variables);
|
||||
|
||||
$this->updateVariables($variables);
|
||||
|
||||
$isChanged = false;
|
||||
|
||||
$changedMap = (object) [];
|
||||
|
||||
foreach ($reloadedEntity->getAttributeList() as $attribute) {
|
||||
if ($reloadedEntity->isAttributeChanged($attribute)) {
|
||||
$changedMap->$attribute = $reloadedEntity->get($attribute);
|
||||
|
||||
$isChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isChanged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$saveOptions = [
|
||||
'modifiedById' => 'system',
|
||||
'skipWorkflow' => !$this->bpmnProcess,
|
||||
'workflowId' => $this->getWorkflowId(),
|
||||
'context' => SaveContextHelper::createDerived($options),
|
||||
];
|
||||
|
||||
$this->entityManager->saveEntity($reloadedEntity, $saveOptions);
|
||||
|
||||
$entity->set($changedMap);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\Stream\Service as StreamService;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class MakeFollowed extends BaseEntity
|
||||
{
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
if (empty($actionData->whatToFollow)) {
|
||||
$actionData->whatToFollow = 'targetEntity';
|
||||
}
|
||||
|
||||
$target = $actionData->whatToFollow;
|
||||
|
||||
$targetEntity = null;
|
||||
|
||||
if ($target === 'targetEntity') {
|
||||
$targetEntity = $entity;
|
||||
}
|
||||
else if (str_starts_with($target, 'created:')) {
|
||||
$targetEntity = $this->getCreatedEntity($target);
|
||||
}
|
||||
else {
|
||||
$link = $target;
|
||||
|
||||
if (str_starts_with($target, 'link:')) {
|
||||
$link = substr($target, 5);
|
||||
}
|
||||
|
||||
$type = $this->metadata
|
||||
->get("entityDefs.{$entity->getEntityType()}.links.$link.type");
|
||||
|
||||
if (empty($type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$idField = $link . 'Id';
|
||||
|
||||
if ($type == Entity::BELONGS_TO) {
|
||||
if (!$entity->get($idField)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$foreignEntityType = $this->metadata
|
||||
->get("entityDefs.{$entity->getEntityType()}.links.$link.entity");
|
||||
|
||||
if (empty($foreignEntityType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$targetEntity = $this->entityManager
|
||||
->getEntityById($foreignEntityType, $entity->get($idField));
|
||||
}
|
||||
else if ($type === Entity::BELONGS_TO_PARENT) {
|
||||
$typeField = $link . 'Type';
|
||||
|
||||
if (!$entity->get($idField)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$entity->get($typeField)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$targetEntity = $this->entityManager
|
||||
->getEntityById($entity->get($typeField), $entity->get($idField));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$targetEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userIdList = $this->getUserIdList($actionData);
|
||||
|
||||
$streamService = $this->injectableFactory->create(StreamService::class);
|
||||
|
||||
$streamService->followEntityMass($targetEntity, $userIdList);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getUserIdList(stdClass $actionData): array
|
||||
{
|
||||
$entity = $this->getEntity();
|
||||
|
||||
if (!empty($actionData->recipient)) {
|
||||
$recipient = $actionData->recipient;
|
||||
} else {
|
||||
$recipient = 'specifiedUsers';
|
||||
}
|
||||
|
||||
$userIdList = [];
|
||||
|
||||
if (isset($actionData->userIdList) && is_array($actionData->userIdList)) {
|
||||
$userIdList = $actionData->userIdList;
|
||||
}
|
||||
|
||||
$teamIdList = [];
|
||||
|
||||
if (isset($actionData->specifiedTeamsIds) && is_array($actionData->specifiedTeamsIds)) {
|
||||
$teamIdList = $actionData->specifiedTeamsIds;
|
||||
}
|
||||
|
||||
return match ($recipient) {
|
||||
'specifiedUsers' => $userIdList,
|
||||
'specifiedTeams' => $this->workflowHelper->getUserIdsByTeamIds($teamIdList),
|
||||
'currentUser' => [$this->user->getId()],
|
||||
'teamUsers' => $this->workflowHelper->getUserIdsByTeamIds($entity->getLinkMultipleIdList('teams')),
|
||||
'followers' => $this->workflowHelper->getFollowerUserIds($entity),
|
||||
default => $this->getRecipients($this->getEntity(), $actionData->recipient)->getIds(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\SaveContextHelper;
|
||||
use stdClass;
|
||||
|
||||
class RelateWithEntity extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
if (empty($actionData->entityId) || empty($actionData->link)) {
|
||||
throw new Error('Workflow['.$this->getWorkflowId().']: Bad params defined for RelateWithEntity');
|
||||
}
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($actionData->link, 'entity');
|
||||
|
||||
if (!$foreignEntityType) {
|
||||
throw new Error('Workflow['.$this->getWorkflowId().
|
||||
']: Could not find foreign entity type for RelateWithEntity');
|
||||
}
|
||||
|
||||
$foreignEntity = $this->entityManager->getEntityById($foreignEntityType, $actionData->entityId);
|
||||
|
||||
if (!$foreignEntity) {
|
||||
throw new Error('Workflow['.$this->getWorkflowId().
|
||||
']: Could not find foreign entity for RelateWithEntity');
|
||||
}
|
||||
|
||||
$relateOptions = [
|
||||
'context' => SaveContextHelper::obtainFromRawOptions($options),
|
||||
];
|
||||
|
||||
$this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $actionData->link)
|
||||
->relate($foreignEntity, null, $relateOptions);
|
||||
|
||||
if ($entity->hasLinkMultipleField($actionData->link)) {
|
||||
$entity->loadLinkMultipleField($actionData->link);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Action\RunAction\ServiceAction;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Utils\Json;
|
||||
use JsonException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class RunService extends Base
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws JsonException
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
if (empty($actionData->methodName)) {
|
||||
throw new Error("No service action name.");
|
||||
}
|
||||
|
||||
$name = $actionData->methodName;
|
||||
|
||||
if (!is_string($name)) {
|
||||
throw new Error("Bad service action name.");
|
||||
}
|
||||
|
||||
$target = $actionData->target ?? 'targetEntity';
|
||||
|
||||
$targetEntity = $this->getFirstTargetFromTargetItem($entity, $target);
|
||||
|
||||
if (!$targetEntity) {
|
||||
$this->log->notice("Workflow Run Service Action: No target {target} found.", ['target' => $target]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = null;
|
||||
|
||||
if (!empty($actionData->additionalParameters)) {
|
||||
$data = Json::decode($actionData->additionalParameters);
|
||||
}
|
||||
|
||||
$variables = null;
|
||||
|
||||
if ($this->hasVariables()) {
|
||||
$variables = $this->getVariables();
|
||||
}
|
||||
|
||||
$output = null;
|
||||
|
||||
$targetEntityType = $targetEntity->getEntityType();
|
||||
|
||||
$className = $this->getClassName($targetEntityType, $name);
|
||||
|
||||
if ($className) {
|
||||
$serviceAction = $this->injectableFactory->create($className);
|
||||
|
||||
$output = $serviceAction->run($targetEntity, $data);
|
||||
}
|
||||
|
||||
// Legacy.
|
||||
if (!$className) {
|
||||
$this->runLegacy($targetEntityType, $name, $targetEntity, $data, $variables);
|
||||
}
|
||||
|
||||
if (!$this->hasVariables()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$variables->__lastServiceActionOutput = $output;
|
||||
|
||||
$this->updateVariables($variables);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data
|
||||
* @throws Error
|
||||
*/
|
||||
private function runLegacy(
|
||||
string $targetEntityType,
|
||||
string $name,
|
||||
Entity $targetEntity,
|
||||
$data,
|
||||
?stdClass $variables
|
||||
): void {
|
||||
|
||||
$serviceName = $this->metadata
|
||||
->get(['entityDefs', 'Workflow', 'serviceActions', $targetEntityType, $name, 'serviceName']);
|
||||
|
||||
$methodName = $this->metadata
|
||||
->get(['entityDefs', 'Workflow', 'serviceActions', $targetEntityType, $name, 'methodName']);
|
||||
|
||||
if (!$serviceName || !$methodName) {
|
||||
$methodName = $name;
|
||||
$serviceName = $targetEntity->getEntityType();
|
||||
}
|
||||
|
||||
$serviceFactory = $this->serviceFactory;
|
||||
|
||||
if (!$serviceFactory->checkExists($serviceName)) {
|
||||
throw new Error("No service $serviceName.");
|
||||
}
|
||||
|
||||
$service = $serviceFactory->create($serviceName);
|
||||
|
||||
if (!method_exists($service, $methodName)) {
|
||||
throw new Error("No method $methodName.");
|
||||
}
|
||||
|
||||
$service->$methodName(
|
||||
$this->getWorkflowId(),
|
||||
$targetEntity,
|
||||
$data,
|
||||
$this->bpmnProcess,
|
||||
$variables ?? (object)[]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<ServiceAction<CoreEntity>>
|
||||
*/
|
||||
private function getClassName(string $targetEntityType, string $name): ?string
|
||||
{
|
||||
/** @var ?class-string<ServiceAction<CoreEntity>> $className */
|
||||
$className =
|
||||
$this->metadata->get("app.workflow.serviceActions.$targetEntityType.$name.className") ??
|
||||
$this->metadata->get("app.workflow.serviceActions.Global.$name.className");
|
||||
|
||||
return $className;
|
||||
}
|
||||
}
|
||||
321
custom/Espo/Modules/Advanced/Core/Workflow/Actions/SendEmail.php
Normal file
321
custom/Espo/Modules/Advanced/Core/Workflow/Actions/SendEmail.php
Normal file
@@ -0,0 +1,321 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Job\QueueName;
|
||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Job;
|
||||
use Espo\Entities\Team;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Jobs\SendEmail as SendEmailJob;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\SendEmailService;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\Repositories\Email as EmailRepository;
|
||||
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
class SendEmail extends Base
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$jobData = [
|
||||
'workflowId' => $this->getWorkflowId(),
|
||||
'entityId' => $this->getEntity()->getId(),
|
||||
'entityType' => $this->getEntity()->getEntityType(),
|
||||
'from' => $this->getEmailAddressData('from'),
|
||||
'to' => $this->getEmailAddressData('to'),
|
||||
'replyTo' => $this->getEmailAddressData('replyTo'),
|
||||
'cc' => $this->getEmailAddressData('cc'),
|
||||
'emailTemplateId' => $actionData->emailTemplateId ?? null,
|
||||
'doNotStore' => $actionData->doNotStore ?? false,
|
||||
'optOutLink' => $actionData->optOutLink ?? false,
|
||||
];
|
||||
|
||||
if ($this->bpmnProcess) {
|
||||
$jobData['processId'] = $this->bpmnProcess->getId();
|
||||
}
|
||||
|
||||
$attachmentsVariable = $actionData->attachmentsVariable ?? null;
|
||||
|
||||
$jobData['attachmentIds'] = $this->getAttachmentIds($attachmentsVariable);
|
||||
|
||||
if (is_null($jobData['to'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($actionData->processImmediately)) {
|
||||
$storeSentEmailData = !!$this->createdEntitiesData && !$jobData['doNotStore'];
|
||||
|
||||
if ($storeSentEmailData) {
|
||||
$jobData['returnEmailId'] = true;
|
||||
}
|
||||
|
||||
if ($this->hasVariables()) {
|
||||
$jobData['variables'] = $this->getVariables();
|
||||
}
|
||||
|
||||
$service = $this->injectableFactory->create(SendEmailService::class);
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
$jobData = json_decode(json_encode($jobData));
|
||||
|
||||
$emailId = $service->send($jobData);
|
||||
|
||||
if (
|
||||
$storeSentEmailData &&
|
||||
$emailId &&
|
||||
isset($actionData->elementId)
|
||||
) {
|
||||
$alias = $actionData->elementId;
|
||||
|
||||
$this->createdEntitiesData->$alias = (object) [
|
||||
'entityType' => Email::ENTITY_TYPE,
|
||||
'entityId' => $emailId,
|
||||
];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$job = $this->entityManager->getNewEntity(Job::ENTITY_TYPE);
|
||||
|
||||
$job->set([
|
||||
'name' => SendEmailJob::class,
|
||||
'className' => SendEmailJob::class,
|
||||
'data' => $jobData,
|
||||
'executeTime' => $this->getExecuteTime($actionData),
|
||||
'queue' => QueueName::E0,
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($job);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return ?array{
|
||||
* email?: string,
|
||||
* type: string,
|
||||
* entityType?: string,
|
||||
* entityId?: string,
|
||||
* }
|
||||
* @throws Error
|
||||
*/
|
||||
private function getEmailAddressData(string $type): ?array
|
||||
{
|
||||
$data = $this->getActionData();
|
||||
|
||||
$fieldValue = $data->$type ?? null;
|
||||
|
||||
switch ($fieldValue) {
|
||||
case 'specifiedEmailAddress':
|
||||
$address = $data->{$type . 'Email'};
|
||||
|
||||
if ($address && str_contains($address, '{$$') && $this->hasVariables()) {
|
||||
$variables = $this->getVariables();
|
||||
|
||||
foreach (get_object_vars($variables) as $key => $v) {
|
||||
if ($v && is_string($v)) {
|
||||
$address = str_replace('{$$'.$key.'}', $v, $address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'email' => $address,
|
||||
'type' => $fieldValue,
|
||||
];
|
||||
|
||||
case 'processAssignedUser':
|
||||
if (!$this->bpmnProcess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->bpmnProcess->get('assignedUserId')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'entityType' => User::ENTITY_TYPE,
|
||||
'entityId' => $this->bpmnProcess->get('assignedUserId'),
|
||||
'type' => $fieldValue,
|
||||
];
|
||||
|
||||
case 'targetEntity':
|
||||
case 'teamUsers':
|
||||
case 'followers':
|
||||
case 'followersExcludingAssignedUser':
|
||||
$entity = $this->getEntity();
|
||||
|
||||
return [
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'entityId' => $entity->getId(),
|
||||
'type' => $fieldValue,
|
||||
];
|
||||
|
||||
case 'specifiedTeams':
|
||||
case 'specifiedUsers':
|
||||
case 'specifiedContacts':
|
||||
$specifiedEntityType = null;
|
||||
|
||||
if ($fieldValue === 'specifiedTeams') {
|
||||
$specifiedEntityType = Team::ENTITY_TYPE;
|
||||
}
|
||||
|
||||
if ($fieldValue === 'specifiedUsers') {
|
||||
$specifiedEntityType = User::ENTITY_TYPE;
|
||||
}
|
||||
|
||||
if ($fieldValue === 'specifiedContacts') {
|
||||
$specifiedEntityType = Contact::ENTITY_TYPE;
|
||||
}
|
||||
|
||||
/** @var string $specifiedEntityType */
|
||||
|
||||
return [
|
||||
'type' => $fieldValue,
|
||||
'entityIds' => $data->{$type . 'SpecifiedEntityIds'},
|
||||
'entityType' => $specifiedEntityType,
|
||||
];
|
||||
|
||||
case 'currentUser':
|
||||
return [
|
||||
'entityType' => User::ENTITY_TYPE,
|
||||
'entityId' => $this->user->getId(),
|
||||
'type' => $fieldValue,
|
||||
];
|
||||
|
||||
case 'system':
|
||||
return [
|
||||
'type' => $fieldValue,
|
||||
];
|
||||
|
||||
case 'fromOrReplyTo':
|
||||
$entity = $this->getEntity();
|
||||
$emailAddress = null;
|
||||
|
||||
/** @var EmailRepository $repo */
|
||||
$repo = $this->entityManager->getRepository(Email::ENTITY_TYPE);
|
||||
|
||||
if (!$entity instanceof Email) {
|
||||
throw new RuntimeException("Workflow send-email fromOrReplyTo did not receive email.");
|
||||
}
|
||||
|
||||
$repo->loadFromField($entity);
|
||||
|
||||
if ($entity->has('replyToString') && $entity->get('replyToString')) {
|
||||
$replyTo = $entity->get('replyToString');
|
||||
|
||||
$arr = explode(';', $replyTo);
|
||||
$emailAddress = $arr[0];
|
||||
|
||||
/** @noinspection RegExpRedundantEscape */
|
||||
preg_match_all("/[\._a-zA-Z0-9-]+@[\._a-zA-Z0-9-]+/i", $emailAddress, $matches);
|
||||
|
||||
if (empty($matches[0])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$emailAddress = $matches[0][0];
|
||||
}
|
||||
else if ($entity->has('from') && $entity->get('from')) {
|
||||
$emailAddress = $entity->get('from');
|
||||
}
|
||||
|
||||
if (!$emailAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $fieldValue,
|
||||
'email' => $emailAddress,
|
||||
];
|
||||
|
||||
default:
|
||||
if (!$fieldValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$recipients = $this->getRecipients($this->getEntity(), $fieldValue);
|
||||
|
||||
if ($recipients->getIds() === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$recipients->getEntityType()) {
|
||||
throw new Error("No Send Email action recipients entity type.");
|
||||
}
|
||||
|
||||
if ($recipients->isOne()) {
|
||||
return [
|
||||
'entityType' => $recipients->getEntityType(),
|
||||
'entityId' => $recipients->getIds()[0],
|
||||
'type' => $fieldValue,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'entityType' => $recipients->getEntityType(),
|
||||
'entityIds' => $recipients->getIds(),
|
||||
'type' => $fieldValue,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getAttachmentIds(mixed $attachmentsVariable): array
|
||||
{
|
||||
$attachmentIds = [];
|
||||
|
||||
if (is_string($attachmentsVariable) && $attachmentsVariable[0] === '$') {
|
||||
$attachmentsVariable = substr($attachmentsVariable, 1);
|
||||
}
|
||||
|
||||
if ($this->hasVariables() && is_string($attachmentsVariable) && $attachmentsVariable) {
|
||||
$attachmentIds = $this->getVariables()->$attachmentsVariable ?? null;
|
||||
|
||||
if (is_string($attachmentIds)) {
|
||||
$attachmentIds = [$attachmentIds];
|
||||
}
|
||||
|
||||
if (!is_array($attachmentIds)) {
|
||||
$attachmentIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($attachmentIds as $id) {
|
||||
if (!is_string($id)) {
|
||||
throw new RuntimeException("Not a string value in attachments variable.");
|
||||
}
|
||||
}
|
||||
|
||||
return $attachmentIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\PlaceholderHelper;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Exceptions\SendRequestError;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
use const CURLE_OPERATION_TIMEDOUT;
|
||||
use const CURLE_OPERATION_TIMEOUTED;
|
||||
use const CURLINFO_HEADER_SIZE;
|
||||
use const CURLINFO_HTTP_CODE;
|
||||
use const CURLOPT_CONNECTTIMEOUT;
|
||||
use const CURLOPT_CUSTOMREQUEST;
|
||||
use const CURLOPT_HEADER;
|
||||
use const CURLOPT_HTTPHEADER;
|
||||
use const CURLOPT_POSTFIELDS;
|
||||
use const CURLOPT_RETURNTRANSFER;
|
||||
use const CURLOPT_TIMEOUT;
|
||||
use const CURLOPT_URL;
|
||||
use const JSON_UNESCAPED_UNICODE;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class SendRequest extends Base
|
||||
{
|
||||
private ?PlaceholderHelper $placeholderHelper = null;
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws SendRequestError
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$requestType = $actionData->requestType ?? null;
|
||||
$contentType = $actionData->contentType ?? null;
|
||||
$requestUrl = $actionData->requestUrl ?? null;
|
||||
$content = $actionData->content ?? null;
|
||||
$contentVariable = $actionData->contentVariable ?? null;
|
||||
$additionalHeaders = $actionData->headers ?? [];
|
||||
|
||||
if (!$requestUrl) {
|
||||
throw new Error("Empty request URL.");
|
||||
}
|
||||
|
||||
if (!$requestType) {
|
||||
throw new Error("Empty request type.");
|
||||
}
|
||||
|
||||
if (!in_array($requestType, ['POST', 'PUT', 'PATCH', 'DELETE', 'GET'])) {
|
||||
throw new Error("Not supported request type.");
|
||||
}
|
||||
|
||||
/** @var non-empty-string $requestUrl */
|
||||
$requestUrl = $this->applyVariables($requestUrl);
|
||||
|
||||
$contentTypeList = [
|
||||
null,
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
];
|
||||
|
||||
if (!in_array($contentType, $contentTypeList)) {
|
||||
throw new Error("Unsupported content-type.");
|
||||
}
|
||||
|
||||
$isGet = $requestType === 'GET';
|
||||
|
||||
$payload = $this->getPayload(
|
||||
isJson: $contentType === 'application/json' && !$isGet,
|
||||
content: $content,
|
||||
contentVariable: $contentVariable,
|
||||
headers: $additionalHeaders,
|
||||
);
|
||||
|
||||
$timeout = $this->config->get('workflowSendRequestTimeout', 7);
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
if ($ch === false) {
|
||||
throw new RuntimeException("CURL init error.");
|
||||
}
|
||||
|
||||
if ($isGet && $payload) {
|
||||
$separator = (parse_url($requestUrl, PHP_URL_QUERY) === null) ? '?' : '&';
|
||||
|
||||
$requestUrl .= $separator . $payload;
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $requestUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $requestType);
|
||||
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS | CURLPROTO_HTTP);
|
||||
|
||||
if (!$isGet) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
|
||||
if ($contentType) {
|
||||
$headers[] = 'Content-Type: ' . $contentType;
|
||||
}
|
||||
|
||||
foreach ($additionalHeaders as $header) {
|
||||
$header = $this->applyVariables($header);
|
||||
$header = $this->getPlaceholderHelper()->applySecrets($header);
|
||||
|
||||
$headers[] = $header;
|
||||
}
|
||||
|
||||
if (!empty($headers)) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
}
|
||||
|
||||
$this->logSendRequest($isGet, $payload);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false || $response === true) {
|
||||
$response = '';
|
||||
}
|
||||
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_errno($ch);
|
||||
|
||||
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
|
||||
//$header = mb_substr($response, 0, $headerSize);
|
||||
$body = mb_substr($response, $headerSize);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if (!is_int($code)) {
|
||||
$code = 0;
|
||||
}
|
||||
|
||||
if ($code && $code >= 400 && $code <= 500) {
|
||||
$message = "Workflow: Send Request action: $requestType $requestUrl; Error $code response.";
|
||||
|
||||
throw new SendRequestError($message, $code);
|
||||
}
|
||||
|
||||
if ($error && in_array($error, [CURLE_OPERATION_TIMEDOUT, CURLE_OPERATION_TIMEOUTED])) {
|
||||
throw new Error("Workflow: Send Request action: $requestUrl; Timeout.");
|
||||
}
|
||||
|
||||
if ($code < 200 || $code >= 300) {
|
||||
$message = "Workflow: Send Request action: $code response.";
|
||||
|
||||
throw new SendRequestError($message, $code);
|
||||
}
|
||||
|
||||
$this->setResponseVariables($body, $code);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $headers
|
||||
* @throws Error
|
||||
*/
|
||||
private function getPayload(
|
||||
bool $isJson,
|
||||
?string $content,
|
||||
?string $contentVariable,
|
||||
array $headers,
|
||||
): ?string {
|
||||
|
||||
if (!$isJson) {
|
||||
foreach ($headers as $header) {
|
||||
if (str_starts_with(strtolower($header), 'content-type: application/json')) {
|
||||
$isJson = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$contentVariable) {
|
||||
if (!$content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = $this->applyVariables($content, true);
|
||||
|
||||
if ($isJson) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$post = json_decode($content, true);
|
||||
|
||||
foreach ($post as $k => $v) {
|
||||
if (is_array($v)) {
|
||||
$post[$k] = '"' . implode(', ', $v) . '"';
|
||||
}
|
||||
}
|
||||
|
||||
return http_build_query($post);
|
||||
}
|
||||
|
||||
if ($contentVariable[0] === '$') {
|
||||
$contentVariable = substr($contentVariable, 1);
|
||||
|
||||
if (!$contentVariable) {
|
||||
throw new Error("Empty variable.");
|
||||
}
|
||||
}
|
||||
|
||||
$content = $this->getVariables()->$contentVariable ?? null;
|
||||
|
||||
if (is_string($content)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if (!$content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$isJson) {
|
||||
if ($content instanceof stdClass) {
|
||||
return http_build_query($content);
|
||||
}
|
||||
|
||||
throw new Error("Workflow: Send Request: Bad value in payload variable. Should be string or object.");
|
||||
}
|
||||
|
||||
if (
|
||||
is_array($content) ||
|
||||
$content instanceof stdClass ||
|
||||
is_scalar($content)
|
||||
) {
|
||||
$result = json_encode($content, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
if ($result === false) {
|
||||
throw new Error("Workflow: Send Request: Could not JSON encode payload.");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
throw new Error("Workflow: Send Request action: Bad value in payload variable.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $body
|
||||
* @param int $code
|
||||
*/
|
||||
private function setResponseVariables($body, $code): void
|
||||
{
|
||||
if (!$this->hasVariables()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->updateVariables(
|
||||
(object) [
|
||||
'_lastHttpResponseBody' => $body,
|
||||
'_lastHttpResponseCode' => $code,
|
||||
]
|
||||
);
|
||||
|
||||
//$this->variables->_lastHttpResponseBody = $body;
|
||||
}
|
||||
|
||||
private function applyVariables(string $content, bool $isJson = false): string
|
||||
{
|
||||
$target = $this->getEntity();
|
||||
|
||||
foreach ($target->getAttributeList() as $a) {
|
||||
$value = $target->get($a) ?? '';
|
||||
|
||||
if (
|
||||
$isJson &&
|
||||
$target->getAttributeParam($a, 'isLinkMultipleIdList') &&
|
||||
$target->get($a) === null
|
||||
) {
|
||||
$relation = $target->getAttributeParam($a, 'relation');
|
||||
|
||||
if ($relation && $target->hasLinkMultipleField($relation)) {
|
||||
$value = $target->getLinkMultipleIdList($relation);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isJson && is_array($value)) {
|
||||
$arr = [];
|
||||
|
||||
foreach ($value as $item) {
|
||||
if (is_string($item)) {
|
||||
$arr[] = str_replace(',', '_COMMA_', $item);
|
||||
}
|
||||
}
|
||||
|
||||
$value = implode(',', $arr);
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = $isJson ?
|
||||
$this->escapeStringForJson($value) :
|
||||
str_replace(["\r\n", "\r", "\n"], "\\n", $value);
|
||||
} else if (is_numeric($value)) {
|
||||
$value = strval($value);
|
||||
} else if (is_array($value)) {
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
} else if (is_bool($value)) {
|
||||
$value = $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$content = str_replace('{$' . $a . '}', $value, $content);
|
||||
}
|
||||
}
|
||||
|
||||
$variables = $this->getVariables();
|
||||
|
||||
foreach (get_object_vars($variables) as $key => $value) {
|
||||
if (
|
||||
!is_string($value) &&
|
||||
!is_int($value) &&
|
||||
!is_float($value) &&
|
||||
!is_array($value) &&
|
||||
!is_bool($value)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_int($value) || is_float($value)) {
|
||||
$value = strval($value);
|
||||
} else if (is_array($value)) {
|
||||
if (!$isJson) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
} else if (is_string($value)) {
|
||||
$value = $isJson ?
|
||||
$this->escapeStringForJson($value) :
|
||||
str_replace(["\r\n", "\r", "\n"], "\\n", $value);
|
||||
} else if (is_bool($value)) { /** @phpstan-ignore-line function.alreadyNarrowedType */
|
||||
$value = $value ? 'true' : 'false';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var string $value */
|
||||
|
||||
$content = str_replace("{\$\$$key}", $value, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function escapeStringForJson(string $string): string
|
||||
{
|
||||
$encoded = json_encode($string, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
if ($encoded === false) {
|
||||
$encoded = '';
|
||||
}
|
||||
|
||||
return substr($encoded, 1, -1);
|
||||
}
|
||||
|
||||
private function getPlaceholderHelper(): PlaceholderHelper
|
||||
{
|
||||
$this->placeholderHelper ??= $this->injectableFactory->create(PlaceholderHelper::class);
|
||||
|
||||
return $this->placeholderHelper;
|
||||
}
|
||||
|
||||
private function logSendRequest(bool $isGet, ?string $post): void
|
||||
{
|
||||
$logMessage = "Workflow: Send request.";
|
||||
|
||||
if (!$isGet) {
|
||||
$logMessage .= " Payload:" . $post;
|
||||
}
|
||||
|
||||
$GLOBALS['log']->debug($logMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Modules\Advanced\Entities\BpmnFlowchart;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Modules\Advanced\Core\Bpmn\BpmnManager;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class StartBpmnProcess extends Base
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$target = $actionData->target ?? null;
|
||||
$flowchartId = $actionData->flowchartId ?? null;
|
||||
$elementId = $actionData->elementId;
|
||||
|
||||
if (!$flowchartId || !$elementId) {
|
||||
throw new Error('Workflow StartBpmnProcess: Not flowchart data.');
|
||||
}
|
||||
|
||||
$targetEntity = $this->getFirstTargetFromTargetItem($entity, $target);
|
||||
|
||||
if (!$targetEntity) {
|
||||
$GLOBALS['log']->notice('Workflow StartBpmnProcess: No target.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
$targetEntity->getEntityType() === $entity->getEntityType() &&
|
||||
$targetEntity->getId() === $entity->getId()
|
||||
) {
|
||||
$targetEntity = $entity;
|
||||
}
|
||||
|
||||
$flowchart = $this->getFlowchart($flowchartId, $targetEntity);
|
||||
|
||||
$bpmnManager = $this->injectableFactory->create(BpmnManager::class);
|
||||
|
||||
$bpmnManager->startProcess(
|
||||
$targetEntity,
|
||||
$flowchart,
|
||||
$elementId,
|
||||
null,
|
||||
$this->getWorkflowId(),
|
||||
$this->getSignalParams()
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
private function getFlowchart(string $flowchartId, Entity $targetEntity): BpmnFlowchart
|
||||
{
|
||||
/** @var ?BpmnFlowchart $flowchart */
|
||||
$flowchart = $this->entityManager->getEntityById(BpmnFlowchart::ENTITY_TYPE, $flowchartId);
|
||||
|
||||
if (!$flowchart) {
|
||||
throw new Error("StartBpmnProcess: Could not find flowchart $flowchartId.");
|
||||
}
|
||||
|
||||
if ($flowchart->getTargetType() !== $targetEntity->getEntityType()) {
|
||||
throw new Error("Workflow StartBpmnProcess: Target entity type doesn't match flowchart target type.");
|
||||
}
|
||||
|
||||
return $flowchart;
|
||||
}
|
||||
|
||||
private function getSignalParams(): ?stdClass
|
||||
{
|
||||
$signalParams = $this->getVariables()->__signalParams ?? null;
|
||||
|
||||
if (!$signalParams instanceof stdClass) {
|
||||
$signalParams = null;
|
||||
}
|
||||
|
||||
if ($signalParams) {
|
||||
$signalParams = clone $signalParams;
|
||||
}
|
||||
|
||||
return $signalParams;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Job\JobSchedulerFactory;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Modules\Advanced\Entities\Workflow;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Jobs\TriggerWorkflow as TriggerWorkflowJob;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Jobs\TriggerWorkflowMany;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Service;
|
||||
use Espo\ORM\Entity;
|
||||
use DateTime;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class TriggerWorkflow extends Base
|
||||
{
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$workflowId = $actionData->workflowId ?? null;
|
||||
$target = $actionData->target ?? null;
|
||||
|
||||
if (!$workflowId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$targetEntity = null;
|
||||
$hasMany = false;
|
||||
|
||||
foreach ($this->getTargetsFromTargetItem($entity, $target) as $i => $it) {
|
||||
if ($i > 0) {
|
||||
$hasMany = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$targetEntity = $it;
|
||||
}
|
||||
|
||||
if (!$targetEntity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($hasMany) {
|
||||
$this->scheduleAnotherWorkflowMany($entity, $actionData, $targetEntity, $target, $workflowId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->triggerAnotherWorkflow($targetEntity, $actionData, $entity);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function scheduleAnotherWorkflowMany(
|
||||
Entity $entity,
|
||||
stdClass $actionData,
|
||||
Entity $firstEntity,
|
||||
?string $target,
|
||||
string $workflowId
|
||||
): void {
|
||||
|
||||
$this->checkNextWorkflow($workflowId, $firstEntity);
|
||||
|
||||
$schedulerFactory = $this->injectableFactory->create(JobSchedulerFactory::class);
|
||||
|
||||
$schedulerFactory
|
||||
->create()
|
||||
->setClassName(TriggerWorkflowMany::class)
|
||||
->setData([
|
||||
'workflowId' => $this->getWorkflowId(),
|
||||
'entityId' => $entity->getId(),
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'nextWorkflowId' => $workflowId,
|
||||
'target' => $target,
|
||||
])
|
||||
->setTime(new DateTime($this->getExecuteTime($actionData)))
|
||||
->schedule();
|
||||
}
|
||||
|
||||
private function triggerAnotherWorkflow(Entity $entity, stdClass $actionData, Entity $originalEntity): void
|
||||
{
|
||||
$workflowId = $actionData->workflowId;
|
||||
$this->checkNextWorkflow($workflowId, $entity);
|
||||
|
||||
$now = true;
|
||||
|
||||
if (
|
||||
property_exists($actionData, 'execution') &&
|
||||
property_exists($actionData->execution, 'type')
|
||||
) {
|
||||
$executeType = $actionData->execution->type;
|
||||
$now = !$executeType || $executeType === 'immediately';
|
||||
}
|
||||
|
||||
if ($now) {
|
||||
$service = $this->injectableFactory->create(Service::class);
|
||||
|
||||
if (
|
||||
$originalEntity->getEntityType() && $entity->getEntityType() &&
|
||||
$originalEntity->getId() === $entity->getId()
|
||||
) {
|
||||
// To preserve 'changed' condition.
|
||||
$entity = $originalEntity;
|
||||
}
|
||||
|
||||
$service->triggerWorkflow($entity, $actionData->workflowId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$schedulerFactory = $this->injectableFactory->create(JobSchedulerFactory::class);
|
||||
|
||||
$schedulerFactory
|
||||
->create()
|
||||
->setClassName(TriggerWorkflowJob::class)
|
||||
->setData([
|
||||
'workflowId' => $this->getWorkflowId(),
|
||||
'entityId' => $entity->getId(),
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'nextWorkflowId' => $workflowId,
|
||||
'values' => $entity->getValueMap(),
|
||||
])
|
||||
->setTime(new DateTime($this->getExecuteTime($actionData)))
|
||||
->schedule();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
private function checkNextWorkflow(string $workflowId, Entity $entity): void
|
||||
{
|
||||
/** @var ?Workflow $workflow */
|
||||
$workflow = $this->entityManager->getEntityById(Workflow::ENTITY_TYPE, $workflowId);
|
||||
|
||||
if (!$workflow) {
|
||||
throw new Error("Workflow: Trigger another workflow: No workflow $workflowId.");
|
||||
}
|
||||
|
||||
if ($entity->getEntityType() !== $workflow->getTargetEntityType()) {
|
||||
throw new Error(
|
||||
"Workflow: Trigger another workflow: Not matching target entity type in workflow $workflowId.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\SaveContextHelper;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class UnrelateFromEntity extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
if (empty($actionData->entityId) || empty($actionData->link)) {
|
||||
throw new Error('Workflow['.$this->getWorkflowId().']: Bad params defined for UnrelateFromEntity');
|
||||
}
|
||||
|
||||
$foreignEntityType = $entity->getRelationParam($actionData->link, 'entity');
|
||||
|
||||
if (!$foreignEntityType) {
|
||||
throw new Error('Workflow['.$this->getWorkflowId().
|
||||
']: Could not find foreign entity type for UnrelateFromEntity');
|
||||
}
|
||||
|
||||
$foreignEntity = $this->entityManager->getEntityById($foreignEntityType, $actionData->entityId);
|
||||
|
||||
if (!$foreignEntity) {
|
||||
throw new Error('Workflow['.$this->getWorkflowId().
|
||||
']: Could not find foreign entity for UnrelateFromEntity');
|
||||
}
|
||||
|
||||
$relateOptions = [
|
||||
'context' => SaveContextHelper::obtainFromRawOptions($options),
|
||||
];
|
||||
|
||||
$this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $actionData->link)
|
||||
->unrelate($foreignEntity, $relateOptions);
|
||||
|
||||
if ($entity->hasLinkMultipleField($actionData->link)) {
|
||||
$entity->loadLinkMultipleField($actionData->link);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use stdClass;
|
||||
|
||||
class UpdateCreatedEntity extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
if (empty($actionData->target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$target = $actionData->target;
|
||||
|
||||
$targetEntity = $this->getCreatedEntity($target);
|
||||
|
||||
if (!$targetEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (property_exists($actionData, 'fields')) {
|
||||
$data = $this->getDataToFill($targetEntity, $actionData->fields);
|
||||
$targetEntity->set($data);
|
||||
}
|
||||
|
||||
if (!empty($actionData->formula)) {
|
||||
$this->formulaManager->run(
|
||||
$actionData->formula,
|
||||
$targetEntity,
|
||||
$this->getFormulaVariables()
|
||||
);
|
||||
}
|
||||
|
||||
if (!$targetEntity->has('modifiedById')) {
|
||||
$targetEntity->set('modifiedByName', 'System');
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($targetEntity, ['modifiedById' => 'system']);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\SaveContextHelper;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class UpdateEntity extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$reloadedEntity = $this->entityManager->getEntityById($entity->getEntityType(), $entity->getId());
|
||||
|
||||
$data = $this->getDataToFill($reloadedEntity, $actionData->fields);
|
||||
|
||||
$reloadedEntity->set($data);
|
||||
$entity->set($data);
|
||||
|
||||
$formula = $actionData->formula ?? null;
|
||||
|
||||
if ($formula) {
|
||||
$this->formulaManager->run($formula, $reloadedEntity, $this->getFormulaVariables());
|
||||
}
|
||||
|
||||
foreach ($reloadedEntity->getAttributeList() as $attribute) {
|
||||
if ($reloadedEntity->isAttributeChanged($attribute)) {
|
||||
$entity->set($attribute, $reloadedEntity->get($attribute));
|
||||
}
|
||||
}
|
||||
|
||||
$saveOptions = [
|
||||
'modifiedById' => 'system',
|
||||
'skipWorkflow' => !$this->bpmnProcess,
|
||||
'workflowId' => $this->getWorkflowId(),
|
||||
'skipAudited' => $entity->isNew(),
|
||||
'context' => SaveContextHelper::createDerived($options),
|
||||
];
|
||||
|
||||
$this->entityManager->saveEntity($reloadedEntity, $saveOptions);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use stdClass;
|
||||
|
||||
class UpdateProcessEntity extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$targetEntity = $this->bpmnProcess;
|
||||
|
||||
$data = [];
|
||||
|
||||
if (property_exists($actionData, 'fields')) {
|
||||
$data = $this->getDataToFill($targetEntity, $actionData->fields);
|
||||
}
|
||||
|
||||
$targetEntity->set($data);
|
||||
|
||||
if (!empty($actionData->formula)) {
|
||||
$this->formulaManager->run($actionData->formula, $targetEntity, $this->getFormulaVariables());
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($targetEntity, [
|
||||
'skipWorkflow' => true,
|
||||
'modifiedById' => 'system',
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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\Core\Workflow\Actions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityCollection;
|
||||
use Exception;
|
||||
use stdClass;
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
class UpdateRelatedEntity extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @throws FormulaError
|
||||
* @throws Error
|
||||
*/
|
||||
protected function run(CoreEntity $entity, stdClass $actionData, array $options): bool
|
||||
{
|
||||
$link = $actionData->link;
|
||||
|
||||
$relatedEntities = $this->getRelatedEntities($entity, $link);
|
||||
|
||||
$saveOptions = [
|
||||
'modifiedById' => 'system',
|
||||
'workflowId' => $this->getWorkflowId(),
|
||||
];
|
||||
|
||||
foreach ($relatedEntities as $relatedEntity) {
|
||||
if (!($relatedEntity instanceof Entity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = true;
|
||||
|
||||
if (
|
||||
$entity->hasRelation($link) &&
|
||||
$entity->getRelationType($link) === 'belongsToParent' &&
|
||||
!empty($actionData->parentEntityType)
|
||||
) {
|
||||
if ($actionData->parentEntityType !== $relatedEntity->getEntityType()) {
|
||||
$update = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$update) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $this->getDataToFill($relatedEntity, $actionData->fields);
|
||||
|
||||
$relatedEntity->set($data);
|
||||
|
||||
if (!empty($actionData->formula)) {
|
||||
$clonedVariables = clone $this->getFormulaVariables();
|
||||
|
||||
$this->formulaManager->run($actionData->formula, $relatedEntity, $clonedVariables);
|
||||
}
|
||||
|
||||
if (!$relatedEntity->has('modifiedById')) {
|
||||
$relatedEntity->set('modifiedByName', 'System');
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($relatedEntity, $saveOptions);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Related Entity,
|
||||
*
|
||||
* @return EntityCollection|iterable<Entity>
|
||||
*/
|
||||
protected function getRelatedEntities(Entity $entity, string $link)
|
||||
{
|
||||
if (empty($link) || !$entity->hasRelation($link)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch ($entity->getRelationType($link)) {
|
||||
case Entity::BELONGS_TO_PARENT:
|
||||
$parentType = $entity->get($link . 'Type');
|
||||
$parentId = $entity->get($link . 'Id');
|
||||
|
||||
if (!$parentType || !$parentId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$relatedEntity = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$GLOBALS['log']->info(
|
||||
'Workflow[UpdateRelatedEntity]: Cannot getRelatedEntities(), error: '. $e->getMessage());
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$fetched = $this->entityManager
|
||||
->getCollectionFactory()
|
||||
->create($entity->getEntityType(), [$relatedEntity]);
|
||||
|
||||
break;
|
||||
|
||||
case Entity::HAS_MANY:
|
||||
case Entity::HAS_CHILDREN:
|
||||
case Entity::MANY_MANY:
|
||||
$fetched = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $link)
|
||||
->sth()
|
||||
->find();
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
try {
|
||||
$fetched = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, $link)
|
||||
->findOne();
|
||||
} catch (Exception $e) {
|
||||
$GLOBALS['log']->info(
|
||||
'Workflow[UpdateRelatedEntity]: Cannot getRelatedEntities(), error: '. $e->getMessage());
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($fetched instanceof Entity) {
|
||||
return $this->entityManager
|
||||
->getCollectionFactory()
|
||||
->create($entity->getEntityType(), [$fetched]);
|
||||
}
|
||||
|
||||
return $fetched ?? [];
|
||||
}
|
||||
}
|
||||
170
custom/Espo/Modules/Advanced/Core/Workflow/BaseManager.php
Normal file
170
custom/Espo/Modules/Advanced/Core/Workflow/BaseManager.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?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\Core\Workflow;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\Formula\Manager as FormulaManager;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Modules\Advanced\Core\Bpmn\Utils\ConditionManager as BpmnConditionManager;
|
||||
|
||||
abstract class BaseManager
|
||||
{
|
||||
protected string $dirName = 'Dummy';
|
||||
private ?string $processId = null;
|
||||
/** @var ?array<string, CoreEntity> */
|
||||
private ?array $entityMap = null;
|
||||
/** @var ?array<string, string> */
|
||||
private ?array $workflowIdList = null;
|
||||
/** @var array<string, class-string<Actions\Base|Conditions\Base>> */
|
||||
private array $actionClassNameMap = [];
|
||||
/** @var string[] */
|
||||
protected array $requiredOptions = [];
|
||||
|
||||
public function __construct(
|
||||
protected Log $log,
|
||||
private InjectableFactory $injectableFactory,
|
||||
protected FormulaManager $formulaManager,
|
||||
protected BpmnConditionManager $conditionManager,
|
||||
) {}
|
||||
|
||||
public function setInitData(string $workflowId, CoreEntity $entity): void
|
||||
{
|
||||
$this->processId = $workflowId . '-'. $entity->getId();
|
||||
|
||||
$this->workflowIdList[$this->processId] = $workflowId;
|
||||
$this->entityMap[$this->processId] = $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getProcessId(): ?string
|
||||
{
|
||||
if (empty($this->processId)) {
|
||||
throw new Error('Workflow['.__CLASS__.'], getProcessId(): Empty processId.');
|
||||
}
|
||||
|
||||
return $this->processId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getWorkflowId(?string $processId = null): string
|
||||
{
|
||||
if (!isset($processId)) {
|
||||
$processId = $this->getProcessId();
|
||||
}
|
||||
|
||||
if (empty($this->workflowIdList[$processId])) {
|
||||
throw new Error('Workflow['.__CLASS__.'], getWorkflowId(): Empty workflowId.');
|
||||
}
|
||||
|
||||
return $this->workflowIdList[$processId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getEntity(?string $processId = null): CoreEntity
|
||||
{
|
||||
if (!isset($processId)) {
|
||||
$processId = $this->getProcessId();
|
||||
}
|
||||
|
||||
if (empty($this->entityMap[$processId])) {
|
||||
throw new Error('Workflow[' . __CLASS__ . '], getEntity(): Empty Entity object.');
|
||||
}
|
||||
|
||||
return $this->entityMap[$processId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Actions\Base|Conditions\Base>
|
||||
* @throws Error
|
||||
*/
|
||||
private function getClassName(string $name): string
|
||||
{
|
||||
if (!isset($this->actionClassNameMap[$name])) {
|
||||
$className = 'Espo\Custom\Modules\Advanced\Core\Workflow\\' . ucfirst($this->dirName) . '\\' . $name;
|
||||
|
||||
if (!class_exists($className)) {
|
||||
$className .= 'Type';
|
||||
|
||||
if (!class_exists($className)) {
|
||||
$className = 'Espo\Modules\Advanced\Core\Workflow\\' . ucfirst($this->dirName) . '\\' . $name;
|
||||
|
||||
if (!class_exists($className)) {
|
||||
$className .= 'Type';
|
||||
|
||||
if (!class_exists($className)) {
|
||||
throw new Error('Class ['.$className.'] does not exist.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @var class-string<Actions\Base|Conditions\Base> $className */
|
||||
|
||||
$this->actionClassNameMap[$name] = $className;
|
||||
}
|
||||
|
||||
return $this->actionClassNameMap[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Actions\Base|Conditions\Base
|
||||
* @throws Error
|
||||
*/
|
||||
protected function createConditionOrAction(string $name, ?string $processId = null): object
|
||||
{
|
||||
$name = ucfirst($name);
|
||||
|
||||
$name = str_replace("\\", "", $name);
|
||||
|
||||
if (!isset($processId)) {
|
||||
$processId = $this->getProcessId();
|
||||
}
|
||||
|
||||
$workflowId = $this->getWorkflowId($processId);
|
||||
|
||||
$className = $this->getClassName($name);
|
||||
|
||||
/** @var Actions\Base|Conditions\Base $obj */
|
||||
$obj = $this->injectableFactory->create($className);
|
||||
|
||||
$obj->setWorkflowId($workflowId);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
protected function validate(object $options): bool
|
||||
{
|
||||
foreach ($this->requiredOptions as $optionName) {
|
||||
if (!property_exists($options, $optionName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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\Core\Workflow;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaException;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class ConditionManager extends BaseManager
|
||||
{
|
||||
protected string $dirName = 'Conditions';
|
||||
|
||||
/** @var string[] */
|
||||
protected array $requiredOptions = [
|
||||
'comparison',
|
||||
'fieldToCompare',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param ?stdClass[] $all
|
||||
* @param ?stdClass[] $any
|
||||
* @throws Error
|
||||
* @throws FormulaException
|
||||
*/
|
||||
public function check(
|
||||
?array $all = null,
|
||||
?array $any = null,
|
||||
?string $formula = null
|
||||
): bool {
|
||||
|
||||
return $this->conditionManager->check($this->getEntity(), $all, $any, $formula);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $conditions
|
||||
* @throws Error
|
||||
*/
|
||||
public function checkConditionsAny(array $conditions): bool
|
||||
{
|
||||
return $this->conditionManager->checkConditionsAny($this->getEntity(), $conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $conditions
|
||||
* @throws Error
|
||||
*/
|
||||
public function checkConditionsAll(array $conditions): bool
|
||||
{
|
||||
return $this->conditionManager->checkConditionsAll($this->getEntity(), $conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $variables Formula variables to pass.
|
||||
* @throws Error
|
||||
* @throws FormulaException
|
||||
*/
|
||||
public function checkConditionsFormula(?string $formula, array $variables = []): bool
|
||||
{
|
||||
return $this->conditionManager->checkConditionsFormula($this->getEntity(), $formula, (object) $variables);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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\Core\Workflow\Conditions;
|
||||
|
||||
use DateTime;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
class After extends Base
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
$subjectValue = $this->getSubjectValue();
|
||||
|
||||
try {
|
||||
$fieldDate = new DateTime($fieldValue);
|
||||
$subjectDate = new DateTime($subjectValue);
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $fieldDate > $subjectDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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\Core\Workflow\Conditions;
|
||||
|
||||
class AfterToday extends After
|
||||
{}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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\Core\Workflow\Conditions;
|
||||
|
||||
class AnyOf extends Base
|
||||
{
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
$subjectValue = $this->getSubjectValue();
|
||||
|
||||
if (!is_array($subjectValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($fieldValue, $subjectValue);
|
||||
}
|
||||
}
|
||||
203
custom/Espo/Modules/Advanced/Core/Workflow/Conditions/Base.php
Normal file
203
custom/Espo/Modules/Advanced/Core/Workflow/Conditions/Base.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?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\Core\Workflow\Conditions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Modules\Advanced\Tools\Workflow\Core\FieldValueHelper;
|
||||
use Espo\Modules\Advanced\Core\Workflow\Utils;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use stdClass;
|
||||
|
||||
abstract class Base
|
||||
{
|
||||
private ?string $workflowId = null;
|
||||
protected ?CoreEntity $entity = null;
|
||||
protected ?stdClass $condition = null;
|
||||
protected ?stdClass $createdEntitiesData = null;
|
||||
|
||||
public function __construct(
|
||||
protected EntityManager $entityManager,
|
||||
protected Config $config,
|
||||
protected FieldValueHelper $fieldValueHelper,
|
||||
) {}
|
||||
|
||||
protected function compareComplex(CoreEntity $entity, stdClass $condition): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
*/
|
||||
abstract protected function compare($fieldValue): bool;
|
||||
|
||||
public function setWorkflowId(?string $workflowId): void
|
||||
{
|
||||
$this->workflowId = $workflowId;
|
||||
}
|
||||
|
||||
protected function getWorkflowId(): ?string
|
||||
{
|
||||
return $this->workflowId;
|
||||
}
|
||||
|
||||
protected function getEntity(): CoreEntity
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
protected function getCondition(): stdClass
|
||||
{
|
||||
return $this->condition;
|
||||
}
|
||||
|
||||
public function process(CoreEntity $entity, stdClass $condition, ?stdClass $createdEntitiesData = null): bool
|
||||
{
|
||||
$this->entity = $entity;
|
||||
$this->condition = $condition;
|
||||
$this->createdEntitiesData = $createdEntitiesData;
|
||||
|
||||
if (!empty($condition->fieldValueMap)) {
|
||||
return $this->compareComplex($entity, $condition);
|
||||
} else {
|
||||
$fieldName = $this->getFieldName();
|
||||
|
||||
if (isset($fieldName)) {
|
||||
return $this->compare($this->getFieldValue());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field name based on fieldToCompare value.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
protected function getFieldName()
|
||||
{
|
||||
$condition = $this->getCondition();
|
||||
|
||||
if (isset($condition->fieldToCompare)) {
|
||||
$entity = $this->getEntity();
|
||||
$field = $condition->fieldToCompare;
|
||||
|
||||
$normalizeFieldName = Utils::normalizeFieldName($entity, $field);
|
||||
|
||||
if (is_array($normalizeFieldName)) { //if field is parent
|
||||
return reset($normalizeFieldName) ?: null;
|
||||
}
|
||||
|
||||
return $normalizeFieldName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
protected function getAttributeName()
|
||||
{
|
||||
return $this->getFieldName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getAttributeValue()
|
||||
{
|
||||
return $this->getFieldValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value of fieldToCompare field.
|
||||
*
|
||||
* @todo Use loader for not set fields.
|
||||
* Only for BPM.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFieldValue()
|
||||
{
|
||||
$entity = $this->getEntity();
|
||||
$condition = $this->getCondition();
|
||||
|
||||
return $this->fieldValueHelper->getValue(
|
||||
entity: $entity,
|
||||
path: $condition->fieldToCompare,
|
||||
createdEntitiesData: $this->createdEntitiesData,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value of subject field.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
protected function getSubjectValue(): mixed
|
||||
{
|
||||
$entity = $this->getEntity();
|
||||
$condition = $this->getCondition();
|
||||
|
||||
$subjectType = $condition->subjectType ?? null;
|
||||
|
||||
if ($subjectType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($subjectType === 'value') {
|
||||
return $condition->value ?? null;
|
||||
}
|
||||
|
||||
if ($subjectType === 'field') {
|
||||
$subjectValue = $this->fieldValueHelper->getValue($entity, $condition->field);
|
||||
|
||||
if (
|
||||
isset($condition->shiftDays) ||
|
||||
isset($condition->shiftUnits)
|
||||
) {
|
||||
return Utils::shiftDays(
|
||||
shiftDays: $condition->shiftDays ?? 0,
|
||||
input: $subjectValue,
|
||||
type: 'date',
|
||||
unit: $condition->shiftUnits ?? 'days',
|
||||
timezone: $this->config->get('timeZone'),
|
||||
);
|
||||
}
|
||||
|
||||
return $subjectValue;
|
||||
}
|
||||
|
||||
if ($subjectType === 'today') {
|
||||
return Utils::shiftDays(
|
||||
shiftDays: $condition->shiftDays ?? 0,
|
||||
type: 'date',
|
||||
unit: $condition->shiftUnits ?? 'days',
|
||||
timezone: $this->config->get('timeZone'),
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error("Workflow[{$this->getWorkflowId()}]: Non supported type '$subjectType'.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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\Core\Workflow\Conditions;
|
||||
|
||||
use DateTime;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
class Before extends Base
|
||||
{
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
$subjectValue = $this->getSubjectValue();
|
||||
|
||||
try {
|
||||
$fieldDate = new DateTime($fieldValue);
|
||||
$subjectDate = new DateTime($subjectValue);
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $fieldDate < $subjectDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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\Core\Workflow\Conditions;
|
||||
|
||||
class BeforeToday extends Before
|
||||
{}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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\Core\Workflow\Conditions;
|
||||
|
||||
class Changed extends Base
|
||||
{
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
$entity = $this->getEntity();
|
||||
$attribute = $this->getAttributeName();
|
||||
|
||||
if (!isset($attribute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!$entity->isNew() &&
|
||||
!$entity->hasFetched($attribute) &&
|
||||
$entity->getAttributeParam($attribute, 'isLinkMultipleIdList')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->isNew()) {
|
||||
$value = $entity->get($attribute);
|
||||
|
||||
if (empty($value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $entity->isAttributeChanged($attribute);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Workflow\Conditions;
|
||||
|
||||
class Contains extends Base
|
||||
{
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
$subjectValue = $this->getSubjectValue();
|
||||
|
||||
return mb_strpos($fieldValue, $subjectValue) !== false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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\Core\Workflow\Conditions;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use stdClass;
|
||||
|
||||
class Equals extends Base
|
||||
{
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
* @throws Error
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
$subjectValue = $this->getSubjectValue();
|
||||
|
||||
return ($fieldValue == $subjectValue);
|
||||
}
|
||||
|
||||
protected function compareComplex(CoreEntity $entity, stdClass $condition): bool
|
||||
{
|
||||
if (empty($condition->fieldValueMap)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fieldValueMap = $condition->fieldValueMap;
|
||||
|
||||
foreach ($fieldValueMap as $field => $value) {
|
||||
$itemValue = $this->fieldValueHelper->getValue(
|
||||
entity: $entity,
|
||||
path: $field,
|
||||
createdEntitiesData: $this->createdEntitiesData,
|
||||
);
|
||||
|
||||
if ($itemValue !== $value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Workflow\Conditions;
|
||||
|
||||
class FalseType extends TrueType
|
||||
{
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
return !(parent::compare($fieldValue));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Workflow\Conditions;
|
||||
|
||||
class GreaterThan extends Base
|
||||
{
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
$subjectValue = $this->getSubjectValue();
|
||||
|
||||
return ($fieldValue > $subjectValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Workflow\Conditions;
|
||||
|
||||
class GreaterThanOrEquals extends Base
|
||||
{
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
$subjectValue = $this->getSubjectValue();
|
||||
|
||||
return ($fieldValue >= $subjectValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/***********************************************************************************
|
||||
* The contents of this file are subject to the Extension License Agreement
|
||||
* ("Agreement") which can be viewed at
|
||||
* https://www.espocrm.com/extension-license-agreement/.
|
||||
* By copying, installing downloading, or using this file, You have unconditionally
|
||||
* agreed to the terms and conditions of the Agreement, and You may not use this
|
||||
* file except in compliance with the Agreement. Under the terms of the Agreement,
|
||||
* You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
|
||||
* redistribute, market, publish, commercialize, or otherwise transfer rights or
|
||||
* usage to the software or any modified version or derivative work of the software
|
||||
* created by or for you.
|
||||
*
|
||||
* Copyright (C) 2015-2025 EspoCRM, Inc.
|
||||
*
|
||||
* License ID: 19bc86a68a7bb01f458cb391d43a9212
|
||||
************************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Advanced\Core\Workflow\Conditions;
|
||||
|
||||
class Has extends Base
|
||||
{
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
$subjectValue = $this->getSubjectValue();
|
||||
|
||||
if (is_array($subjectValue)) {
|
||||
// @todo Remove this?
|
||||
if ($subjectValue === $fieldValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_array($fieldValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($subjectValue, $fieldValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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\Core\Workflow\Conditions;
|
||||
|
||||
class IsEmpty extends Base
|
||||
{
|
||||
/**
|
||||
* @param mixed $fieldValue
|
||||
*/
|
||||
protected function compare($fieldValue): bool
|
||||
{
|
||||
if (empty($fieldValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user