Compare commits

...

176 Commits
5.2.4 ... 5.3.2

Author SHA1 Message Date
yuri
92fedd2c2e Merge branch 'hotfix/5.3.2' of ssh://172.20.0.1/var/git/espo/backend 2018-07-12 10:09:31 +03:00
Taras Machyshyn
0ec450d59d Schema bug fixes 2018-07-12 10:09:01 +03:00
yuri
f0a1634f90 version 2018-07-12 10:03:04 +03:00
Taras Machyshyn
5b5f5c8ab7 Schema bug fixes 2018-07-11 15:16:59 +03:00
yuri
19d37e8081 text filter disabling 2018-07-11 11:04:01 +03:00
yuri
d8ab10fd75 textFilterDisabled param 2018-07-11 10:37:17 +03:00
yuri
312de11a15 smtp local host name 2018-07-10 15:53:15 +03:00
yuri
47a22042b2 fix select records 2 2018-07-10 13:13:07 +03:00
yuri
31cea9e36e fix select records 2018-07-10 12:26:37 +03:00
yuri
2b983ad880 fix attachment multiple 2018-07-10 12:11:45 +03:00
yuri
21b0c2b2eb attachment multiple in list view 2018-07-10 11:58:11 +03:00
yuri
59b4aa61e4 not sortable by default 2018-07-10 11:36:20 +03:00
yuri
25d91a1e73 Merge branch 'hotfix/5.3.1' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.3.1 2018-07-09 13:36:24 +03:00
yuri
6750d32bd6 skip printing x status reason for pdo exception 2018-07-09 13:30:36 +03:00
yuri
70b8fc9ac9 foreign field: supporting number field 2018-07-09 12:52:55 +03:00
yuri
38a1be5ae3 version 2018-07-09 12:04:26 +03:00
yuri
1d9e9752c8 mass remove memory usage 2018-07-09 12:03:45 +03:00
yuri
eb37c1dc47 fix email text filter by email 2018-07-09 11:58:31 +03:00
yuri
1df6ec7f0c mass update improvement memory usage 2018-07-09 11:52:33 +03:00
yuri
1d6ee3e030 clean up 2018-07-09 11:37:39 +03:00
yuri
6eb80b747c login view name in clientDefs 2018-07-09 11:14:28 +03:00
yuri
539c0d22d7 convert lead skip disabled entity types 2018-07-09 10:54:29 +03:00
yuri
8e7d607ad4 fix dynamic logic not has 2018-07-09 10:49:44 +03:00
yuri
59e15eb71f fix select all results modal 2018-07-09 10:48:36 +03:00
Taras Machyshyn
c1ba6d5330 Metadata improvements 2018-07-06 17:02:19 +03:00
yuri
410aec734a lead select name fix 2018-07-05 17:21:58 +03:00
yuri
6078b17d38 export memory usage optimization 2018-07-05 16:47:20 +03:00
yuri
3b36d607ac code style 2018-07-05 16:20:36 +03:00
yuri
156cd85474 export memory usage improvement 2018-07-05 16:18:06 +03:00
yuri
0e29798e2b cleanup 2018-07-05 15:41:03 +03:00
yuri
19dbe81c79 hide followers in portal 2018-07-05 11:13:51 +03:00
yuri
62838961bb record max size limit improvement 2018-07-05 11:09:20 +03:00
yuri
ee84162470 createDisabled for modal select records 2018-07-03 12:22:12 +03:00
yuri
7a76dcce2c email to case copy body and attachments 2018-07-03 12:13:28 +03:00
yuri
0c21ed2e31 fix case 2018-07-03 11:50:53 +03:00
yuri
39daa763ab use numberic format param 2018-07-03 11:41:39 +03:00
yuri
f0c9690152 opp mandatory account 2018-07-03 11:32:10 +03:00
yuri
450091e71f contact mandatory account id 2018-07-03 11:31:28 +03:00
yuri
3a5c64b877 email skip replyTo when adding user 2018-07-02 15:16:33 +03:00
yuri
949d96db7a fix expanded list css 2018-07-02 14:58:16 +03:00
yuri
c974ce8864 entity manager check exists 2018-07-02 11:21:54 +03:00
yuri
caab8e9bbb custom calendar views 2018-07-02 11:05:02 +03:00
Taras Machyshyn
12469ee6f9 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-29 16:04:06 +03:00
Taras Machyshyn
dcd90b6f70 AdminNotifications improvements 2018-06-29 16:03:43 +03:00
Taras Machyshyn
f92b3c3d16 Bug fixes in Util 2018-06-29 16:02:45 +03:00
yuri
f3b41783c5 fix campaingn stats 2018-06-29 15:50:31 +03:00
yuri
3ec33c6054 fix currency converted hook 2018-06-29 15:36:34 +03:00
yuri
eb7c0da40c naming fix 2018-06-29 15:30:00 +03:00
yuri
2817c0027e fix campaign log record 2018-06-29 15:28:52 +03:00
yuri
c7457b95d1 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-29 14:29:55 +03:00
yuri
05244d598c email teams improvements 2018-06-29 13:08:49 +03:00
Taras Machyshyn
f55e0b2cb0 Minor bug fixes 2018-06-29 12:36:52 +03:00
Taras Machyshyn
ed6256da2c Possibility to check new versions of extensions 2018-06-29 12:10:25 +03:00
Taras Machyshyn
0398137ba7 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-28 16:36:03 +03:00
yuri
a6448a2769 kanban support link multiple fields 2018-06-28 13:07:31 +03:00
yuri
71cf0d01f8 fix template 2018-06-28 11:29:49 +03:00
yuri
d2a6d7ee99 fix event confirmation 2018-06-28 11:28:36 +03:00
yuri
a07bc15f00 fix typo 2018-06-28 11:26:22 +03:00
yuri
52ebd35785 supporting link multiple field on the list view 2018-06-28 11:20:13 +03:00
Taras Machyshyn
636d24a117 Check version url for extensions 2018-06-27 17:33:10 +03:00
yuri
8afbfaeb31 view jobs button 2018-06-27 11:19:49 +03:00
yuri
117084f835 fix sorting by index 2018-06-27 11:08:00 +03:00
yuri
eae92d8638 version 2018-06-27 10:52:41 +03:00
yuri
31f5df9db4 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-27 10:48:25 +03:00
Taras Machyshyn
00419a4cfc Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-26 18:02:05 +03:00
Taras Machyshyn
3f36e5b2e8 Bug fixes 2018-06-26 18:01:52 +03:00
Taras Machyshyn
cc2abc961e Fulltext index Fixes 2018-06-26 16:47:44 +03:00
yuri
bea0398776 grunt clean custom dir 2018-06-26 16:08:13 +03:00
yuri
fd4c55ba9d Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-26 15:05:35 +03:00
Taras Machyshyn
fef5edce28 Fulltext index changes 2018-06-26 15:05:21 +03:00
Taras Machyshyn
6423e4cb68 Changing text to medium types for fulltext index fields 2018-06-26 15:01:58 +03:00
yuri
2acaf1f7ff orm: 0 result if not existing attribnure is used in where 2018-06-26 12:35:24 +03:00
yuri
fe31b078f6 full-text search for email 2018-06-26 11:30:55 +03:00
yuri
e3d81f4a61 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-26 11:26:18 +03:00
yuri
fb1d1d8fc5 fix import 2018-06-26 11:15:16 +03:00
yuri
cabef5906c fix list view 2018-06-25 14:29:39 +03:00
Taras Machyshyn
2dfffe00d0 Database helper improvements 2018-06-25 13:03:07 +03:00
yuri
78390efe45 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-25 11:29:10 +03:00
yuri
8ae0d2da88 email: add teams from replied 2018-06-25 11:24:58 +03:00
Taras Machyshyn
ecb3273883 Possibility to check if table supports fulltext index 2018-06-25 11:08:51 +03:00
yuri
256b94f877 fix panels row actions links 2018-06-25 10:54:21 +03:00
yuri
422f02b5c9 text filter supporting int and autoincrement fields 2018-06-22 17:00:48 +03:00
yuri
7e4c31db1a relationship proper orm attribute type 2018-06-22 16:03:31 +03:00
yuri
c9918c07b8 htmlizer var helper 2018-06-22 15:59:51 +03:00
Taras Machyshyn
ed438b1a31 Code improvements 2018-06-22 15:46:02 +03:00
yuri
83843cfe46 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-22 14:47:30 +03:00
yuri
462de7b025 fix fulltext 2018-06-22 12:51:14 +03:00
yuri
01dbc183f8 fix email text search 2018-06-22 12:30:07 +03:00
yuri
8817c82996 fix select attributes 2018-06-22 12:02:03 +03:00
yuri
3493addec5 print pdf mass max 50 2018-06-22 11:20:42 +03:00
yuri
d41a588bb5 disable full text for email 2018-06-22 11:15:16 +03:00
Taras Machyshyn
7953705e30 MEDIUMTEXT is a default text type 2018-06-21 19:00:36 +03:00
Taras Machyshyn
763a1ad96f Possibility to ignore creating fulltext indexes 2018-06-21 18:59:34 +03:00
yuri
b58d958f51 mb string functions 2018-06-21 16:10:16 +03:00
yuri
0b8d43f734 fixes 2018-06-21 14:21:38 +03:00
yuri
87518f33a9 full-text search 3 2018-06-21 12:53:12 +03:00
yuri
d83530bbf6 fix massDelete 2018-06-20 16:53:15 +03:00
yuri
517e1bab7c serach by email address performance improvement 2018-06-20 16:01:37 +03:00
yuri
cd22552e4a entity manager full-text search parameter 2018-06-20 15:24:39 +03:00
yuri
abc394512c Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-06-20 15:07:06 +03:00
yuri
fa0cb01660 full text search 2 2018-06-20 14:51:50 +03:00
Taras Machyshyn
e6d509bd0b Added support fulltext index 2018-06-20 14:37:23 +03:00
yuri
775641aee1 fix globals search name field 2018-06-20 12:20:34 +03:00
yuri
3c51c6bc77 remove test 2018-06-19 18:46:59 +03:00
yuri
9ddd7b1d32 Merge branch 'hotfix/5.2.6' 2018-06-19 18:44:54 +03:00
yuri
6020a01a62 fix orm empty in 2018-06-19 18:44:47 +03:00
yuri
1ad9ee10f6 full text search 2018-06-19 17:29:12 +03:00
yuri
59863b8a91 test 2018-06-19 15:51:22 +03:00
yuri
1a083c247c fix css 2018-06-19 15:48:14 +03:00
yuri
cd7ca31212 orm query: supporting match 2018-06-19 15:35:30 +03:00
yuri
9be9ff3d68 mass print to pdf 2018-06-19 12:16:07 +03:00
yuri
e3b1ead830 fix acl manager empty action 2018-06-19 12:14:11 +03:00
yuri
b6041592ea fix job select attributes 2018-06-19 11:58:15 +03:00
yuri
0fa8b3da0b mail merge only with address 2018-06-18 16:13:36 +03:00
yuri
7dd0fe07ac fixes 2018-06-18 15:23:04 +03:00
yuri
2d1770f439 ignore select attributes 2018-06-18 15:11:50 +03:00
yuri
6be192514a force select all attributes 2018-06-18 15:06:46 +03:00
yuri
63fc42f8cf email mandatory select attributes 2018-06-18 14:47:32 +03:00
yuri
6f8a593f09 list select changes 2018-06-18 14:45:20 +03:00
yuri
b00e8f8900 merge 2018-06-18 14:26:00 +03:00
yuri
4e226ebcb7 select records button disable 2018-06-18 14:23:04 +03:00
yuri
30909c497b record dashlet populate assigned user 2018-06-18 14:14:48 +03:00
yuri
efd5ccfa96 list view select only attributes from layout 2018-06-18 13:23:35 +03:00
yuri
88d159d4c6 field manager load language after save 2018-06-15 15:28:45 +03:00
yuri
29788c353b link manager ui prevent existing link names 2018-06-15 15:18:43 +03:00
yuri
7897272f65 mail merge 2018-06-15 15:03:22 +03:00
yuri
5cabc76782 Merge branch 'hotfix/5.2.6' 2018-06-15 15:02:54 +03:00
yuri
b0ef416a4f target list listed status 2018-06-14 17:23:51 +03:00
yuri
0de0768bfb remove et category from layout manager 2018-06-14 17:16:08 +03:00
yuri
e4ac128a2e cleanup 2018-06-14 15:39:51 +03:00
yuri
2307f21d04 version 2018-06-14 15:15:15 +03:00
yuri
89d706c94f layout manager default panel 2018-06-14 13:00:18 +03:00
yuri
23350a0ffe field manager tpl fix 2018-06-14 12:04:02 +03:00
yuri
65d047f831 skip default opt out for users 2018-06-14 11:58:36 +03:00
yuri
dc40045de6 fix css 2018-06-14 11:57:01 +03:00
yuri
50d91ea6d8 email folder count 200 2018-06-14 11:50:33 +03:00
yuri
3a8865e382 calendar task creating fix 2018-06-14 11:47:47 +03:00
yuri
fddcef284f fix calendar task 2018-06-14 10:48:57 +03:00
yuri
c0854250e4 cleanup 2018-06-12 14:47:27 +03:00
yuri
71c9501354 cleanup 2018-06-12 14:38:25 +03:00
yuri
a85d0f91a2 layout manager panels dynamic logic 2018-06-12 14:34:57 +03:00
yuri
977514f5ef fix panels css 2018-06-12 13:22:17 +03:00
yuri
bf9ad953a1 lead converted panel use dynamic logic 2018-06-12 12:52:32 +03:00
yuri
5b1d96f649 ability to remove notifications for regular users 2018-06-12 11:24:53 +03:00
yuri
bf4ac0c9f3 opp detect closed stages by probability 2018-06-11 15:16:28 +03:00
yuri
3c8b2534eb opp last stage field 2018-06-11 15:06:19 +03:00
yuri
16c9f46583 countRelated subRelated functions 2018-06-11 12:01:52 +03:00
yuri
ef9609b710 target list listed status 2018-06-08 16:00:31 +03:00
yuri
a4c15992a9 fix calendar task 2018-06-08 14:20:35 +03:00
yuri
14d1173a0c calendar prevent drag between allday and hours 2018-06-08 14:01:06 +03:00
yuri
1e7acbdbd2 fix calendar 2018-06-08 12:47:11 +03:00
yuri
35e729b25c Merge branch 'hotfix/5.2.5' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.2.5 2018-06-08 12:12:18 +03:00
Taras Machyshyn
4ee5ea78e3 LDAP bug fixes 2018-06-08 12:11:32 +03:00
yuri
fe971f9f67 outboundEmailBccAddress on ui 2018-06-08 11:30:04 +03:00
yuri
a828523f26 external email client for email address field link 2018-06-08 11:25:32 +03:00
yuri
af9ca6788e re-render header on name change after sync 2018-06-07 16:28:48 +03:00
yuri
36d1c3af63 version 2018-06-07 13:58:52 +03:00
yuri
1853e98209 wysiwyg text filters support 2018-06-07 12:43:38 +03:00
yuri
f526d43798 improve dynamic logic conditions ui 2018-06-07 12:30:38 +03:00
yuri
ae8c76cecb fix select manager 2018-06-07 10:48:48 +03:00
yuri
2b32c94543 Merge branch 'hotfix/5.2.5' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.2.5 2018-06-07 10:45:38 +03:00
Taras Machyshyn
b6f5909df1 MySQL 8 bug fixes 2018-06-06 16:44:06 +03:00
yuri
ef35bbbb63 import: setting null for wrong date time 2018-06-05 13:11:34 +03:00
yuri
64f2cc6c7e fix import varchar length exceeded 2018-06-05 13:03:00 +03:00
yuri
a16635eb26 fix import ui 2018-06-05 13:02:31 +03:00
yuri
6e614f0a7d history has attachment 2018-06-04 16:40:07 +03:00
yuri
859f4eab0a target list panels change order 2018-06-04 14:33:11 +03:00
yuri
104e0b9079 target list changes 2 2018-06-04 14:25:00 +03:00
yuri
612abbf5c0 fix range fields 2018-06-04 12:46:54 +03:00
yuri
2dfbf71806 target list changes 2018-06-04 12:39:57 +03:00
yuri
c43f4d129d fix calendar 2018-06-04 10:47:14 +03:00
yuri
8d47a48f62 lang fix 2018-06-04 10:44:55 +03:00
yuri
9a333e6e38 list layout widthPx preserving 2018-06-01 12:07:45 +03:00
yuri
f6e0ef8cc6 layout manager data attribute list for custom layouts 2018-06-01 11:37:53 +03:00
yuri
36a45717f1 fix excel export varchar 2018-05-31 11:47:34 +03:00
yuri
86f63d72e1 fix list expanded 2018-05-31 11:41:38 +03:00
254 changed files with 4803 additions and 878 deletions

View File

@@ -118,6 +118,9 @@ module.exports = function (grunt) {
clean: {
start: ['build/*'],
final: ['build/tmp'],
beforeFinal: {
src: ['build/tmp/custom/Espo/Custom/*', '!build/tmp/custom/Espo/Custom/.htaccess']
}
},
less: lessData,
uglify: {
@@ -290,9 +293,10 @@ module.exports = function (grunt) {
'copy:frontendLib',
'copy:backend',
'replace',
'clean:beforeFinal',
'copy:final',
'chmod',
'clean:final',
'clean:final'
]);
};

View File

@@ -89,6 +89,9 @@ class EntityManager extends \Espo\Core\Controllers\Base
if (!empty($data['iconClass'])) {
$params['iconClass'] = $data['iconClass'];
}
if (isset($data['fullTextSearch'])) {
$params['fullTestSearch'] = $data['fullTextSearch'];
}
$params['kanbanViewMode'] = !empty($data['kanbanViewMode']);
if (!empty($data['kanbanStatusIgnoreList'])) {

View File

@@ -0,0 +1,60 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\BadRequest;
use \Espo\Core\Exceptions\Error;
class Pdf extends \Espo\Core\Controllers\Base
{
public function postActionMassPrint($params, $data)
{
if (empty($data->idList) || !is_array($data->idList)) {
throw new BadRequest();
}
if (empty($data->entityType)) {
throw new BadRequest();
}
if (empty($data->templateId)) {
throw new BadRequest();
}
if (!$this->getAcl()->checkScope('Template')) {
throw new Forbidden();
}
if (!$this->getAcl()->checkScope($data->entityType)) {
throw new Forbidden();
}
return [
'id' => $this->getServiceFactory()->create('Pdf')->massGenerate($data->entityType, $data->idList, $data->templateId, true)
];
}
}

View File

@@ -182,9 +182,15 @@ class AclManager
$impl = $this->getImplementation($scope);
$methodName = 'checkEntity' . ucfirst($action);
if (method_exists($impl, $methodName)) {
return $impl->$methodName($user, $entity, $data);
if (!$action) {
$action = 'read';
}
if ($action) {
$methodName = 'checkEntity' . ucfirst($action);
if (method_exists($impl, $methodName)) {
return $impl->$methodName($user, $entity, $data);
}
}
return $impl->checkEntity($user, $entity, $data, $action);

View File

@@ -128,7 +128,7 @@ class Application
$slim->run();
} catch (\Exception $e) {
$container->get('output')->processError($e->getMessage(), $e->getCode(), true);
$container->get('output')->processError($e->getMessage(), $e->getCode(), true, $e);
}
}
@@ -177,7 +177,7 @@ class Application
try {
$auth = $this->createAuth();
} catch (\Exception $e) {
$container->get('output')->processError($e->getMessage(), $e->getCode());
$container->get('output')->processError($e->getMessage(), $e->getCode(), false, $e);
}
$apiAuth = $this->createApiAuth($auth);
@@ -227,7 +227,7 @@ class Application
$result = $controllerManager->process($controllerName, $actionName, $params, $data, $slim->request());
$container->get('output')->render($result);
} catch (\Exception $e) {
$container->get('output')->processError($e->getMessage(), $e->getCode());
$container->get('output')->processError($e->getMessage(), $e->getCode(), false, $e);
}
});

View File

@@ -273,7 +273,7 @@ class Container
return new \Espo\Core\Utils\Metadata\OrmMetadata(
$this->get('metadata'),
$this->get('fileManager'),
$this->get('config')->get('useCache')
$this->get('config')
);
}

View File

@@ -133,11 +133,12 @@ class Record extends Base
$q = $request->get('q');
$textFilter = $request->get('textFilter');
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = self::MAX_SIZE_LIMIT;
$maxSize = $maxSizeLimit;
}
if (!empty($maxSize) && $maxSize > self::MAX_SIZE_LIMIT) {
throw new Forbidden("Max should should not exceed " . self::MAX_SIZE_LIMIT . ". Use pagination (offset, limit).");
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$params = array(
@@ -174,11 +175,12 @@ class Record extends Base
$q = $request->get('q');
$textFilter = $request->get('textFilter');
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = self::MAX_SIZE_LIMIT;
$maxSize = $maxSizeLimit;
}
if (!empty($maxSize) && $maxSize > self::MAX_SIZE_LIMIT) {
throw new Forbidden("Max should should not exceed " . self::MAX_SIZE_LIMIT . ". Use pagination (offset, limit).");
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$params = array(
@@ -213,6 +215,10 @@ class Record extends Base
if ($request->get('filterList')) {
$params['filterList'] = $request->get('filterList');
}
if ($request->get('select')) {
$params['select'] = explode(',', $request->get('select'));
}
}
public function actionListLinked($params, $data, $request)
@@ -228,11 +234,12 @@ class Record extends Base
$q = $request->get('q');
$textFilter = $request->get('textFilter');
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = self::MAX_SIZE_LIMIT;
$maxSize = $maxSizeLimit;
}
if (!empty($maxSize) && $maxSize > self::MAX_SIZE_LIMIT) {
throw new Forbidden();
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$params = array(

View File

@@ -53,6 +53,8 @@ class DataManager
*/
public function rebuild($entityList = null)
{
$this->populateConfigParameters();
$result = $this->clearCache();
$result &= $this->rebuildMetadata();
@@ -172,5 +174,25 @@ class DataManager
$this->getContainer()->get('config')->save();
return true;
}
}
protected function populateConfigParameters()
{
$config = $this->getContainer()->get('config');
$pdo = $this->getContainer()->get('entityManager')->getPDO();
$query = "SHOW VARIABLES LIKE 'ft_min_word_len'";
$sth = $pdo->prepare($query);
$sth->execute();
$fullTextSearchMinLength = null;
if ($row = $sth->fetch(\PDO::FETCH_ASSOC)) {
if (isset($row['Value'])) {
$fullTextSearchMinLength = intval($row['Value']);
}
}
$config->set('fullTextSearchMinLength', $fullTextSearchMinLength);
$config->save();
}
}

View File

@@ -62,18 +62,26 @@ class Xlsx extends \Espo\Core\Injectable
public function loadAdditionalFields(Entity $entity, $fieldList)
{
foreach ($entity->getRelationList() as $link) {
if ($entity->getRelationType($link) === 'belongsToParent') {
if (in_array($link, $fieldList)) {
$parent = $entity->get($link);
if ($parent instanceof Entity) {
$entity->set($link . 'Name', $parent->get('name'));
if (in_array($link, $fieldList)) {
if ($entity->getRelationType($link) === 'belongsToParent') {
if (!$entity->get($link . 'Name')) {
$entity->loadParentNameField($link);
}
}
} else if ($entity->getRelationType($link) === 'belongsTo' && $entity->getRelationParam($link, 'noJoin') && $entity->hasField($link . 'Name')) {
if (in_array($link, $fieldList)) {
$related = $entity->get($link);
if ($related instanceof Entity) {
$entity->set($link . 'Name', $related->get('name'));
} else if (
(
(
$entity->getRelationType($link) === 'belongsTo'
&&
$entity->getRelationParam($link, 'noJoin')
)
||
$entity->getRelationType($link) === 'hasOne'
)
&&
$entity->hasAttribute($link . 'Name')
) {
if (!$entity->get($link . 'Name') || !$entity->get($link . 'Id')) {
$entity->loadLinkField($link);
}
}
}
@@ -469,7 +477,7 @@ class Xlsx extends \Espo\Core\Injectable
} else {
if (array_key_exists($name, $row)) {
$sheet->setCellValue("$col$rowNumber", $row[$name]);
$sheet->setCellValueExplicit("$col$rowNumber", $row[$name], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
}
}

View File

@@ -0,0 +1,89 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\EntityGroup;
use \Espo\ORM\Entity;
use \Espo\Core\Exceptions\Error;
class CountRelatedType extends \Espo\Core\Formula\Functions\Base
{
protected function init()
{
$this->addDependency('entityManager');
$this->addDependency('selectManagerFactory');
}
public function process(\StdClass $item)
{
if (!property_exists($item, 'value')) {
throw new Error();
}
if (!is_array($item->value)) {
throw new Error();
}
if (count($item->value) < 1) {
throw new Error();
}
$link = $this->evaluate($item->value[0]);
if (empty($link)) {
throw new Error("No link passed to countRelated function.");
}
$filter = null;
if (count($item->value) > 1) {
$filter = $this->evaluate($item->value[1]);
}
$entity = $this->getEntity();
$entityManager = $this->getInjection('entityManager');
$foreignEntityType = $entity->getRelationParam($link, 'entity');
if (empty($foreignEntityType)) {
throw new Error();
}
$foreignSelectManager = $this->getInjection('selectManagerFactory')->create($foreignEntityType);
$selectParams = $foreignSelectManager->getEmptySelectParams();
if ($filter) {
$foreignSelectManager->applyFilter($filter, $selectParams);
}
return $entityManager->getRepository($entity->getEntityType())->countRelated($entity, $link, $selectParams);
}
}

View File

@@ -0,0 +1,119 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\EntityGroup;
use \Espo\ORM\Entity;
use \Espo\Core\Exceptions\Error;
class SumRelatedType extends \Espo\Core\Formula\Functions\Base
{
protected function init()
{
$this->addDependency('entityManager');
$this->addDependency('selectManagerFactory');
}
public function process(\StdClass $item)
{
if (!property_exists($item, 'value')) {
throw new Error();
}
if (!is_array($item->value)) {
throw new Error();
}
if (count($item->value) < 2) {
throw new Error();
}
$link = $this->evaluate($item->value[0]);
if (empty($link)) {
throw new Error("No link passed to sumRelated function.");
}
$field = $this->evaluate($item->value[1]);
if (empty($field)) {
throw new Error("No field passed to sumRelated function.");
}
$filter = null;
if (count($item->value) > 2) {
$filter = $this->evaluate($item->value[2]);
}
$entity = $this->getEntity();
$entityManager = $this->getInjection('entityManager');
$foreignEntityType = $entity->getRelationParam($link, 'entity');
if (empty($foreignEntityType)) {
throw new Error();
}
$foreignSelectManager = $this->getInjection('selectManagerFactory')->create($foreignEntityType);
$foreignLink = $entity->getRelationParam($link, 'foreign');
if (empty($foreignLink)) {
throw new Error("No foreign link for link {$link}.");
}
$selectParams = $foreignSelectManager->getEmptySelectParams();
if ($filter) {
$foreignSelectManager->applyFilter($filter, $selectParams);
}
$selectParams['select'] = [[$foreignLink . '.id', 'foreignId'], 'SUM:' . $field];
$foreignSelectManager->addJoin($foreignLink, $selectParams);
$selectParams['groupBy'] = [$foreignLink . '.id'];
$entityManager->getRepository($foreignEntityType)->handleSelectParams($selectParams);
$sql = $entityManager->getQuery()->createSelectQuery($foreignEntityType, $selectParams);
$pdo = $entityManager->getPDO();
$sth = $pdo->prepare($sql);
$sth->execute();
$rowList = $sth->fetchAll(\PDO::FETCH_ASSOC);
if (empty($rowList)) {
return 0;
}
return $rowList[0]['SUM:' . $field];
}
}

View File

@@ -100,10 +100,10 @@ class Htmlizer
$forbidenAttributeList = $this->getAcl()->getScopeForbiddenAttributeList($entity->getEntityType(), 'read');
}
foreach ($fieldList as $field) {
if (in_array($field, $forbidenAttributeList)) continue;
$type = $entity->getAttributeType($field);
if ($type == Entity::DATETIME) {
@@ -219,6 +219,14 @@ class Htmlizer
return number_format($number, $decimals, $decimalPoint, $thousandsSeparator);
}
return '';
},
'var' => function ($context, $options) {
if ($context && isset($context[0]) && isset($context[1])) {
if (isset($context[1][$context[0]])) {
return $context[1][$context[0]];
}
}
return;
}
],
'hbhelpers' => [

View File

@@ -205,6 +205,10 @@ class Importer
))->findOne();
if ($replied) {
$email->set('repliedId', $replied->id);
$repliedTeamIdList = $replied->getLinkMultipleIdList('teams');
foreach ($repliedTeamIdList as $repliedTeamId) {
$email->addLinkMultipleId('teams', $repliedTeamId);
}
}
}
@@ -296,6 +300,23 @@ class Importer
$this->getEntityManager()->getPdo()->query('UNLOCK TABLES');
if ($parentFound) {
$parentType = $email->get('parentType');
$parentId = $email->get('parentId');
$emailKeepParentTeamsEntityList = $this->getConfig()->get('emailKeepParentTeamsEntityList', []);
if ($parentId && in_array($parentType, $emailKeepParentTeamsEntityList)) {
if ($this->getEntityManager()->hasRepository($parentType)) {
$parent = $this->getEntityManager()->getEntity($parentType, $parentId);
if ($parent) {
$parentTeamIdList = $parent->getLinkMultipleIdList('teams');
foreach ($parentTeamIdList as $parentTeamId) {
$email->addLinkMultipleId('teams', $parentTeamId);
}
}
}
}
}
$this->getEntityManager()->saveEntity($email);
foreach ($inlineAttachmentList as $attachment) {

View File

@@ -89,8 +89,12 @@ class Sender
$this->transport = new SmtpTransport();
$config = $this->config;
$localHostName = $config->get('smtpLocalHostName', gethostname());
$opts = array(
'name' => 'admin',
'name' => $localHostName,
'host' => $params['server'],
'port' => $params['port'],
'connection_config' => array()
@@ -132,8 +136,10 @@ class Sender
$config = $this->config;
$localHostName = $config->get('smtpLocalHostName', gethostname());
$opts = array(
'name' => 'admin',
'name' => $localHostName,
'host' => $config->get('smtpServer'),
'port' => $config->get('smtpPort'),
'connection_config' => array()

View File

@@ -53,11 +53,6 @@ class Entity extends \Espo\ORM\Entity
$repository = $this->entityManager->getRepository($parentType);
$select = ['id', 'name'];
if ($parentType === 'Lead') {
$select[] = 'accountName';
$select[] = 'emailAddress';
$select[] = 'phoneNumber';
}
$foreignEntity = $repository->select($select)->where(['id' => $parentId])->findOne();
if ($foreignEntity) {
$this->set($field . 'Name', $foreignEntity->get('name'));
@@ -109,11 +104,6 @@ class Entity extends \Espo\ORM\Entity
}
$defs['select'] = ['id', 'name'];
if ($foreignEntityType === 'Lead') {
$defs['select'][] = 'accountName';
$defs['select'][] = 'emailAddress';
$defs['select'][] = 'phoneNumber';
}
$hasType = false;
if ($this->hasField($field . 'Types')) {
@@ -165,7 +155,13 @@ class Entity extends \Espo\ORM\Entity
if (!$this->hasRelation($field) || !$this->hasAttribute($field . 'Id')) return;
if ($this->getRelationType($field) !== 'hasOne' && $this->getRelationType($field) !== 'belongsTo') return;
$entity = $this->get($field);
$relatedEntityType = $this->getRelationParam($field, 'entity');
$select = ['id', 'name'];
$entity = $this->get($field, [
'select' => $select
]);
$entityId = null;
$entityName = null;

View File

@@ -40,6 +40,13 @@ class Tcpdf extends \TCPDF
protected $footerPosition = 15;
protected $useGroupNumbers = false;
public function setUseGroupNumbers($value)
{
$this->useGroupNumbers = $value;
}
public function setFooterHtml($html)
{
$this->footerHtml = $html;
@@ -58,7 +65,16 @@ class Tcpdf extends \TCPDF
$this->SetY((-1) * $this->footerPosition);
$html = str_replace('{pageNumber}', '{:pnp:}', $this->footerHtml);
$html = $this->footerHtml;
if ($this->useGroupNumbers) {
$html = str_replace('{pageNumber}', '{:png:}', $html);
$html = str_replace('{pageAbsoluteNumber}', '{:pnp:}', $html);
} else {
$html = str_replace('{pageNumber}', '{:pnp:}', $html);
$html = str_replace('{pageAbsoluteNumber}', '{:pnp:}', $html);
}
$this->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, '', 0, false, 'T');
$this->SetAutoPageBreak($autoPageBreak, $breakMargin);
@@ -98,6 +114,9 @@ class Tcpdf extends \TCPDF
++$pagegroupnum;
$pnga = TCPDF_STATIC::formatPageNumber($pagegroupnum);
$pngu = TCPDF_FONTS::UTF8ToUTF16BE($pnga, false, $this->isunicode, $this->CurrentFont);
$pnga = $pngu;
$png_num_chars = $this->GetNumChars($pnga);
// replace page numbers
$replace = array();

View File

@@ -64,6 +64,10 @@ class Base
const MIN_LENGTH_FOR_CONTENT_SEARCH = 4;
const MIN_LENGTH_FOR_FULL_TEXT_SEARCH = 4;
protected $fullTextSearchDataCacheHash = [];
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Metadata $metadata, Config $config, InjectableFactory $injectableFactory)
{
$this->entityManager = $entityManager;
@@ -146,7 +150,7 @@ class Base
} else {
$orderPart = 'DESC';
}
$result['orderBy'] = [[$sortBy . 'Country', $orderPart], [$sortBy . 'City', $orderPart], [$sortBy . 'Street', $orderPart]];
$result['orderBy'] = [[$sortBy . 'Country', $orderPart], [$sortBy . 'City', $orderPart], [$sortBy . '_eet', $orderPart]];
return;
} else if ($type === 'enum') {
$list = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'fields', $sortBy, 'options']);
@@ -203,8 +207,8 @@ class Base
}
$this->applyBoolFilter($filter, $result);
}
} else if ($item['type'] == 'textFilter' && !empty($item['value'])) {
if (!empty($item['value'])) {
} else if ($item['type'] == 'textFilter') {
if (isset($item['value']) || $item['value'] !== '') {
$this->textFilter($item['value'], $result);
}
} else if ($item['type'] == 'primary' && !empty($item['value'])) {
@@ -387,7 +391,7 @@ class Base
protected function q($params, &$result)
{
if (!empty($params['q'])) {
if (isset($params['q']) && $params['q'] !== '') {
$this->textFilter($params['q'], $result);
}
}
@@ -401,7 +405,7 @@ class Base
public function manageTextFilter($textFilter, &$result)
{
$this->prepareResult($result);
$this->q(array('q' => $textFilter), $result);
$this->q(['q' => $textFilter], $result);
}
public function getEmptySelectParams()
@@ -732,7 +736,7 @@ class Base
$this->where($params['where'], $result);
}
if (!empty($params['textFilter'])) {
if (isset($params['textFilter']) && $params['textFilter'] !== '') {
$this->textFilter($params['textFilter'], $result);
}
@@ -1007,7 +1011,7 @@ class Base
foreach ($item['value'] as $i) {
$a = $this->getWherePart($i, $result);
foreach ($a as $left => $right) {
if (!empty($right) || is_null($right) || $right === '') {
if (!empty($right) || is_null($right) || $right === '' || $right === 0 || $right === false) {
$arr[] = array($left => $right);
}
}
@@ -1500,36 +1504,211 @@ class Base
);
}
public function getFullTextSearchDataForTextFilter($textFilter, $isAuxiliaryUse = false)
{
if (array_key_exists($textFilter, $this->fullTextSearchDataCacheHash)) {
return $this->fullTextSearchDataCacheHash[$textFilter];
}
if ($this->getConfig()->get('fullTextSearchDisabled')) {
return null;
}
$result = null;
$fieldList = $this->getTextFilterFieldList();
if ($isAuxiliaryUse) {
$textFilter = str_replace('%', '', $textFilter);
}
$fullTextSearchColumnList = $this->getEntityManager()->getOrmMetadata()->get($this->getEntityType(), ['fullTextSearchColumnList']);
$useFullTextSearch = false;
if (
$this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'collection', 'fullTextSearch'])
&&
!empty($fullTextSearchColumnList)
) {
$fullTextSearchMinLength = $this->getConfig()->get('fullTextSearchMinLength', self::MIN_LENGTH_FOR_FULL_TEXT_SEARCH);
if (!$fullTextSearchMinLength) {
$fullTextSearchMinLength = 0;
}
if (mb_strlen($textFilter) >= $fullTextSearchMinLength) {
$useFullTextSearch = true;
}
}
$fullTextSearchFieldList = [];
if ($useFullTextSearch) {
foreach ($fieldList as $field) {
$defs = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'fields', $field], []);
if (empty($defs['type'])) continue;
$fieldType = $defs['type'];
if (!empty($defs['notStorable'])) continue;
if (!$this->getMetadata()->get(['fields', $fieldType, 'fullTextSearch'])) continue;
$fullTextSearchFieldList[] = $field;
}
if (!count($fullTextSearchFieldList)) {
$useFullTextSearch = false;
}
}
if (empty($fullTextSearchColumnList)) {
$useFullTextSearch = false;
}
if ($useFullTextSearch) {
if (
$isAuxiliaryUse
||
mb_strpos($textFilter, ' ') === false
&&
mb_strpos($textFilter, '+') === false
&&
mb_strpos($textFilter, '-') === false
&&
mb_strpos($textFilter, '*') === false
) {
$function = 'MATCH_NATURAL_LANGUAGE';
} else {
$function = 'MATCH_BOOLEAN';
}
$fullTextSearchColumnSanitizedList = [];
$query = $this->getEntityManager()->getQuery();
foreach ($fullTextSearchColumnList as $i => $field) {
$fullTextSearchColumnSanitizedList[$i] = $query->sanitize($query->toDb($field));
}
$where = $function . ':' . implode(',', $fullTextSearchColumnSanitizedList) . ':' . $textFilter;
$result = [
'where' => $where,
'fieldList' => $fullTextSearchFieldList,
'columnList' => $fullTextSearchColumnList
];
}
$this->fullTextSearchDataCacheHash[$textFilter] = $result;
return $result;
}
protected function textFilter($textFilter, &$result)
{
$fieldDefs = $this->getSeed()->getAttributes();
$fieldList = $this->getTextFilterFieldList();
$d = array();
$group = [];
$textFilterContainsMinLength = $this->getConfig()->get('textFilterContainsMinLength', self::MIN_LENGTH_FOR_CONTENT_SEARCH);
foreach ($fieldList as $field) {
if (
strlen($textFilter) >= $textFilterContainsMinLength
&&
(
!empty($fieldDefs[$field]['type']) && $fieldDefs[$field]['type'] == 'text'
||
!empty($this->textFilterUseContainsAttributeList[$field])
||
!empty($fieldDefs[$field]['type']) && $fieldDefs[$field]['type'] == 'varchar' &&
$this->getConfig()->get('textFilterUseContainsForVarchar')
)
) {
$expression = '%' . $textFilter . '%';
} else {
$expression = $textFilter . '%';
}
$d[$field . '*'] = $expression;
$fullTextSearchData = null;
$forceFullTextSearch = false;
$useFullTextSearch = !empty($result['useFullTextSearch']);
if (mb_strpos($textFilter, 'ft:') === 0) {
$textFilter = mb_substr($textFilter, 3);
$useFullTextSearch = true;
$forceFullTextSearch = true;
}
$result['whereClause'][] = array(
'OR' => $d
);
$skipWidlcards = false;
if (!$useFullTextSearch) {
if (mb_strpos($textFilter, '*') !== false) {
$skipWidlcards = true;
$textFilter = str_replace('*', '%', $textFilter);
}
}
$fullTextSearchData = $this->getFullTextSearchDataForTextFilter($textFilter, !$useFullTextSearch);
$fullTextGroup = [];
$fullTextSearchFieldList = [];
if ($fullTextSearchData) {
$fullTextGroup[] = $fullTextSearchData['where'];
$fullTextSearchFieldList = $fullTextSearchData['fieldList'];
}
foreach ($fieldList as $field) {
if ($useFullTextSearch) {
if (in_array($field, $fullTextSearchFieldList)) continue;
}
if ($forceFullTextSearch) continue;
$attributeType = null;
if (!empty($fieldDefs[$field]['type'])) {
$attributeType = $fieldDefs[$field]['type'];
}
if ($attributeType === 'int') {
if (is_numeric($textFilter)) {
$group[$field] = intval($textFilter);
}
continue;
}
if (!$skipWidlcards) {
if (
mb_strlen($textFilter) >= $textFilterContainsMinLength
&&
(
$attributeType == 'text'
||
in_array($field, $this->textFilterUseContainsAttributeList)
||
$attributeType == 'varchar' && $this->getConfig()->get('textFilterUseContainsForVarchar')
)
) {
$expression = '%' . $textFilter . '%';
} else {
$expression = $textFilter . '%';
}
} else {
$expression = $textFilter;
}
if ($fullTextSearchData) {
if (!$useFullTextSearch) {
if (in_array($field, $fullTextSearchFieldList)) {
if (!array_key_exists('OR', $fullTextGroup)) {
$fullTextGroup['OR'] = [];
}
$fullTextGroup['OR'][$field . '*'] = $expression;
continue;
}
}
}
$group[$field . '*'] = $expression;
}
if (!$forceFullTextSearch) {
$this->applyAdditionalToTextFilterGroup($textFilter, $group, $result);
}
if (!empty($fullTextGroup)) {
$group['AND'] = $fullTextGroup;
}
if (count($group) === 0) {
$result['whereClause'][] = [
'id' => null
];
}
$result['whereClause'][] = [
'OR' => $group
];
}
protected function applyAdditionalToTextFilterGroup($textFilter, &$group, &$result)
{
}
public function applyAccess(&$result)

View File

@@ -149,6 +149,11 @@ class Install extends \Espo\Core\Upgrades\Actions\Base\Install
'fileList' => $fileList,
'description' => $manifest['description'],
);
if (!empty($manifest['checkVersionUrl'])) {
$data['checkVersionUrl'] = $manifest['checkVersionUrl'];
}
$extensionEntity->set($data);
return $entityManager->saveEntity($extensionEntity);

View File

@@ -95,7 +95,13 @@ class AdminNotificationManager
$extensionsNeedingUpgrade = $this->getExtensionsNeedingUpgrade();
if (!empty($extensionsNeedingUpgrade)) {
foreach ($extensionsNeedingUpgrade as $extensionName => $extensionDetails) {
$message = $this->getLanguage()->translate('newExtensionVersionIsAvailable', 'messages', 'Admin');
$label = 'new' . Util::toCamelCase($extensionName, ' ', true) .'VersionIsAvailable';
$message = $this->getLanguage()->get(['Admin', 'messages', $label]);
if (!$message) {
$message = $this->getLanguage()->translate('newExtensionVersionIsAvailable', 'messages', 'Admin');
}
$notificationList[] = array(
'id' => 'newExtensionVersionIsAvailable' . Util::toCamelCase($extensionName, ' ', true),
'type' => 'newExtensionVersionIsAvailable',

View File

@@ -42,6 +42,9 @@ class Output
500 => 'Internal Server Error',
);
protected $ignorePrintXStatusReasonExceptionClassNameList = [
'PDOException'
];
public function __construct(\Espo\Core\Utils\Api\Slim $slim)
{
@@ -69,7 +72,7 @@ class Output
echo $data;
}
public function processError($message = 'Error', $code = 500, $isPrint = false)
public function processError($message = 'Error', $code = 500, $isPrint = false, $exception = null)
{
$currentRoute = $this->getSlim()->router()->getCurrentRoute();
@@ -79,7 +82,7 @@ class Output
$GLOBALS['log']->error('API ['.$this->getSlim()->request()->getMethod().']:'.$currentRoute->getPattern().', Params:'.print_r($currentRoute->getParams(), true).', InputData: '.$inputData.' - '.$message);
}
$this->displayError($message, $code, $isPrint);
$this->displayError($message, $code, $isPrint, $exception);
}
/**
@@ -90,15 +93,21 @@ class Output
*
* @return void
*/
public function displayError($text, $statusCode = 500, $isPrint = false)
public function displayError($text, $statusCode = 500, $isPrint = false, $exception = null)
{
$GLOBALS['log']->error('Display Error: '.$text.', Code: '.$statusCode.' URL: '.$_SERVER['REQUEST_URI']);
ob_clean();
if (!empty( $this->slim)) {
$toPrintXStatusReason = true;
if ($exception && in_array(get_class($exception), $this->ignorePrintXStatusReasonExceptionClassNameList)) {
$toPrintXStatusReason = false;
}
$this->getSlim()->response()->setStatus($statusCode);
$this->getSlim()->response()->headers->set('X-Status-Reason', $text);
if ($toPrintXStatusReason) {
$this->getSlim()->response()->headers->set('X-Status-Reason', $text);
}
if ($isPrint) {
$status = $this->getCodeDesc($statusCode);

View File

@@ -251,6 +251,8 @@ class LDAP extends Base
$data[$fieldName] = $fieldValue;
}
$this->getAuth()->useNoAuth();
$user = $this->getEntityManager()->getEntity('User');
$user->set($data);

View File

@@ -29,8 +29,8 @@
namespace Espo\Core\Utils\Database;
use Espo\Core\Utils\Util,
Espo\ORM\Entity;
use Espo\Core\Utils\Util;
use Espo\ORM\Entity;
class Converter
{
@@ -38,15 +38,18 @@ class Converter
private $fileManager;
private $config;
private $schemaConverter;
private $schemaFromMetadata = null;
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager)
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager, \Espo\Core\Utils\Config $config = null)
{
$this->metadata = $metadata;
$this->fileManager = $fileManager;
$this->ormConverter = new Orm\Converter($this->metadata, $this->fileManager);
$this->config = $config;
$this->ormConverter = new Orm\Converter($this->metadata, $this->fileManager, $this->config);
}
protected function getMetadata()

View File

@@ -315,12 +315,142 @@ class MySqlPlatform extends \Doctrine\DBAL\Platforms\MySqlPlatform
$options['collate'] = 'utf8mb4_unicode_ci';
}
return parent::_getCreateTableSQL($tableName, $columns, $options);
$queryFields = $this->getColumnDeclarationListSQL($columns);
if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
foreach ($options['uniqueConstraints'] as $index => $definition) {
$queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($index, $definition);
}
}
// add all indexes
if (isset($options['indexes']) && ! empty($options['indexes'])) {
foreach($options['indexes'] as $index => $definition) {
$queryFields .= ', ' . $this->getIndexDeclarationSQL($index, $definition);
}
}
// attach all primary keys
if (isset($options['primary']) && ! empty($options['primary'])) {
$keyColumns = array_unique(array_values($options['primary']));
$queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
}
$query = 'CREATE ';
if (!empty($options['temporary'])) {
$query .= 'TEMPORARY ';
}
$query .= 'TABLE ' . $this->espoQuote($tableName) . ' (' . $queryFields . ') ';
$query .= $this->buildTableOptions($options);
$query .= $this->buildPartitionOptions($options);
$sql[] = $query;
if (isset($options['foreignKeys'])) {
foreach ((array) $options['foreignKeys'] as $definition) {
$sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
}
}
return $sql;
}
public function getColumnCollationDeclarationSQL($collation)
{
return $this->getCollationFieldDeclaration($collation);
}
/**
* Build SQL for table options
*
* @param array $options
*
* @return string
*/
private function buildTableOptions(array $options)
{
if (isset($options['table_options'])) {
return $options['table_options'];
}
$tableOptions = array();
// Charset
if ( ! isset($options['charset'])) {
$options['charset'] = 'utf8';
}
$tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']);
// Collate
if ( ! isset($options['collate'])) {
$options['collate'] = 'utf8_unicode_ci';
}
$tableOptions[] = sprintf('COLLATE %s', $options['collate']);
// Engine
if ( ! isset($options['engine'])) {
$options['engine'] = 'InnoDB';
}
$tableOptions[] = sprintf('ENGINE = %s', $options['engine']);
// Auto increment
if (isset($options['auto_increment'])) {
$tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']);
}
// Comment
if (isset($options['comment'])) {
$comment = trim($options['comment'], " '");
$tableOptions[] = sprintf("COMMENT = '%s' ", str_replace("'", "''", $comment));
}
// Row format
if (isset($options['row_format'])) {
$tableOptions[] = sprintf('ROW_FORMAT = %s', $options['row_format']);
}
return implode(' ', $tableOptions);
}
/**
* Build SQL for partition options.
*
* @param array $options
*
* @return string
*/
private function buildPartitionOptions(array $options)
{
return (isset($options['partition_options']))
? ' ' . $options['partition_options']
: '';
}
public function getClobTypeDeclarationSQL(array $field)
{
if ( ! empty($field['length']) && is_numeric($field['length'])) {
$length = $field['length'];
if ($length <= static::LENGTH_LIMIT_TINYTEXT) {
return 'TINYTEXT';
}
if ($length <= static::LENGTH_LIMIT_TEXT) {
return 'TEXT';
}
if ($length > static::LENGTH_LIMIT_MEDIUMTEXT) {
return 'LONGTEXT';
}
}
return 'MEDIUMTEXT';
}
//end: ESPO
}

View File

@@ -0,0 +1,188 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database;
use Espo\Core\Utils\Util;
use Espo\ORM\Entity;
class Helper
{
private $config;
private $connection;
protected $drivers = array(
'mysqli' => '\Espo\Core\Utils\Database\DBAL\Driver\Mysqli\Driver',
'pdo_mysql' => '\Espo\Core\Utils\Database\DBAL\Driver\PDOMySql\Driver',
);
public function __construct(\Espo\Core\Utils\Config $config = null)
{
$this->config = $config;
}
protected function getConfig()
{
return $this->config;
}
public function getDbalConnection()
{
if (!isset($this->connection)) {
if (!$this->getConfig()) {
return null;
}
$connectionParams = $this->getConfig()->get('database');
if (empty($connectionParams['dbname']) || empty($connectionParams['user'])) {
return null;
}
$connectionParams['driverClass'] = $this->drivers[ $connectionParams['driver'] ];
unset($connectionParams['driver']);
$dbalConfig = new \Doctrine\DBAL\Configuration();
$this->connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $dbalConfig);
}
return $this->connection;
}
/**
* Get maximum index length. If $tableName is empty get a value for all database tables
*
* @param string|null $tableName
*
* @return int
*/
public function getMaxIndexLength($tableName = null, $default = 1000)
{
$mysqlEngine = $this->getMysqlEngine($tableName);
if (!$mysqlEngine) {
return $default;
}
switch ($mysqlEngine) {
case 'InnoDB':
$mysqlVersion = $this->getMysqlVersion();
if (version_compare($mysqlVersion, '10.0.0') >= 0) {
return 767; //InnoDB, MariaDB
}
if (version_compare($mysqlVersion, '5.7.0') >= 0) {
return 3072; //InnoDB, MySQL 5.7+
}
return 767; //InnoDB
break;
}
return 1000; //MyISAM
}
public function getTableMaxIndexLength($tableName, $default = 1000)
{
return $this->getMaxIndexLength($tableName, $default);
}
protected function getMysqlVersion()
{
$connection = $this->getDbalConnection();
if (!$connection) {
return null;
}
return $connection->fetchColumn("select version()");
}
/**
* Get table/database tables engine. If $tableName is empty get a value for all database tables
*
* @param string|null $tableName
*
* @return string
*/
protected function getMysqlEngine($tableName = null, $default = null)
{
$connection = $this->getDbalConnection();
if (!$connection) {
return $default;
}
$query = "SHOW TABLE STATUS WHERE Engine = 'MyISAM'";
if (!empty($tableName)) {
$query = "SHOW TABLE STATUS WHERE Engine = 'MyISAM' AND Name = '" . $tableName . "'";
}
$result = $connection->fetchColumn($query);
if (!empty($result)) {
return 'MyISAM';
}
return 'InnoDB';
}
/**
* Check if full text supports. If $tableName is empty get a value for all database tables
*
* @param string $tableName
*
* @return boolean
*/
public function isSupportsFulltext($tableName = null, $default = false)
{
$mysqlEngine = $this->getMysqlEngine($tableName);
if (!$mysqlEngine) {
return $default;
}
switch ($mysqlEngine) {
case 'InnoDB':
$mysqlVersion = $this->getMysqlVersion();
if (version_compare($mysqlVersion, '5.6.4') >= 0) {
return true; //InnoDB, MySQL 5.6.4+
}
return false; //InnoDB
break;
}
return true; //MyISAM
}
public function isTableSupportsFulltext($tableName, $default = false)
{
return $this->isSupportsFulltext($tableName, $default);
}
}

View File

@@ -28,20 +28,28 @@
************************************************************************/
namespace Espo\Core\Utils\Database\Orm;
use Espo\Core\Utils\Util,
Espo\ORM\Entity;
use Espo\Core\Utils\Util;
use Espo\ORM\Entity;
class Converter
{
private $metadata;
private $fileManager;
private $config;
private $metadataHelper;
private $databaseHelper;
private $relationManager;
private $entityDefs;
protected $defaultFieldType = 'varchar';
protected $defaultNaming = 'postfix';
protected $defaultLength = array(
@@ -99,14 +107,16 @@ class Converter
'additionalTables',
);
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager)
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager, \Espo\Core\Utils\Config $config = null)
{
$this->metadata = $metadata;
$this->fileManager = $fileManager; //need to featue with ormHooks. Ex. isFollowed field
$this->config = $config;
$this->relationManager = new RelationManager($this->metadata);
$this->metadataHelper = new \Espo\Core\Utils\Metadata\Helper($this->metadata);
$this->databaseHelper = new \Espo\Core\Utils\Database\Helper($this->config);
}
protected function getMetadata()
@@ -114,6 +124,11 @@ class Converter
return $this->metadata;
}
protected function getConfig()
{
return $this->config;
}
protected function getEntityDefs($reload = false)
{
if (empty($this->entityDefs) || $reload) {
@@ -138,6 +153,11 @@ class Converter
return $this->metadataHelper;
}
protected function getDatabaseHelper()
{
return $this->databaseHelper;
}
/**
* Orm metadata convertation process
*
@@ -186,6 +206,8 @@ class Converter
$ormMetadata = Util::merge($ormMetadata, $convertedLinks);
$this->applyFullTextSearch($ormMetadata, $entityName);
if (!empty($entityMetadata['collection']) && is_array($entityMetadata['collection'])) {
$collectionDefs = $entityMetadata['collection'];
$ormMetadata[$entityName]['collection'] = array();
@@ -467,4 +489,47 @@ class Converter
return $values;
}
protected function applyFullTextSearch(&$ormMetadata, $entityType)
{
if (!$this->getDatabaseHelper()->isTableSupportsFulltext(Util::toUnderScore($entityType))) return;
if (!$this->getMetadata()->get(['entityDefs', $entityType, 'collection', 'fullTextSearch'])) return;
$fieldList = $this->getMetadata()->get(['entityDefs', $entityType, 'collection', 'textFilterFields'], ['name']);
$fullTextSearchColumnList = [];
foreach ($fieldList as $field) {
$defs = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field], []);
if (empty($defs['type'])) continue;
$fieldType = $defs['type'];
if (!empty($defs['notStorable'])) continue;
if (!$this->getMetadata()->get(['fields', $fieldType, 'fullTextSearch'])) continue;
$partList = $this->getMetadata()->get(['fields', $fieldType, 'fullTextSearchColumnList']);
if ($partList) {
if ($this->getMetadata()->get(['fields', $fieldType, 'naming']) === 'prefix') {
foreach ($partList as $part) {
$fullTextSearchColumnList[] = $part . ucfirst($field);
}
} else {
foreach ($partList as $part) {
$fullTextSearchColumnList[] = $field . ucfirst($part);
}
}
} else {
$fullTextSearchColumnList[] = $field;
}
}
if (!empty($fullTextSearchColumnList)) {
$ormMetadata[$entityType]['fullTextSearchColumnList'] = $fullTextSearchColumnList;
if (!array_key_exists('indexes', $ormMetadata[$entityType])) {
$ormMetadata[$entityType]['indexes'] = [];
}
$ormMetadata[$entityType]['indexes']['system_fullTextSearch'] = [
'columns' => $fullTextSearchColumnList,
'flags' => ['fulltext']
];
}
}
}

View File

@@ -40,7 +40,8 @@ class AttachmentMultiple extends Base
'type' => 'jsonArray',
'notStorable' => true,
'orderBy' => [['createdAt', 'ASC'], ['name', 'ASC']],
'isLinkMultipleIdList' => true
'isLinkMultipleIdList' => true,
'relation' => $fieldName
),
$fieldName.'Names' => array(
'type' => 'jsonObject',

View File

@@ -46,40 +46,40 @@ class Email extends Base
JOIN email_address ON email_address.id = entity_email_address.email_address_id
WHERE
entity_email_address.deleted = 0 AND entity_email_address.entity_type = '{$entityName}' AND
email_address.deleted = 0 AND email_address.name LIKE {value}
email_address.deleted = 0 AND email_address.lower LIKE {value}
)",
'=' => array(
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
'sql' => 'emailAddressesMultiple.name = {value}',
'sql' => 'emailAddressesMultiple.lower = {value}',
'distinct' => true
),
'<>' => array(
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
'sql' => 'emailAddressesMultiple.name <> {value}',
'sql' => 'emailAddressesMultiple.lower <> {value}',
'distinct' => true
),
'IN' => array(
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
'sql' => 'emailAddressesMultiple.name IN {value}',
'sql' => 'emailAddressesMultiple.lower IN {value}',
'distinct' => true
),
'NOT IN' => array(
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
'sql' => 'emailAddressesMultiple.name NOT IN {value}',
'sql' => 'emailAddressesMultiple.lower NOT IN {value}',
'distinct' => true
),
'IS NULL' => array(
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
'sql' => 'emailAddressesMultiple.name IS NULL',
'sql' => 'emailAddressesMultiple.lower IS NULL',
'distinct' => true
),
'IS NOT NULL' => array(
'leftJoins' => [['emailAddresses', 'emailAddressesMultiple']],
'sql' => 'emailAddressesMultiple.name IS NOT NULL',
'sql' => 'emailAddressesMultiple.lower IS NOT NULL',
'distinct' => true
)
),
'orderBy' => 'emailAddresses.name {direction}',
'orderBy' => 'emailAddresses.lower {direction}',
),
$fieldName .'Data' => array(
'type' => 'text',

View File

@@ -43,11 +43,11 @@ class HasMany extends Base
$entityName => array (
'fields' => array(
$linkName.'Ids' => array(
'type' => 'varchar',
'type' => 'jsonArray',
'notStorable' => true,
),
$linkName.'Names' => array(
'type' => 'varchar',
'type' => 'jsonObject',
'notStorable' => true,
),
),

View File

@@ -50,11 +50,11 @@ class ManyMany extends Base
$entityName => array(
'fields' => array(
$linkName.'Ids' => array(
'type' => 'varchar',
'type' => 'jsonArray',
'notStorable' => true,
),
$linkName.'Names' => array(
'type' => 'varchar',
'type' => 'jsonObject',
'notStorable' => true,
),
),

View File

@@ -81,11 +81,12 @@ class Converter
protected $maxIndexLength;
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager, \Espo\Core\Utils\Database\Schema\Schema $databaseSchema)
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager, \Espo\Core\Utils\Database\Schema\Schema $databaseSchema, \Espo\Core\Utils\Config $config = null)
{
$this->metadata = $metadata;
$this->fileManager = $fileManager;
$this->databaseSchema = $databaseSchema;
$this->config = $config;
$this->typeList = array_keys(\Doctrine\DBAL\Types\Type::getTypesMap());
}
@@ -100,6 +101,11 @@ class Converter
return $this->fileManager;
}
protected function getConfig()
{
return $this->config;
}
/**
* Get schema
*
@@ -124,7 +130,7 @@ class Converter
protected function getMaxIndexLength()
{
if (!isset($this->maxIndexLength)) {
$this->maxIndexLength = $this->getDatabaseSchema()->getMaxIndexLength();
$this->maxIndexLength = $this->getDatabaseSchema()->getDatabaseHelper()->getMaxIndexLength();
}
return $this->maxIndexLength;
@@ -238,8 +244,10 @@ class Converter
$tables[$entityName]->setPrimaryKey($primaryColumns);
if (!empty($indexList[$entityName])) {
foreach($indexList[$entityName] as $indexName => $indexColumnList) {
$tables[$entityName]->addIndex($indexColumnList, $indexName);
foreach($indexList[$entityName] as $indexName => $indexParams) {
$indexColumnList = $indexParams['columns'];
$indexFlagList = isset($indexParams['flags']) ? $indexParams['flags'] : array();
$tables[$entityName]->addIndex($indexColumnList, $indexName, $indexFlagList);
}
}

View File

@@ -48,12 +48,7 @@ class Schema
private $converter;
private $connection;
protected $drivers = array(
'mysqli' => '\Espo\Core\Utils\Database\DBAL\Driver\Mysqli\Driver',
'pdo_mysql' => '\Espo\Core\Utils\Database\DBAL\Driver\PDOMySql\Driver',
);
private $databaseHelper;
protected $fieldTypePaths = array(
'application/Espo/Core/Utils/Database/DBAL/FieldTypes',
@@ -79,8 +74,6 @@ class Schema
*/
protected $rebuildActionClasses = null;
public function __construct(\Espo\Core\Utils\Config $config, \Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager, \Espo\Core\ORM\EntityManager $entityManager, \Espo\Core\Utils\File\ClassParser $classParser, \Espo\Core\Utils\Metadata\OrmMetadata $ormMetadata)
{
$this->config = $config;
@@ -89,17 +82,17 @@ class Schema
$this->entityManager = $entityManager;
$this->classParser = $classParser;
$this->databaseHelper = new \Espo\Core\Utils\Database\Helper($this->config);
$this->comparator = new \Espo\Core\Utils\Database\DBAL\Schema\Comparator();
$this->initFieldTypes();
$this->converter = new \Espo\Core\Utils\Database\Converter($this->metadata, $this->fileManager);
$this->schemaConverter = new Converter($this->metadata, $this->fileManager, $this);
$this->converter = new \Espo\Core\Utils\Database\Converter($this->metadata, $this->fileManager, $this->config);
$this->schemaConverter = new Converter($this->metadata, $this->fileManager, $this, $this->config);
$this->ormMetadata = $ormMetadata;
}
protected function getConfig()
{
return $this->config;
@@ -140,26 +133,16 @@ class Schema
return $this->getConnection()->getDatabasePlatform();
}
public function getDatabaseHelper()
{
return $this->databaseHelper;
}
public function getConnection()
{
if (isset($this->connection)) {
return $this->connection;
}
$dbalConfig = new \Doctrine\DBAL\Configuration();
$connectionParams = $this->getConfig()->get('database');
$connectionParams['driverClass'] = $this->drivers[ $connectionParams['driver'] ];
unset($connectionParams['driver']);
$this->connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $dbalConfig);
return $this->connection;
return $this->getDatabaseHelper()->getDbalConnection();
}
protected function initFieldTypes()
{
foreach($this->fieldTypePaths as $path) {
@@ -187,8 +170,6 @@ class Schema
}
}
/*
* Rebuild database schema
*/
@@ -224,7 +205,6 @@ class Schema
return (bool) $result;
}
/*
* Get current database schema
*
@@ -248,7 +228,6 @@ class Schema
//return $schema->toSql($this->getPlatform()); //it can return with DROP TABLE
}
/*
* Get SQL queries to get from one to another schema
*
@@ -261,8 +240,6 @@ class Schema
return $this->toSql($schemaDiff); //$schemaDiff->toSql($this->getPlatform());
}
/**
* Init Rebuild Actions, get all classes and create them
* @return void
@@ -311,46 +288,4 @@ class Schema
}
}
}
public function getMaxIndexLength()
{
$connection = $this->getConnection();
$mysqlEngine = $this->getMysqlEngine();
switch ($mysqlEngine) {
case 'InnoDB':
$mysqlVersion = $this->getMysqlVersion();
if (version_compare($mysqlVersion, '10.0.0') >= 0) {
return 767; //InnoDB, MariaDB
}
if (version_compare($mysqlVersion, '5.7.0') >= 0) {
return 3072; //InnoDB, MySQL 5.7+
}
return 767; //InnoDB
break;
}
return 1000; //MyISAM
}
protected function getMysqlVersion()
{
$connection = $this->getConnection();
return $connection->fetchColumn("select version()");
}
protected function getMysqlEngine()
{
$connection = $this->getConnection();
$result = $connection->fetchColumn("SHOW TABLE STATUS WHERE Engine = 'MyISAM'");
if (!empty($result)) {
return 'MyISAM';
}
return 'InnoDB';
}
}

View File

@@ -33,7 +33,7 @@ use Espo\Core\Utils\Util;
class Utils
{
public static function getIndexList(array $ormMeta)
public static function getIndexList(array $ormMeta, array $ignoreFlags = [])
{
$indexList = array();
@@ -53,19 +53,37 @@ class Utils
if ($keyValue === true) {
$tableIndexName = static::generateIndexName($columnName);
$indexList[$entityName][$tableIndexName] = array($columnName);
$indexList[$entityName][$tableIndexName]['columns'] = array($columnName);
} else if (is_string($keyValue)) {
$tableIndexName = static::generateIndexName($keyValue);
$indexList[$entityName][$tableIndexName][] = $columnName;
$indexList[$entityName][$tableIndexName]['columns'][] = $columnName;
}
}
}
if (isset($entityParams['indexes']) && is_array($entityParams['indexes'])) {
foreach ($entityParams['indexes'] as $indexName => $indexParams) {
$tableIndexName = static::generateIndexName($indexName);
if (isset($indexParams['flags']) && is_array($indexParams['flags'])) {
$skipIndex = false;
foreach ($ignoreFlags as $ignoreFlag) {
if (($flagKey = array_search($ignoreFlag, $indexParams['flags'])) !== false) {
unset($indexParams['flags'][$flagKey]);
$skipIndex = true;
}
}
if ($skipIndex && empty($indexParams['flags'])) {
continue;
}
$indexList[$entityName][$tableIndexName]['flags'] = $indexParams['flags'];
}
if (is_array($indexParams['columns'])) {
$tableIndexName = static::generateIndexName($indexName);
$indexList[$entityName][$tableIndexName] = Util::toUnderScore($indexParams['columns']);
$indexList[$entityName][$tableIndexName]['columns'] = Util::toUnderScore($indexParams['columns']);
}
}
}
@@ -83,7 +101,7 @@ class Utils
return substr(implode('_', $nameList), 0, $maxLength);
}
public static function getFieldListExceededIndexMaxLength(array $ormMeta, $indexMaxLength = 1000, $characterLength = 4)
public static function getFieldListExceededIndexMaxLength(array $ormMeta, $indexMaxLength = 1000, array $indexList = null, $characterLength = 4)
{
$permittedFieldTypeList = [
'varchar',
@@ -91,10 +109,14 @@ class Utils
$fields = array();
$indexList = static::getIndexList($ormMeta);
if (!isset($indexList)) {
$indexList = static::getIndexList($ormMeta, ['fulltext']);
}
foreach ($indexList as $entityName => $indexes) {
foreach ($indexes as $indexName => $columnList) {
foreach ($indexes as $indexName => $indexParams) {
$columnList = $indexParams['columns'];
$indexLength = 0;
foreach ($columnList as $columnName) {
$fieldName = Util::toCamelCase($columnName);

View File

@@ -0,0 +1,80 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\Schema\rebuildActions;
class FulltextIndex extends \Espo\Core\Utils\Database\Schema\BaseRebuildActions
{
public function beforeRebuild()
{
$currentSchema = $this->getCurrentSchema();
$tables = $currentSchema->getTables();
if (empty($tables)) return;
$databaseHelper = new \Espo\Core\Utils\Database\Helper($this->getConfig());
$connection = $databaseHelper->getDbalConnection();
$metadataSchema = $this->getMetadataSchema();
$tables = $metadataSchema->getTables();
foreach ($tables as $table) {
$tableName = $table->getName();
$indexes = $table->getIndexes();
foreach ($indexes as $index) {
if (!$index->hasFlag('fulltext')) {
continue;
}
$columns = $index->getColumns();
foreach ($columns as $columnName) {
$query = "SHOW FULL COLUMNS FROM `". $tableName ."` WHERE Field = '" . $columnName . "'";
try {
$row = $connection->fetchAssoc($query);
} catch (\Exception $e) {
continue;
}
switch (strtoupper($row['Type'])) {
case 'LONGTEXT':
$alterQuery = "ALTER TABLE `". $tableName ."` MODIFY `". $columnName ."` MEDIUMTEXT COLLATE ". $row['Collation'] ."";
$GLOBALS['log']->info('SCHEMA, Execute Query: ' . $alterQuery);
$connection->executeQuery($alterQuery);
break;
}
}
}
}
}
}

View File

@@ -128,6 +128,28 @@ class EntityManager
return false;
}
protected function checkRelationshipExists($name)
{
$name = ucfirst($name);
$scopeList = array_keys($this->getMetadata()->get(['scopes'], []));
foreach ($scopeList as $entityType) {
$relationsDefs = $this->getEntityManager()->getMetadata()->get($entityType, 'relations');
if (empty($relationsDefs)) continue;
foreach ($relationsDefs as $link => $item) {
if (empty($item['type'])) continue;
if (empty($item['relationName'])) continue;
if ($item['type'] === 'manyMany') {
if (ucfirst($item['relationName']) === $name) {
return true;
}
}
}
}
return false;
}
public function create($name, $type, $params = [], $replaceData = [])
{
$name = ucfirst($name);
@@ -172,6 +194,10 @@ class EntityManager
throw new Conflict('Entity name \''.$name.'\' is not allowed.');
}
if ($this->checkRelationshipExists($name)) {
throw new Conflict('Relationship with the same name \''.$name.'\' exists.');
}
$normalizedName = Util::normilizeClassName($name);
$templateNamespace = "\Espo\Core\Templates";
@@ -403,6 +429,16 @@ class EntityManager
$this->getMetadata()->set('entityDefs', $name, $entityDefsData);
}
if (isset($data['fullTextSearch'])) {
$entityDefsData = [
'collection' => [
'fullTextSearch' => !!$data['fullTextSearch']
]
];
$this->getMetadata()->set('entityDefs', $name, $entityDefsData);
}
if (array_key_exists('kanbanStatusIgnoreList', $data)) {
$scopeData['kanbanStatusIgnoreList'] = $data['kanbanStatusIgnoreList'];
$this->getMetadata()->set('scopes', $name, $scopeData);
@@ -546,6 +582,12 @@ class EntityManager
} else {
$relationName = lcfirst($entity) . $entityForeign;
}
if ($this->getMetadata()->get(['scopes', ucfirst($relationName)])) {
throw new Conflict("Entity with the same name '{$relationName}' exists.");
}
if ($this->checkRelationshipExists($relationName)) {
throw new Conflict("Relationship with the same name '{$relationName}' exists.");
}
}
if (empty($link) || empty($linkForeign)) {

View File

@@ -66,7 +66,11 @@ class FieldManagerUtil
}
if ($naming == 'prefix') {
foreach ($list as $f) {
$fieldList[] = $f . ucfirst($name);
if ($f === '') {
$fieldList[] = $name;
} else {
$fieldList[] = $f . ucfirst($name);
}
}
} else {
foreach ($list as $f) {

View File

@@ -358,6 +358,14 @@ class Metadata
*/
public function saveCustom($key1, $key2, $data)
{
if (is_object($data)) {
foreach ($data as $key => $item) {
if ($item == new \stdClass()) {
unset($data->$key);
}
}
}
$filePath = array($this->paths['customPath'], $key1, $key2.'.json');
$changedData = Json::encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
@@ -380,6 +388,14 @@ class Metadata
*/
public function set($key1, $key2, $data)
{
if (is_array($data)) {
foreach ($data as $key => $item) {
if (is_array($item) && empty($item)) {
unset($data[$key]);
}
}
}
$newData = array(
$key1 => array(
$key2 => $data,

View File

@@ -41,19 +41,28 @@ class OrmMetadata
protected $fileManager;
protected $config;
protected $useCache;
public function __construct($metadata, $fileManager, $useCache = false)
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager, $config)
{
$this->metadata = $metadata;
$this->fileManager = $fileManager;
$this->useCache = $useCache;
$this->useCache = false;
if ($config instanceof \Espo\Core\Utils\Config) {
$this->config = $config;
$this->useCache = $this->config->get('useCache', false);
} elseif (is_bool($config)) {
$this->useCache = $config;
}
}
protected function getConverter()
{
if (!isset($this->converter)) {
$this->converter = new \Espo\Core\Utils\Database\Converter($this->metadata, $this->fileManager);
$this->converter = new \Espo\Core\Utils\Database\Converter($this->metadata, $this->fileManager, $this->config);
}
return $this->converter;
@@ -64,6 +73,11 @@ class OrmMetadata
return $this->fileManager;
}
protected function getConfig()
{
return $this->config;
}
public function clearData()
{
$this->ormData = null;

View File

@@ -86,10 +86,12 @@ class Util
return $name;
}
if($capitaliseFirstChar) {
$name[0] = strtoupper($name[0]);
$name = lcfirst($name);
if ($capitaliseFirstChar) {
$name = ucfirst($name);
}
return preg_replace_callback('/'.$symbol.'([a-z])/', 'static::toCamelCaseConversion', $name);
return preg_replace_callback('/'.$symbol.'([a-zA-Z])/', 'static::toCamelCaseConversion', $name);
}
protected static function toCamelCaseConversion($matches)

View File

@@ -111,6 +111,7 @@ return array (
'adminNotifications' => true,
'adminNotificationsNewVersion' => true,
'adminNotificationsCronIsNotConfigured' => true,
'adminNotificationsNewExtensionVersion' => true,
'assignmentEmailNotifications' => false,
'assignmentEmailNotificationsEntityList' => ['Lead', 'Opportunity', 'Task', 'Case'],
'assignmentNotificationsEntityList' => ['Meeting', 'Call', 'Task', 'Email'],
@@ -170,6 +171,9 @@ return array (
'inlineAttachmentUploadMaxSize' => 20,
'textFilterUseContainsForVarchar' => false,
'tabColorsDisabled' => false,
'massPrintPdfMaxCount' => 50,
'emailKeepParentTeamsEntityList' => ['Case'],
'recordListMaxSizeLimit' => 200,
'isInstalled' => false
);

View File

@@ -154,7 +154,11 @@ return array ( 'defaultPermissions' =>
'ldapUserTeamsIds',
'ldapUserTeamsNames',
'cleanupJobPeriod',
'cleanupActionHistoryPeriod'
'cleanupActionHistoryPeriod',
'adminNotifications',
'adminNotificationsNewVersion',
'adminNotificationsCronIsNotConfigured',
'adminNotificationsNewExtensionVersion'
),
'userItems' =>
array (

View File

@@ -76,8 +76,8 @@ class CurrencyConverted extends \Espo\Core\Hooks\Base
$targetValue = $value;
} else {
$targetValue = $value;
$targetValue = $targetValue * (isset($rates[$defaultCurrency]) ? $rates[$defaultCurrency] : 1.0);
$targetValue = $targetValue / (isset($rates[$currency]) ? $rates[$currency] : 1.0);
$targetValue = $targetValue / (isset($rates[$baseCurrency]) ? $rates[$baseCurrency] : 1.0);
$targetValue = $targetValue * (isset($rates[$currency]) ? $rates[$currency] : 1.0);
$targetValue = round($targetValue, 2);
}
$entity->set($fieldName, $targetValue);

View File

@@ -0,0 +1,54 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Jobs;
use Espo\Core\Exceptions;
class CheckNewExtensionVersion extends CheckNewVersion
{
public function run()
{
if (!$this->getConfig()->get('adminNotifications') || !$this->getConfig()->get('adminNotificationsNewExtensionVersion')) {
return true;
}
$job = $this->getEntityManager()->getEntity('Job');
$job->set(array(
'name' => 'Check for new versions of installed extensions (job)',
'serviceName' => 'AdminNotifications',
'methodName' => 'jobCheckNewExtensionVersion',
'executeTime' => $this->getRunTime()
));
$this->getEntityManager()->saveEntity($job);
return true;
}
}

View File

@@ -191,7 +191,7 @@ class Cleanup extends \Espo\Core\Jobs\Base
$collection = $this->getEntityManager()->getRepository('Attachment')->where(array(
'OR' => array(
array(
'role' => ['Export File']
'role' => ['Export File', 'Mail Merge', 'Mass Pdf']
)
),
'createdAt<' => $datetime->format('Y-m-d H:i:s')

View File

@@ -37,7 +37,7 @@ class Activities extends \Espo\Core\Controllers\Base
{
protected $maxCalendarRange = 123;
protected $maxSizeLimit = 200;
const MAX_SIZE_LIMIT = 200;
public function actionListCalendarEvents($params, $data, $request)
{
@@ -113,11 +113,12 @@ class Activities extends \Espo\Core\Controllers\Base
$futureDays = intval($request->get('futureDays'));
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = $this->maxSizeLimit;
$maxSize = $maxSizeLimit;
}
if ($maxSize > $this->maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $this->maxSizeLimit . ". Use pagination (offset, limit).");
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
return $service->getUpcomingActivities($userId, array(
@@ -175,11 +176,12 @@ class Activities extends \Espo\Core\Controllers\Base
$sortBy = $request->get('sortBy');
$where = $request->get('where');
$maxSizeLimit = $this->getConfig()->get('recordListMaxSizeLimit', self::MAX_SIZE_LIMIT);
if (empty($maxSize)) {
$maxSize = $this->maxSizeLimit;
$maxSize = $maxSizeLimit;
}
if ($maxSize > $this->maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $this->maxSizeLimit . ". Use pagination (offset, limit).");
if (!empty($maxSize) && $maxSize > $maxSizeLimit) {
throw new Forbidden("Max should should not exceed " . $maxSizeLimit . ". Use offset and limit.");
}
$scope = null;

View File

@@ -29,7 +29,27 @@
namespace Espo\Modules\Crm\Controllers;
use \Espo\Core\Exceptions\Error,
\Espo\Core\Exceptions\Forbidden,
\Espo\Core\Exceptions\BadRequest;
class Campaign extends \Espo\Core\Controllers\Record
{
public function postActionGenerateMailMergePdf($params, $data, $request)
{
if (empty($data->campaignId)) {
throw new BadRequest();
}
if (empty($data->link)) {
throw new BadRequest();
}
if (!$this->getAcl()->checkScope('Campaign', 'read')) {
throw new Forbidden();
}
return [
'id' => $this->getRecordService()->generateMailMergePdf($data->campaignId, $data->link, true)
];
}
}

View File

@@ -87,8 +87,9 @@ class Opportunity extends \Espo\Core\Controllers\Record
$dateFrom = $request->get('dateFrom');
$dateTo = $request->get('dateTo');
$dateFilter = $request->get('dateFilter');
$useLastStage = $request->get('useLastStage') === 'true';
return $this->getService('Opportunity')->reportSalesPipeline($dateFilter, $dateFrom, $dateTo);
return $this->getService('Opportunity')->reportSalesPipeline($dateFilter, $dateFrom, $dateTo, $useLastStage);
}
public function postActionMassConvertCurrency($params, $data, $request)

View File

@@ -63,6 +63,9 @@ class TargetList extends \Espo\Core\Controllers\Record
if (empty($data->targetId)) {
throw new BadRequest();
}
$data->id = strval($data->id);
$data->targetId = strval($data->targetId);
return $this->getRecordService()->optOut($data->id, $data->targetType, $data->targetId);
}
@@ -77,6 +80,9 @@ class TargetList extends \Espo\Core\Controllers\Record
if (empty($data->targetId)) {
throw new BadRequest();
}
$data->id = strval($data->id);
$data->targetId = strval($data->targetId);
return $this->getRecordService()->cancelOptOut($data->id, $data->targetType, $data->targetId);
}
}

View File

@@ -116,6 +116,8 @@ class EventConfirmation extends \Espo\Core\EntryPoints\Base
";
$this->getClientManager()->display($runScript);
return;
}
throw new Error();

View File

@@ -56,7 +56,7 @@ class CaseObj extends \Espo\Core\ORM\Repositories\RDB
if ($entity->getFetched('contactId')) {
$previousPortalUser = $this->getEntityManager()->getRepository('User')->where(array(
'contactId' => $entity->getFetched('contactId'),
'isPortal' => true
'isPortalUser' => true
))->findOne();
if ($previousPortalUser) {
$this->getInjection('serviceFactory')->create('Stream')->unfollowEntity($entity, $previousPortalUser->id);
@@ -70,9 +70,10 @@ class CaseObj extends \Espo\Core\ORM\Repositories\RDB
$portalUser = $this->getEntityManager()->getRepository('User')->where(array(
'contactId' => $contactId,
'isPortal' => true,
'isPortalUser' => true,
'isActive' => true
))->findOne();
if ($portalUser) {
$this->getInjection('serviceFactory')->create('Stream')->followEntity($entity, $portalUser->id);
}
@@ -97,4 +98,3 @@ class CaseObj extends \Espo\Core\ORM\Repositories\RDB
}
}
}

View File

@@ -41,5 +41,19 @@ class Lead extends \Espo\Core\ORM\Repositories\RDB
$this->relate($entity, 'targetLists', $entity->get('targetListId'));
}
}
}
public function handleSelectParams(&$params)
{
parent::handleSelectParams($params);
if (array_key_exists('select', $params)) {
if (in_array('name', $params['select'])) {
$additionalAttributeList = ['emailAddress', 'phoneNumber', 'accountName'];
foreach ($additionalAttributeList as $attribute) {
if (!in_array($attribute, $params['select'])) {
$params['select'][] = $attribute;
}
}
}
}
}
}

View File

@@ -42,6 +42,53 @@ class Opportunity extends \Espo\Core\ORM\Repositories\RDB
}
}
if (!$entity->isAttributeChanged('lastStage') && $entity->isAttributeChanged('stage')) {
$probability = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap', $entity->get('stage')], 0);
$probabilityMap = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap'], []);
if (!$probability) {
$stageList = $this->getMetadata()->get('entityDefs.Opportunity.fields.stage.options', []);
if ($entity->isNew()) {
if (count($stageList)) {
$min = 100;
$minStage = null;
foreach ($stageList as $stage) {
if (!empty($probabilityMap[$stage]) && $probabilityMap[$stage] !== 100) {
if ($probabilityMap[$stage] < $min) {
$min = $probabilityMap[$stage];
$minStage = $stage;
}
}
}
if ($minStage) {
$entity->set('lastStage', $minStage);
}
}
} else {
$lastStageProbability = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap', $entity->get('lastStage')], 0);
if ($lastStageProbability === 100) {
if (count($stageList)) {
$max = 0;
$maxStage = null;
foreach ($stageList as $stage) {
if (!empty($probabilityMap[$stage]) && $probabilityMap[$stage] !== 100) {
if ($probabilityMap[$stage] > $max) {
$max = $probabilityMap[$stage];
$maxStage = $stage;
}
}
}
if ($maxStage) {
$entity->set('lastStage', $maxStage);
}
}
}
}
} else {
$entity->set('lastStage', $entity->get('stage'));
}
}
parent::beforeSave($entity, $options);
}

View File

@@ -19,7 +19,12 @@
"revenue": "Revenue",
"revenueConverted": "Revenue (converted)",
"budget": "Budget",
"budgetConverted": "Budget (converted)"
"budgetConverted": "Budget (converted)",
"contactsTemplate": "Contacts Template",
"leadsTemplate": "Leads Template",
"accountsTemplate": "Accounts Template",
"usersTemplate": "Users Template",
"mailMergeOnlyWithAddress": "Skip records w/o filled address"
},
"links": {
"targetLists": "Target Lists",
@@ -30,7 +35,11 @@
"opportunities": "Opportunities",
"campaignLogRecords": "Log",
"massEmails": "Mass Emails",
"trackingUrls": "Tracking URLs"
"trackingUrls": "Tracking URLs",
"contactsTemplate": "Contacts Template",
"leadsTemplate": "Leads Template",
"accountsTemplate": "Accounts Template",
"usersTemplate": "Users Template"
},
"options": {
"type": {
@@ -59,7 +68,9 @@
"Email Templates": "Email Templates",
"Unsubscribe again": "Unsubscribe again",
"Subscribe again": "Subscribe again",
"Create Target List": "Create Target List"
"Create Target List": "Create Target List",
"Mail Merge": "Mail Merge",
"Generate Mail Merge PDF": "Generate Mail Merge PDF"
},
"presetFilters": {
"active": "Active"

View File

@@ -1,5 +1,6 @@
{
"fields": {
"futureDays": "Next X Days"
"futureDays": "Next X Days",
"useLastStage": "Group by last reached stage"
}
}

View File

@@ -15,7 +15,8 @@
"campaign": "Campaign",
"originalLead": "Original Lead",
"amountCurrency": "Amount Currency",
"contactRole": "Contact Role"
"contactRole": "Contact Role",
"lastStage": "Last Stage"
},
"links": {
"contacts": "Contacts",

View File

@@ -3,11 +3,14 @@
"name": "Name",
"description": "Description",
"entryCount": "Entry Count",
"optedOutCount": "Opted Out Count",
"campaigns": "Campaigns",
"endDate": "End Date",
"targetLists": "Target Lists",
"includingActionList": "Including",
"excludingActionList": "Excluding"
"excludingActionList": "Excluding",
"targetStatus": "Target Status",
"isOptedOut": "Is Opted Out"
},
"links": {
"accounts": "Accounts",
@@ -23,6 +26,10 @@
"Television": "Television",
"Radio": "Radio",
"Newsletter": "Newsletter"
},
"targetStatus": {
"Opted Out": "Opted Out",
"Listed": "Listed"
}
},
"labels": {

View File

@@ -1,6 +1,6 @@
[
{
"label":"Overview",
"label": "Overview",
"rows":[
[{"name":"name"}, {"name":"status"}],
[{"name":"type"}, {"name":"startDate"}],
@@ -8,5 +8,13 @@
[{"name":"targetLists"}, {"name":"excludingTargetLists"}],
[{"name":"description", "fullWidth": true}]
]
},
{
"label": "Mail Merge",
"name": "mailMerge",
"rows":[
[{"name":"contactsTemplate"}, {"name":"leadsTemplate"}],
[{"name":"accountsTemplate"}, {"name":"mailMergeOnlyWithAddress"}]
]
}
]

View File

@@ -3,6 +3,7 @@
"label":"Overview",
"rows":[
[{"name":"name"}, {"name":"entryCount"}],
[false, {"name":"optedOutCount"}],
[{"name":"description", "fullWidth": true}]
]
}

View File

@@ -4,6 +4,7 @@
"rows": [
[{"name":"name", "fullWidth": true}],
[{"name":"entryCount", "fullWidth": true}],
[{"name":"optedOutCount", "fullWidth": true}],
[{"name":"description", "fullWidth": true}]
]
}

View File

@@ -1,4 +1,4 @@
[
{"name":"name", "link":true},
{"name":"entryCount", "width": 25, "notSortable": true}
{"name":"targetStatus", "width": 25, "notSortable": true}
]

View File

@@ -1,6 +1,6 @@
[
"contacts",
"leads",
"users",
"accounts"
"accounts",
"users"
]

View File

@@ -61,7 +61,9 @@
"create": false
},
"targetLists": {
"layout": "listForTarget"
"rowActionsView": "crm:views/record/row-actions/relationship-target",
"layout": "listForTarget",
"view": "crm:views/record/panels/target-lists"
}
},
"filterList": [

View File

@@ -56,36 +56,130 @@
"filterList": [
"active"
],
"formDependency": {
"type": {
"map": {
"Email": [
{
"action": "show",
"fields": ["targetLists", "excludingTargetLists"]
}
],
"Newsletter": [
{
"action": "show",
"fields": ["targetLists", "excludingTargetLists"]
}
],
"Mail": [
{
"action": "show",
"fields": ["targetLists", "excludingTargetLists"]
}
]
},
"default": [
{
"action": "hide",
"fields": ["targetLists", "excludingTargetLists"]
"dynamicLogic": {
"fields": {
"targetLists": {
"visible": {
"conditionGroup": [
{
"type": "or",
"value": [
{
"type": "equals",
"attribute": "type",
"value": "Email"
},
{
"type": "equals",
"attribute": "type",
"value": "Newsletter"
},
{
"type": "equals",
"attribute": "type",
"value": "Mail"
}
]
}
]
}
},
"excludingTargetLists": {
"visible": {
"conditionGroup": [
{
"type": "or",
"value": [
{
"type": "equals",
"attribute": "type",
"value": "Email"
},
{
"type": "equals",
"attribute": "type",
"value": "Newsletter"
},
{
"type": "equals",
"attribute": "type",
"value": "Mail"
}
]
}
]
}
},
"contactsTemplate": {
"visible": {
"conditionGroup": [
{
"type": "equals",
"attribute": "type",
"value": "Mail"
}
]
}
},
"leadsTemplate": {
"visible": {
"conditionGroup": [
{
"type": "equals",
"attribute": "type",
"value": "Mail"
}
]
}
},
"accountsTemplate": {
"visible": {
"conditionGroup": [
{
"type": "equals",
"attribute": "type",
"value": "Mail"
}
]
}
},
"usersTemplate": {
"visible": {
"conditionGroup": [
{
"type": "equals",
"attribute": "type",
"value": "Mail"
}
]
}
},
"mailMergeOnlyWithAddress": {
"visible": {
"conditionGroup": [
{
"type": "equals",
"attribute": "type",
"value": "Mail"
}
]
}
}
]
}
},
},
"panels": {
"mailMerge": {
"visible": {
"conditionGroup": [
{
"type": "equals",
"attribute": "type",
"value": "Mail"
}
]
}
}
}
},
"boolFilterList": ["onlyMy"],
"iconClass": "fas fa-chart-line"
}

View File

@@ -85,8 +85,9 @@
},
"targetLists": {
"create": false,
"rowActionsView": "views/record/row-actions/relationship-unlink-only",
"layout": "listForTarget"
"rowActionsView": "crm:views/record/row-actions/relationship-target",
"layout": "listForTarget",
"view": "crm:views/record/panels/target-lists"
}
},
"boolFilterList": [

View File

@@ -6,28 +6,6 @@
"recordViews":{
"detail":"crm:views/lead/record/detail"
},
"formDependency":{
"status":{
"map":{
"Converted":[
{
"action":"show",
"panels":[
"convertedTo"
]
}
]
},
"default":[
{
"action":"hide",
"panels":[
"convertedTo"
]
}
]
}
},
"sidePanels":{
"detail":[
{
@@ -112,14 +90,15 @@
},
"relationshipPanels":{
"campaignLogRecords":{
"rowActionsView":"Record.RowActions.Empty",
"rowActionsView":"views/record/row-actions/empty",
"select":false,
"create":false
},
"targetLists":{
"create":false,
"rowActionsView":"views/record/row-actions/relationship-unlink-only",
"layout": "listForTarget"
"create": false,
"rowActionsView": "crm:views/record/row-actions/relationship-target",
"layout": "listForTarget",
"view": "crm:views/record/panels/target-lists"
}
},
"filterList":[
@@ -154,6 +133,19 @@
]
}
}
},
"panels": {
"convertedTo": {
"visible": {
"conditionGroup": [
{
"type": "equals",
"attribute": "status",
"value": "Converted"
}
]
}
}
}
},
"color": "#d6a2c9",

View File

@@ -73,6 +73,21 @@
}
},
"kanbanViewMode": true,
"dynamicLogic": {
"fields": {
"lastStage": {
"visible": {
"conditionGroup": [
{
"type": "equals",
"attribute": "stage",
"value": "Closed Lost"
}
]
}
}
}
},
"color": "#9fc77e",
"iconClass": "fas fa-dollar-sign"
}

View File

@@ -25,6 +25,7 @@
"sortBy": "dateStart",
"asc": true,
"displayRecords": 5,
"populateAssignedUser": true,
"expandedLayout": {
"rows": [
[

View File

@@ -25,6 +25,7 @@
"sortBy": "createdAt",
"asc": false,
"displayRecords": 5,
"populateAssignedUser": true,
"expandedLayout": {
"rows": [
[

View File

@@ -25,6 +25,7 @@
"sortBy": "createdAt",
"asc": false,
"displayRecords": 5,
"populateAssignedUser": true,
"expandedLayout": {
"rows": [
[

View File

@@ -25,6 +25,7 @@
"sortBy": "dateStart",
"asc": true,
"displayRecords": 5,
"populateAssignedUser": true,
"expandedLayout": {
"rows": [
[

View File

@@ -25,6 +25,7 @@
"sortBy": "closeDate",
"asc": true,
"displayRecords": 5,
"populateAssignedUser": true,
"expandedLayout": {
"rows": [
[

View File

@@ -21,6 +21,9 @@
"options": ["currentYear", "currentQuarter", "currentMonth", "ever", "between"],
"default": "currentYear",
"translation": "Global.options.dateSearchRanges"
},
"useLastStage": {
"type": "bool"
}
},
"layout": [
@@ -31,7 +34,7 @@
],
[
{"name": "dateFilter"},
false
{"name": "useLastStage"}
],
[
{"name": "dateFrom"},

View File

@@ -206,6 +206,12 @@
"layoutListDisabled": true,
"readOnly": true,
"view": "views/fields/link-one"
},
"targetListIsOptedOut": {
"type": "bool",
"notStorable": true,
"readOnly": true,
"disabled": true
}
},
"links": {

View File

@@ -142,6 +142,30 @@
},
"budget": {
"type": "currency"
},
"contactsTemplate": {
"type": "link",
"view": "crm:views/campaign/fields/template",
"targetEntityType": "Contact"
},
"leadsTemplate": {
"type": "link",
"view": "crm:views/campaign/fields/template",
"targetEntityType": "Lead"
},
"accountsTemplate": {
"type": "link",
"view": "crm:views/campaign/fields/template",
"targetEntityType": "Account"
},
"usersTemplate": {
"type": "link",
"view": "crm:views/campaign/fields/template",
"targetEntityType": "User"
},
"mailMergeOnlyWithAddress": {
"type": "bool",
"default": true
}
},
"links": {
@@ -209,6 +233,26 @@
"entity": "MassEmail",
"foreign": "campaign",
"layoutRelationshipsDisabled": true
},
"contactsTemplate": {
"type": "belongsTo",
"entity": "Template",
"noJoin": true
},
"leadsTemplate": {
"type": "belongsTo",
"entity": "Template",
"noJoin": true
},
"accountsTemplate": {
"type": "belongsTo",
"entity": "Template",
"noJoin": true
},
"usersTemplate": {
"type": "belongsTo",
"entity": "Template",
"noJoin": true
}
},
"collection": {

View File

@@ -82,8 +82,7 @@
"view": "views/fields/teams"
},
"attachments": {
"type": "attachmentMultiple",
"layoutListDisabled": true
"type": "attachmentMultiple"
}
},
"links": {
@@ -166,7 +165,8 @@
},
"collection": {
"sortBy": "number",
"asc": false
"asc": false,
"textFilterFields": ["name", "number"]
},
"indexes": {
"status": {

View File

@@ -66,7 +66,8 @@
"distinct": true
}
},
"trim": true
"trim": true,
"textFilterDisabled": true
},
"description": {
"type": "text"
@@ -130,7 +131,8 @@
"layoutFiltersDisabled": true,
"exportDisabled": true,
"importDisabled": true,
"view": "crm:views/contact/fields/account-role"
"view": "crm:views/contact/fields/account-role",
"textFilterDisabled": true
},
"accountIsInactive": {
"type": "bool",
@@ -289,6 +291,12 @@
"layoutListDisabled": true,
"readOnly": true,
"view": "views/fields/link-one"
},
"targetListIsOptedOut": {
"type": "bool",
"notStorable": true,
"readOnly": true,
"disabled": true
}
},
"links": {

View File

@@ -34,7 +34,8 @@
},
"order": {
"type": "int",
"disableFormatting": true
"disableFormatting": true,
"textFilterDisabled": true
},
"description": {
"type": "text"

View File

@@ -27,7 +27,8 @@
"order": {
"type": "int",
"required": true,
"disableFormatting": true
"disableFormatting": true,
"textFilterDisabled": true
},
"teams": {
"type": "linkMultiple",

View File

@@ -194,6 +194,12 @@
"layoutMassUpdateDisabled": true,
"layoutFiltersDisabled": true,
"entity": "TargetList"
},
"targetListIsOptedOut": {
"type": "bool",
"notStorable": true,
"readOnly": true,
"disabled": true
}
},
"links": {

View File

@@ -62,6 +62,12 @@
}
]
},
"lastStage": {
"type": "enum",
"view": "crm:views/opportunity/fields/last-stage",
"customizationOptionsDisabled": true,
"customizationDefaultDisabled": true
},
"probability": {
"type": "int",
"required": true,
@@ -247,6 +253,9 @@
"stage": {
"columns": ["stage", "deleted"]
},
"lastStage": {
"columns": ["lastStage"]
},
"assignedUser": {
"columns": ["assignedUserId", "deleted"]
},

View File

@@ -8,7 +8,17 @@
"entryCount": {
"type": "int",
"readOnly": true,
"notStorable": true
"notStorable": true,
"layoutFiltersDisabled": true,
"layoutMassUpdateDisabled": true
},
"optedOutCount": {
"type": "int",
"readOnly": true,
"notStorable": true,
"layoutListDisabled": true,
"layoutFiltersDisabled": true,
"layoutMassUpdateDisabled": true
},
"description": {
"type": "text"
@@ -60,6 +70,28 @@
"layoutLinkDisabled": true,
"notStorable": true,
"disabled": true
},
"targetStatus": {
"type": "enum",
"options": ["Listed", "Opted Out"],
"notStorable": true,
"readOnly": true,
"layoutListDisabled": true,
"layoutDetailDisabled": true,
"layoutMassUpdateDisabled": true,
"exportDisabled": true,
"importDisabled": true,
"view": "crm:views/target-list/fields/target-status"
},
"isOptedOut": {
"type": "bool",
"notStorable": true,
"readOnly": true,
"layoutListDisabled": true,
"layoutDetailDisabled": true,
"layoutMassUpdateDisabled": true,
"exportDisabled": true,
"importDisabled": true
}
},
"links": {

View File

@@ -99,8 +99,7 @@
},
"attachments": {
"type": "attachmentMultiple",
"sourceList": ["Document"],
"layoutListDisabled": true
"sourceList": ["Document"]
}
},
"links": {

View File

@@ -4,6 +4,12 @@
"type": "hasMany",
"entity": "TargetList",
"foreign": "users"
},
"targetListIsOptedOut": {
"type": "bool",
"notStorable": true,
"readOnly": true,
"disabled": true
}
}
}

View File

@@ -34,23 +34,47 @@ class Opportunity extends \Espo\Core\SelectManagers\Base
protected function filterOpen(&$result)
{
$result['whereClause'][] = array(
'stage!=' => ['Closed Won', 'Closed Lost']
'stage!=' => array_merge($this->getWonStageList(), $this->getLostStageList())
);
}
protected function filterWon(&$result)
{
$result['whereClause'][] = array(
'stage=' => 'Closed Won'
'stage=' => $this->getWonStageList()
);
}
protected function filterLost(&$result)
{
$result['whereClause'][] = array(
'stage=' => 'Closed Lost'
'stage=' => $this->getLostStageList()
);
}
}
protected function getLostStageList()
{
$lostStageList = [];
$probabilityMap = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap'], []);
$stageList = $this->getMetadata()->get('entityDefs.Opportunity.fields.stage.options', []);
foreach ($stageList as $stage) {
if (empty($probabilityMap[$stage])) {
$lostStageList[] = $stage;
}
}
return $lostStageList;
}
protected function getWonStageList()
{
$wonStageList = [];
$probabilityMap = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap'], []);
$stageList = $this->getMetadata()->get('entityDefs.Opportunity.fields.stage.options', []);
foreach ($stageList as $stage) {
if (!empty($probabilityMap[$stage]) && $probabilityMap[$stage] == 100) {
$wonStageList[] = $stage;
}
}
return $wonStageList;
}
}

View File

@@ -39,6 +39,11 @@ class Account extends \Espo\Services\Record
'role' => 'accountRole',
'isInactive' => 'accountIsInactive'
)
),
'targetLists' => array(
'additionalColumns' => array(
'optedOut' => 'isOptedOut'
)
)
);

View File

@@ -114,7 +114,8 @@ class Activities extends \Espo\Core\Services\Base
'parentType',
'parentId',
'status',
'createdAt'
'createdAt',
['VALUE:', 'hasAttachment']
],
'leftJoins' => [['users', 'usersLeft']],
'whereClause' => array(
@@ -165,7 +166,8 @@ class Activities extends \Espo\Core\Services\Base
'parentType',
'parentId',
'status',
'createdAt'
'createdAt',
['VALUE:', 'hasAttachment']
],
'leftJoins' => [['users', 'usersLeft']],
'whereClause' => array(
@@ -223,7 +225,8 @@ class Activities extends \Espo\Core\Services\Base
'parentType',
'parentId',
'status',
'createdAt'
'createdAt',
'hasAttachment'
],
'leftJoins' => [['users', 'usersLeft']],
'whereClause' => array(
@@ -269,7 +272,8 @@ class Activities extends \Espo\Core\Services\Base
'parentType',
'parentId',
'status',
'createdAt'
'createdAt',
['VALUE:', 'hasAttachment']
],
'whereClause' => array(),
'customJoin' => ''
@@ -375,7 +379,8 @@ class Activities extends \Espo\Core\Services\Base
'parentType',
'parentId',
'status',
'createdAt'
'createdAt',
['VALUE:', 'hasAttachment']
],
'whereClause' => array()
);
@@ -480,7 +485,8 @@ class Activities extends \Espo\Core\Services\Base
'parentType',
'parentId',
'status',
'createdAt'
'createdAt',
'hasAttachment'
],
'whereClause' => array(),
'customJoin' => ''
@@ -627,15 +633,21 @@ class Activities extends \Espo\Core\Services\Base
$sth->execute();
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
$rowList = $sth->fetchAll(PDO::FETCH_ASSOC);
$list = array();
foreach ($rows as $row) {
$boolAttributeList = ['hasAttachment'];
$list = [];
foreach ($rowList as $row) {
foreach ($boolAttributeList as $attribute) {
if (!array_key_exists($attribute, $row)) continue;
$row[$attribute] = $row[$attribute] == '1' ? true : false;
}
$list[] = $row;
}
return array(
'list' => $rows,
'list' => $list,
'total' => $totalCount
);
}
@@ -1004,7 +1016,8 @@ class Activities extends \Espo\Core\Services\Base
($seed->hasAttribute('parentType') ? ['parentType', 'parentType'] : ['VALUE:', 'parentType']),
($seed->hasAttribute('parentId') ? ['parentId', 'parentId'] : ['VALUE:', 'parentId']),
'status',
'createdAt'
'createdAt',
['VALUE:', 'hasAttachment']
];
$selectParams = $selectManager->getEmptySelectParams();

View File

@@ -31,8 +31,25 @@ namespace Espo\Modules\Crm\Services;
use \Espo\ORM\Entity;
use \Espo\Core\Exceptions\Error,
\Espo\Core\Exceptions\Forbidden,
\Espo\Core\Exceptions\BadRequest;
class Campaign extends \Espo\Services\Record
{
protected function init()
{
parent::init();
$this->addDependency('container');
}
protected $entityTypeAddressFieldListMap = [
'Account' => ['billingAddress', 'shippingAddress'],
'Contact' => ['address'],
'Lead' => ['address'],
'User' => []
];
public function loadAdditionalFields(Entity $entity)
{
parent::loadAdditionalFields($entity);
@@ -47,8 +64,7 @@ class Campaign extends \Espo\Services\Record
$openedCount = $this->getEntityManager()->getRepository('CampaignLogRecord')->where(array(
'campaignId' => $entity->id,
'action' => 'Opened',
'isTest' => false,
'groupBy' => ['queueItemId']
'isTest' => false
))->count();
$entity->set('openedCount', $openedCount);
@@ -61,8 +77,7 @@ class Campaign extends \Espo\Services\Record
$clickedCount = $this->getEntityManager()->getRepository('CampaignLogRecord')->where(array(
'campaignId' => $entity->id,
'action' => 'Clicked',
'isTest' => false,
'groupBy' => ['queueItemId']
'isTest' => false
))->count();
$entity->set('clickedCount', $clickedCount);
@@ -75,8 +90,7 @@ class Campaign extends \Espo\Services\Record
$optedOutCount = $this->getEntityManager()->getRepository('CampaignLogRecord')->where(array(
'campaignId' => $entity->id,
'action' => 'Opted Out',
'isTest' => false,
'groupBy' => ['queueItemId']
'isTest' => false
))->count();
$entity->set('optedOutCount', $optedOutCount);
@@ -89,8 +103,7 @@ class Campaign extends \Espo\Services\Record
$bouncedCount = $this->getEntityManager()->getRepository('CampaignLogRecord')->where(array(
'campaignId' => $entity->id,
'action' => 'Bounced',
'isTest' => false,
'groupBy' => ['queueItemId']
'isTest' => false
))->count();
$entity->set('bouncedCount', $bouncedCount);
@@ -283,5 +296,105 @@ class Campaign extends \Espo\Services\Record
$this->getEntityManager()->saveEntity($logRecord);
}
}
public function generateMailMergePdf($campaignId, $link, $checkAcl = false)
{
$campaign = $this->getEntityManager()->getEntity('Campaign', $campaignId);
if ($checkAcl && !$this->getAcl()->check($campaign, 'read')) {
throw new Forbidden();
}
if ($checkAcl) {
$targetEntityType = $campaign->getRelationParam($link, 'entity');
if (!$this->getAcl()->check($targetEntityType, 'read')) {
throw new Forbidden("Could not mail merge campaign because access to target enity type is forbidden.");
}
}
if (!in_array($link, ['accounts', 'contacts', 'leads', 'users'])) {
throw new BadRequest();
}
if ($campaign->get('type') !== 'Mail') {
throw new Error("Could not mail merge campaign not of Mail type.");
}
if (
!$campaign->get($link . 'TemplateId')
) {
throw new Error("Could not mail merge campaign w/o specified template.");
}
$template = $this->getEntityManager()->getEntity('Template', $campaign->get($link . 'TemplateId'));
if (!$template) {
throw new Error("Template not found");
}
if ($template->get('entityType') !== $targetEntityType) {
throw new Error("Template is not of proper entity type.");
}
$campaign->loadLinkMultipleField('targetLists');
$campaign->loadLinkMultipleField('excludingTargetLists');
if (count($campaign->getLinkMultipleIdList('targetLists')) === 0) {
throw new Error("Could not mail merge campaign w/o any specified target list.");
}
$metTargetHash = [];
$targetEntityList = [];
$excludingTargetListList = $campaign->get('excludingTargetLists');
foreach ($excludingTargetListList as $excludingTargetList) {
foreach ($excludingTargetList->get($link) as $excludingTarget) {
$hashId = $excludingTarget->getEntityType() . '-'. $excludingTarget->id;
$metTargetHash[$hashId] = true;
}
}
$addressFieldList = $this->entityTypeAddressFieldListMap[$targetEntityType];
$targetListCollection = $campaign->get('targetLists');
foreach ($targetListCollection as $targetList) {
if (!$campaign->get($link . 'TemplateId')) continue;
$entityList = $targetList->get($link, [
'additionalColumnsConditions' => [
'optedOut' => false
]
]);
foreach ($entityList as $e) {
$hashId = $e->getEntityType() . '-'. $e->id;
if (!empty($metTargetHash[$hashId])) {
continue;
}
$metTargetHash[$hashId] = true;
if ($campaign->get('mailMergeOnlyWithAddress')) {
if (empty($addressFieldList)) continue;
$hasAddress = false;
foreach ($addressFieldList as $addressField) {
if ($e->get($addressField . 'Street') || $e->get($addressField . 'PostalCode')) {
$hasAddress = true;
break;
}
}
if (!$hasAddress) continue;
}
$targetEntityList[] = $e;
}
}
if (empty($targetEntityList)) {
throw new Error("No targets available for mail merge.");
}
$filename = $campaign->get('name') . ' - ' . $this->getDefaultLanguage()->translate($targetEntityType, 'scopeNamesPlural');
return $this->getServiceFactory()->create('Pdf')->generateMailMerge($targetEntityType, $targetEntityList, $template, $filename, $campaign->id);
}
protected function getDefaultLanguage()
{
return $this->getInjection('container')->get('defaultLanguage');
}
}

View File

@@ -0,0 +1,38 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Services;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\ORM\Entity;
class CampaignLogRecord extends \Espo\Services\Record
{
protected $forceSelectAllAttributes = true;
}

View File

@@ -34,6 +34,8 @@ use \Espo\ORM\Entity;
class CampaignTrackingUrl extends \Espo\Services\Record
{
protected $mandatorySelectAttributeList = ['campaignId'];
protected function beforeCreateEntity(Entity $entity, $data)
{
parent::beforeCreateEntity($entity, $data);

View File

@@ -43,6 +43,19 @@ class Contact extends \Espo\Core\Templates\Services\Person
'title'
];
protected $linkSelectParams = array(
'targetLists' => array(
'additionalColumns' => array(
'optedOut' => 'isOptedOut'
)
)
);
protected $mandatorySelectAttributeList = [
'accountId',
'accountName'
];
protected function afterCreateEntity(Entity $entity, $data)
{
if (!empty($data->emailId)) {

View File

@@ -43,6 +43,14 @@ class Lead extends \Espo\Core\Templates\Services\Person
$this->addDependency('container');
}
protected $linkSelectParams = array(
'targetLists' => array(
'additionalColumns' => array(
'optedOut' => 'isOptedOut'
)
)
);
protected function getFieldManager()
{
return $this->getInjection('container')->get('fieldManager');

View File

@@ -45,6 +45,8 @@ class MassEmail extends \Espo\Services\Record
private $emailTemplateService = null;
protected $mandatorySelectAttributeList = ['campaignId'];
protected function init()
{
parent::init();

View File

@@ -36,7 +36,12 @@ use \Espo\Core\Exceptions\Forbidden;
class Opportunity extends \Espo\Services\Record
{
public function reportSalesPipeline($dateFilter, $dateFrom = null, $dateTo = null)
protected $mandatorySelectAttributeList = [
'accountId',
'accountName'
];
public function reportSalesPipeline($dateFilter, $dateFrom = null, $dateTo = null, $useLastStage = false)
{
if (in_array('amount', $this->getAcl()->getScopeForbiddenAttributeList('Opportunity'))) {
throw new Forbidden();
@@ -46,19 +51,27 @@ class Opportunity extends \Espo\Services\Record
list($dateFrom, $dateTo) = $this->getDateRangeByFilter($dateFilter);
}
$lostStageList = $this->getLostStageList();
$pdo = $this->getEntityManager()->getPDO();
$options = $this->getMetadata()->get('entityDefs.Opportunity.fields.stage.options', []);
$selectManager = $this->getSelectManagerFactory()->create('Opportunity');
$stageField = 'stage';
if ($useLastStage) {
$stageField = 'lastStage';
}
$selectParams = [
'select' => ['stage', ['SUM:amountConverted', 'amount']],
'select' => [$stageField, ['SUM:amountConverted', 'amount']],
'whereClause' => [
'stage!=' => 'Closed Lost'
[$stageField . '!=' => $lostStageList],
[$stageField . '!=' => null]
],
'orderBy' => 'LIST:stage:' . implode(',', $options),
'groupBy' => ['stage']
'orderBy' => 'LIST:'.$stageField.':' . implode(',', $options),
'groupBy' => [$stageField]
];
if ($dateFilter !== 'ever') {
@@ -81,7 +94,7 @@ class Opportunity extends \Espo\Services\Record
$result = array();
foreach ($rows as $row) {
$result[$row['stage']] = floatval($row['amount']);
$result[$row[$stageField]] = floatval($row['amount']);
}
return $result;
@@ -109,7 +122,7 @@ class Opportunity extends \Espo\Services\Record
$selectParams = [
'select' => ['leadSource', ['SUM:amountWeightedConverted', 'amount']],
'whereClause' => [
'stage!=' => 'Closed Lost',
'stage!=' => $this->getLostStageList(),
['leadSource!=' => ''],
['leadSource!=' => null]
],
@@ -162,8 +175,12 @@ class Opportunity extends \Espo\Services\Record
$selectParams = [
'select' => ['stage', ['SUM:amountConverted', 'amount']],
'whereClause' => [
'stage!=' => 'Closed Lost',
'stage!=' => 'Closed Won'
[
'stage!=' => $this->getLostStageList()
],
[
'stage!=' => $this->getWonStageList()
]
],
'orderBy' => 'LIST:stage:' . implode(',', $options),
'groupBy' => ['stage']
@@ -176,7 +193,7 @@ class Opportunity extends \Espo\Services\Record
];
}
$stageIgnoreList = ['Closed Lost', 'Closed Won'];
$stageIgnoreList = array_merge($this->getLostStageList(), $this->getWonStageList());
$selectManager->applyAccess($selectParams);
@@ -215,7 +232,7 @@ class Opportunity extends \Espo\Services\Record
$selectParams = [
'select' => [['MONTH:closeDate', 'month'], ['SUM:amountConverted', 'amount']],
'whereClause' => [
'stage' => 'Closed Won'
'stage' => $this->getWonStageList()
],
'orderBy' => 1,
'groupBy' => ['MONTH:closeDate']
@@ -396,4 +413,30 @@ class Opportunity extends \Espo\Services\Record
'count' => $count
);
}
protected function getLostStageList()
{
$lostStageList = [];
$probabilityMap = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap'], []);
$stageList = $this->getMetadata()->get('entityDefs.Opportunity.fields.stage.options', []);
foreach ($stageList as $stage) {
if (empty($probabilityMap[$stage])) {
$lostStageList[] = $stage;
}
}
return $lostStageList;
}
protected function getWonStageList()
{
$wonStageList = [];
$probabilityMap = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap'], []);
$stageList = $this->getMetadata()->get('entityDefs.Opportunity.fields.stage.options', []);
foreach ($stageList as $stage) {
if (!empty($probabilityMap[$stage]) && $probabilityMap[$stage] == 100) {
$wonStageList[] = $stage;
}
}
return $wonStageList;
}
}

View File

@@ -48,6 +48,29 @@ class TargetList extends \Espo\Services\Record
'User' => 'users'
);
protected $linkSelectParams = [
'accounts' => [
'additionalColumns' => [
'optedOut' => 'targetListIsOptedOut'
]
],
'contacts' => [
'additionalColumns' => [
'optedOut' => 'targetListIsOptedOut'
]
],
'leads' => [
'additionalColumns' => [
'optedOut' => 'targetListIsOptedOut'
]
],
'users' => [
'additionalColumns' => [
'optedOut' => 'targetListIsOptedOut'
]
]
];
protected function init()
{
parent::init();
@@ -60,6 +83,7 @@ class TargetList extends \Espo\Services\Record
{
parent::loadAdditionalFields($entity);
$this->loadEntryCountField($entity);
$this->loadOptedOutCountField($entity);
}
public function loadAdditionalFieldsForList(Entity $entity)
@@ -78,6 +102,34 @@ class TargetList extends \Espo\Services\Record
$entity->set('entryCount', $count);
}
protected function loadOptedOutCountField(Entity $entity)
{
$count = 0;
$count += $this->getEntityManager()->getRepository('Contact')->join(['targetLists'])->where([
'targetListsMiddle.targetListId' => $entity->id,
'targetListsMiddle.optedOut' => 1
])->count();
$count += $this->getEntityManager()->getRepository('Lead')->join(['targetLists'])->where([
'targetListsMiddle.targetListId' => $entity->id,
'targetListsMiddle.optedOut' => 1
])->count();
$count += $this->getEntityManager()->getRepository('Account')->join(['targetLists'])->where([
'targetListsMiddle.targetListId' => $entity->id,
'targetListsMiddle.optedOut' => 1
])->count();
$count += $this->getEntityManager()->getRepository('User')->join(['targetLists'])->where([
'targetListsMiddle.targetListId' => $entity->id,
'targetListsMiddle.optedOut' => 1
])->count();
$entity->set('optedOutCount', $count);
}
protected function afterCreate(Entity $entity, array $data = array())
{
if (array_key_exists('sourceCampaignId', $data) && !empty($data['includingActionList'])) {

View File

@@ -28,6 +28,7 @@
************************************************************************/
namespace Espo\ORM\DB;
use Espo\ORM\Entity;
use Espo\ORM\IEntity;
use Espo\ORM\EntityFactory;

View File

@@ -107,6 +107,14 @@ abstract class Base
'LENGTH'
];
protected $matchFunctionList = ['MATCH_BOOLEAN', 'MATCH_NATURAL_LANGUAGE', 'MATCH_QUERY_EXPANSION'];
protected $matchFunctionMap = [
'MATCH_BOOLEAN' => 'IN BOOLEAN MODE',
'MATCH_NATURAL_LANGUAGE' => 'IN NATURAL LANGUAGE MODE',
'MATCH_QUERY_EXPANSION' => 'WITH QUERY EXPANSION'
];
protected $entityFactory;
protected $pdo;
@@ -308,6 +316,45 @@ abstract class Base
return $function . '(' . $part . ')';
}
protected function convertMatchExpression($entity, $expression)
{
$delimiterPosition = strpos($expression, ':');
if ($delimiterPosition === false) {
throw new \Exception("Bad MATCH usage.");
}
$function = substr($expression, 0, $delimiterPosition);
$rest = substr($expression, $delimiterPosition + 1);
if (empty($rest)) {
throw new \Exception("Empty MATCH parameters.");
}
$delimiterPosition = strpos($rest, ':');
if ($delimiterPosition === false) {
throw new \Exception("Bad MATCH usage.");
}
$columns = substr($rest, 0, $delimiterPosition);
$query = mb_substr($rest, $delimiterPosition + 1);
$columnList = explode(',', $columns);
$tableName = $this->toDb($entity->getEntityType());
foreach ($columnList as $i => $column) {
$columnList[$i] = $tableName . '.' . $this->sanitize($column);
}
$query = $this->quote($query);
if (!in_array($function, $this->matchFunctionList)) return;
$modePart = ' ' . $this->matchFunctionMap[$function];
$result = "MATCH (" . implode(',', $columnList) . ") AGAINST (" . $query . "" . $modePart . ")";
return $result;
}
protected function convertComplexExpression($entity, $field, $distinct = false)
{
@@ -317,7 +364,14 @@ abstract class Base
$entityType = $entity->getEntityType();
if (strpos($field, ':')) {
list($function, $field) = explode(':', $field);
$dilimeterPosition = strpos($field, ':');
$function = substr($field, 0, $dilimeterPosition);
if (in_array($function, $this->matchFunctionList)) {
return $this->convertMatchExpression($entity, $field);
}
$field = substr($field, $dilimeterPosition + 1);
}
if (!empty($function)) {
$function = preg_replace('/[^A-Za-z0-9_]+/', '', $function);
@@ -724,6 +778,13 @@ abstract class Base
foreach ($whereClause as $field => $value) {
if (is_int($field)) {
if (is_string($value)) {
if (strpos($value, 'MATCH_') === 0) {
$rightPart = $this->convertMatchExpression($entity, $value);
$whereParts[] = $rightPart;
continue;
}
}
$field = 'AND';
}
@@ -776,6 +837,7 @@ abstract class Base
if (empty($isComplex)) {
if (!isset($entity->fields[$field])) {
$whereParts[] = '0';
continue;
}
@@ -908,7 +970,7 @@ abstract class Base
}
} else {
$internalPart = $this->getWhere($entity, $value, $field, $params, $level + 1);
if ($internalPart) {
if ($internalPart || $internalPart === '0') {
$whereParts[] = "(" . $internalPart . ")";
}
}

View File

@@ -206,6 +206,11 @@ class EntityManager
return $this->metadata;
}
public function getOrmMetadata()
{
return $this->getMetadata();
}
public function getPDO()
{
if (empty($this->pdo)) {
@@ -231,6 +236,11 @@ class EntityManager
return $collection;
}
public function getEntityFactory()
{
return $this->entityFactory;
}
protected function init()
{
}

View File

@@ -192,10 +192,11 @@ class RDB extends \Espo\ORM\Repository
public function find(array $params = array())
{
$params = $this->getSelectParams($params);
if (empty($params['skipAdditionalSelectParams'])) {
$this->handleSelectParams($params);
}
$params = $this->getSelectParams($params);
$dataArr = $this->getMapper()->select($this->seed, $params);

View File

@@ -49,7 +49,11 @@ class Email extends \Espo\Core\ORM\Repositories\RDB
$address = $entity->get($type);
$idList = [];
if (!empty($address) || !filter_var($address, FILTER_VALIDATE_EMAIL)) {
if (
(!empty($address) || !filter_var($address, FILTER_VALIDATE_EMAIL))
&&
$type !== 'replyTo'
) {
$arr = array_map(function ($e) {
return trim($e);
}, explode(';', $address));

View File

@@ -162,7 +162,8 @@
"readOnly": "Read-only",
"maxFileSize": "Max File Size (Mb)",
"isPersonalData": "Is Personal Data",
"useIframe": "Use Iframe"
"useIframe": "Use Iframe",
"useNumericFormat": "Use Numeric Format"
},
"messages": {
"upgradeVersion": "EspoCRM will be upgraded to version <strong>{version}</strong>. Please be patient as this may take a while.",
@@ -177,9 +178,9 @@
"selectExtensionPackage": "Select extension package",
"extensionInstalled": "Extension {name} {version} has been installed.",
"installExtension": "Extension {name} {version} is ready for an installation.",
"cronIsNotConfigured": "Scheduled jobs are not running. Hence inbound emails, notifications and reminders are not working. Please follow the <a target=\"_blank\" href=\"https://www.espocrm.com/documentation/administration/server-configuration/#user-content-setup-a-crontab\">instructions</a> to setup cron job.",
"cronIsNotConfigured": "Scheduled jobs are not running. Hence inbound emails, notifications and reminders are not working. Please follow the [instructions](https://www.espocrm.com/documentation/administration/server-configuration/#user-content-setup-a-crontab) to setup cron job.",
"newVersionIsAvailable": "New EspoCRM version {latestVersion} is available.",
"newExtensionVersionIsAvailable": "New release {latestVersion} of {extensionName} is available.",
"newExtensionVersionIsAvailable": "New {extensionName} version {latestVersion} is available.",
"uninstallConfirmation": "Are you sure you want to uninstall the extension?"
},
"descriptions": {

View File

@@ -5,7 +5,8 @@
"EmailTemplates": "Email Templates"
},
"fields": {
"order": "Order"
"order": "Order",
"childList": "Child List"
},
"links": {
"emailTemplates": "Email Templates"

Some files were not shown because too many files have changed in this diff Show More