duplicate check inprovements

This commit is contained in:
Yuri Kuznetsov
2023-07-10 11:19:58 +03:00
parent 9b176299ab
commit 82efcefac7
23 changed files with 495 additions and 191 deletions

View File

@@ -29,85 +29,5 @@
namespace Espo\Classes\DuplicateWhereBuilders;
use Espo\Core\Duplicate\WhereBuilder;
use Espo\Core\Field\EmailAddressGroup;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\ORM\Entity;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\Part\Where\OrGroup;
use Espo\ORM\Query\Part\WhereItem;
/**
* @implements WhereBuilder<CoreEntity>
*/
class Company implements WhereBuilder
{
/**
* @param CoreEntity $entity
*/
public function build(Entity $entity): ?WhereItem
{
$orBuilder = OrGroup::createBuilder();
$toCheck = false;
if ($entity->get('name')) {
$orBuilder->add(
Cond::equal(
Cond::column('name'),
$entity->get('name')
),
);
$toCheck = true;
}
if (
($entity->get('emailAddress') || $entity->get('emailAddressData')) &&
(
$entity->isNew() ||
$entity->isAttributeChanged('emailAddress') ||
$entity->isAttributeChanged('emailAddressData')
)
) {
foreach ($this->getEmailAddressList($entity) as $emailAddress) {
$orBuilder->add(
Cond::equal(
Cond::column('emailAddress'),
$emailAddress
)
);
$toCheck = true;
}
}
if (!$toCheck) {
return null;
}
return $orBuilder->build();
}
/**
* @return string[]
*/
private function getEmailAddressList(CoreEntity $entity): array
{
if ($entity->get('emailAddressData')) {
/** @var EmailAddressGroup $eaGroup */
$eaGroup = $entity->getValueObject('emailAddress');
return $eaGroup->getAddressList();
}
if ($entity->get('emailAddress')) {
return [
$entity->get('emailAddress')
];
}
return [];
}
}
class Company extends General
{}

View File

@@ -0,0 +1,262 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://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\Classes\DuplicateWhereBuilders;
use Espo\Core\Duplicate\WhereBuilder;
use Espo\Core\Field\EmailAddressGroup;
use Espo\Core\Field\PhoneNumberGroup;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\Part\Where\OrGroup;
use Espo\ORM\Query\Part\Where\OrGroupBuilder;
use Espo\ORM\Query\Part\WhereItem;
use Espo\ORM\Type\AttributeType;
/**
* @implements WhereBuilder<CoreEntity>
*/
class General implements WhereBuilder
{
public function __construct(
private Metadata $metadata,
private Defs $ormDefs
) {}
/**
* @param CoreEntity $entity
*/
public function build(Entity $entity): ?WhereItem
{
/** @var string[] $fieldList */
$fieldList = $this->metadata->get(['scopes', $entity->getEntityType(), 'duplicateCheckFieldList']) ?? [];
$orBuilder = OrGroup::createBuilder();
$toCheck = false;
foreach ($fieldList as $field) {
$toCheckItem = $this->applyField($field, $entity, $orBuilder);
if ($toCheckItem) {
$toCheck = true;
}
}
if (!$toCheck) {
return null;
}
return $orBuilder->build();
}
private function applyField(
string $field,
CoreEntity $entity,
OrGroupBuilder $orBuilder
): bool {
$type = $this->ormDefs
->getEntity($entity->getEntityType())
->tryGetField($field)
?->getType();
if ($type === 'personName') {
return $this->applyFieldPersonName($field, $entity, $orBuilder);
}
if ($type === 'email') {
return $this->applyFieldEmail($field, $entity, $orBuilder);
}
if ($type === 'phone') {
return $this->applyFieldPhone($field, $entity, $orBuilder);
}
if ($entity->getAttributeType($field) === AttributeType::VARCHAR) {
return $this->applyFieldVarchar($field, $entity, $orBuilder);
}
return false;
}
private function applyFieldPersonName(
string $field,
CoreEntity $entity,
OrGroupBuilder $orBuilder
): bool {
$first = 'first' . ucfirst($field);
$last = 'last' . ucfirst($field);
if (!$entity->get($first) && !$entity->get($last)) {
return false;
}
$orBuilder->add(
Cond::and(
Cond::equal(
Cond::column($first),
$entity->get($first)
),
Cond::equal(
Cond::column($last),
$entity->get($last)
)
)
);
return true;
}
private function applyFieldEmail(
string $field,
CoreEntity $entity,
OrGroupBuilder $orBuilder
): bool {
$toCheck = false;
if (
($entity->get($field) || $entity->get($field . 'Data')) &&
(
$entity->isNew() ||
$entity->isAttributeChanged($field) ||
$entity->isAttributeChanged($field . 'Data')
)
) {
foreach ($this->getEmailAddressList($entity) as $emailAddress) {
$orBuilder->add(
Cond::equal(
Cond::column($field),
$emailAddress
)
);
$toCheck = true;
}
}
return $toCheck;
}
private function applyFieldPhone(
string $field,
CoreEntity $entity,
OrGroupBuilder $orBuilder
): bool {
$toCheck = false;
if (
($entity->get($field) || $entity->get($field . 'Data')) &&
(
$entity->isNew() ||
$entity->isAttributeChanged($field) ||
$entity->isAttributeChanged($field . 'Data')
)
) {
foreach ($this->getPhoneNumberList($entity) as $phoneNumber) {
$orBuilder->add(
Cond::equal(
Cond::column($field),
$phoneNumber
)
);
$toCheck = true;
}
}
return $toCheck;
}
private function applyFieldVarchar(
string $field,
CoreEntity $entity,
OrGroupBuilder $orBuilder
): bool {
if (!$entity->get($field)) {
return false;
}
$orBuilder->add(
Cond::equal(
Cond::column($field),
$entity->get($field)
),
);
return true;
}
/**
* @return string[]
*/
private function getEmailAddressList(CoreEntity $entity): array
{
if ($entity->get('emailAddressData')) {
/** @var EmailAddressGroup $eaGroup */
$eaGroup = $entity->getValueObject('emailAddress');
return $eaGroup->getAddressList();
}
if ($entity->get('emailAddress')) {
return [
$entity->get('emailAddress')
];
}
return [];
}
/**
* @return string[]
*/
private function getPhoneNumberList(CoreEntity $entity): array
{
if ($entity->get('phoneNumberData')) {
/** @var PhoneNumberGroup $eaGroup */
$eaGroup = $entity->getValueObject('phoneNumber');
return $eaGroup->getNumberList();
}
if ($entity->get('phoneNumber')) {
return [$entity->get('phoneNumber')];
}
return [];
}
}

View File

@@ -29,92 +29,5 @@
namespace Espo\Classes\DuplicateWhereBuilders;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\Duplicate\WhereBuilder;
use Espo\Core\Field\EmailAddressGroup;
use Espo\ORM\Entity;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\Part\Where\OrGroup;
use Espo\ORM\Query\Part\WhereItem;
/**
* @implements WhereBuilder<CoreEntity>
*/
class Person implements WhereBuilder
{
/**
* @param CoreEntity $entity
*/
public function build(Entity $entity): ?WhereItem
{
$orBuilder = OrGroup::createBuilder();
$toCheck = false;
if ($entity->get('firstName') || $entity->get('lastName')) {
$orBuilder->add(
Cond::and(
Cond::equal(
Cond::column('firstName'),
$entity->get('firstName')
),
Cond::equal(
Cond::column('lastName'),
$entity->get('lastName')
)
)
);
$toCheck = true;
}
if (
($entity->get('emailAddress') || $entity->get('emailAddressData')) &&
(
$entity->isNew() ||
$entity->isAttributeChanged('emailAddress') ||
$entity->isAttributeChanged('emailAddressData')
)
) {
foreach ($this->getEmailAddressList($entity) as $emailAddress) {
$orBuilder->add(
Cond::equal(
Cond::column('emailAddress'),
$emailAddress
)
);
$toCheck = true;
}
}
if (!$toCheck) {
return null;
}
return $orBuilder->build();
}
/**
* @return string[]
*/
private function getEmailAddressList(CoreEntity $entity): array
{
if ($entity->get('emailAddressData')) {
/** @var EmailAddressGroup $eaGroup */
$eaGroup = $entity->getValueObject('emailAddress');
return $eaGroup->getAddressList();
}
if ($entity->get('emailAddress')) {
return [
$entity->get('emailAddress')
];
}
return [];
}
}
class Person extends General
{}

View File

@@ -29,6 +29,9 @@
namespace Espo\Core\Templates\Entities;
class Base extends \Espo\Core\ORM\Entity
use Espo\Core\ORM\Entity;
class Base extends Entity
{
public const TEMPLATE_TYPE = 'Base';
}

View File

@@ -29,6 +29,9 @@
namespace Espo\Core\Templates\Entities;
class BasePlus extends \Espo\Core\ORM\Entity
use Espo\Core\ORM\Entity;
class BasePlus extends Entity
{
public const TEMPLATE_TYPE = 'BasePlus';
}

View File

@@ -0,0 +1,3 @@
{
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
}

View File

@@ -0,0 +1,3 @@
{
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
}

View File

@@ -1,3 +1,3 @@
{
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\Company"
}
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
}

View File

@@ -7,5 +7,6 @@
"aclPortalLevelList": ["all", "account", "contact", "own", "no"],
"customizable": true,
"importable": true,
"notifications": true
}
"notifications": true,
"duplicateCheckFieldList": ["name", "emailAddress"]
}

View File

@@ -1,3 +1,3 @@
{
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\Person"
}
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
}

View File

@@ -8,5 +8,6 @@
"customizable": true,
"importable": true,
"notifications": true,
"hasPersonalData": true
}
"hasPersonalData": true,
"duplicateCheckFieldList": ["name", "emailAddress"]
}

View File

@@ -3,11 +3,9 @@
"activityStatusList": "Activity Statuses",
"historyStatusList": "History Statuses",
"completedStatusList": "Completed Statuses",
"canceledStatusList": "Canceled Statuses",
"updateDuplicateCheck": "Duplicate check on update"
"canceledStatusList": "Canceled Statuses"
},
"tooltips": {
"updateDuplicateCheck": "Perform checking for duplicates when updating a record.",
"activityStatusList": "Status values determining that an activity record should be displayed in the Activity panel and considered as actual.",
"historyStatusList": "Status values determining that an activity record should be displayed in the History panel.",
"completedStatusList": "Status values determining that an activity is completed.",

View File

@@ -6,6 +6,14 @@
"type": "bool",
"tooltip": true
}
},
"duplicateCheckFieldList": {
"location": "scopes",
"fieldDefs": {
"type": "bool",
"tooltip": true,
"view": "views/admin/entity-manager/fields/duplicate-check-field-list"
}
}
},
"Contact": {
@@ -15,6 +23,14 @@
"type": "bool",
"tooltip": true
}
},
"duplicateCheckFieldList": {
"location": "scopes",
"fieldDefs": {
"type": "bool",
"tooltip": true,
"view": "views/admin/entity-manager/fields/duplicate-check-field-list"
}
}
},
"Lead": {
@@ -24,6 +40,14 @@
"type": "bool",
"tooltip": true
}
},
"duplicateCheckFieldList": {
"location": "scopes",
"fieldDefs": {
"type": "bool",
"tooltip": true,
"view": "views/admin/entity-manager/fields/duplicate-check-field-list"
}
}
},
"Meeting": {

View File

@@ -1,3 +1,3 @@
{
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\Company"
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
}

View File

@@ -1,3 +1,3 @@
{
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\Person"
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
}

View File

@@ -1,3 +1,3 @@
{
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\Person"
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
}

View File

@@ -10,5 +10,6 @@
"importable": true,
"notifications": true,
"object": true,
"hasPersonalData": true
"hasPersonalData": true,
"duplicateCheckFieldList": ["name", "emailAddress"]
}

View File

@@ -10,5 +10,6 @@
"importable": true,
"notifications": true,
"object": true,
"hasPersonalData": true
"hasPersonalData": true,
"duplicateCheckFieldList": ["name", "emailAddress"]
}

View File

@@ -11,5 +11,6 @@
"notifications": true,
"object": true,
"statusField": "status",
"hasPersonalData": true
"hasPersonalData": true,
"duplicateCheckFieldList": ["name", "emailAddress"]
}

View File

@@ -40,7 +40,9 @@
"fullTextSearch": "Full-Text Search",
"parentEntityTypeList": "Parent Entity Types",
"foreignLinkEntityTypeList": "Foreign Links",
"optimisticConcurrencyControl": "Optimistic concurrency control"
"optimisticConcurrencyControl": "Optimistic concurrency control",
"updateDuplicateCheck": "Duplicate check on update",
"duplicateCheckFieldList": "Duplicate check fields"
},
"options": {
"type": {
@@ -75,6 +77,8 @@
"beforeSaveApiScript": "A script called on create and update API requests before an entity is saved. Use for custom validation and duplicate checking."
},
"tooltips": {
"duplicateCheckFieldList": "Which fields to check when performing checking for duplicates.",
"updateDuplicateCheck": "Perform checking for duplicates when updating a record.",
"optimisticConcurrencyControl": "Prevents writing conflicts.",
"statusField": "Updates of this field are logged in stream.",
"textFilterFields": "Fields used by text search.",

View File

@@ -15,6 +15,14 @@
"type": "bool",
"tooltip": true
}
},
"duplicateCheckFieldList": {
"location": "scopes",
"fieldDefs": {
"type": "bool",
"tooltip": true,
"view": "views/admin/entity-manager/fields/duplicate-check-field-list"
}
}
},
"@Person": {
@@ -24,6 +32,48 @@
"type": "bool",
"tooltip": true
}
},
"duplicateCheckFieldList": {
"location": "scopes",
"fieldDefs": {
"type": "bool",
"tooltip": true,
"view": "views/admin/entity-manager/fields/duplicate-check-field-list"
}
}
},
"@Base": {
"updateDuplicateCheck": {
"location": "recordDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
},
"duplicateCheckFieldList": {
"location": "scopes",
"fieldDefs": {
"type": "bool",
"tooltip": true,
"view": "views/admin/entity-manager/fields/duplicate-check-field-list"
}
}
},
"@BasePlus": {
"updateDuplicateCheck": {
"location": "recordDefs",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
},
"duplicateCheckFieldList": {
"location": "scopes",
"fieldDefs": {
"type": "bool",
"tooltip": true,
"view": "views/admin/entity-manager/fields/duplicate-check-field-list"
}
}
}
}

View File

@@ -0,0 +1,68 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://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.
************************************************************************/
import MultiEnumFieldView from 'views/fields/multi-enum';
class DuplicateFieldListCheckEntityManagerFieldView extends MultiEnumFieldView {
fieldTypeList = [
'varchar',
'personName',
'email',
'phone',
'url',
'barcode',
]
setupOptions() {
let entityType = this.model.get('name');
let options =
this.getFieldManager()
.getEntityTypeFieldList(entityType, {
typeList: this.fieldTypeList,
onlyAvailable: true,
})
.sort((a, b) => {
return this.getLanguage().translate(a, 'fields', this.entityType)
.localeCompare(
this.getLanguage().translate(b, 'fields', this.entityType)
);
});
this.translatedOptions = {};
options.forEach(item => {
this.translatedOptions[item] = this.translate(item, 'fields', entityType);
})
this.params.options = options;
}
}
export default DuplicateFieldListCheckEntityManagerFieldView;

View File

@@ -32,6 +32,11 @@ use Espo\Entities\Role;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\Part\Expression;
use Espo\ORM\Query\UpdateBuilder;
use Espo\Core\Templates\Entities\Company;
use Espo\Core\Templates\Entities\Person;
use Espo\Core\Templates\Entities\Base;
use Espo\Core\Templates\Entities\BasePlus;
use Espo\Core\Utils\Metadata;
class AfterUpgrade
{
@@ -40,6 +45,10 @@ class AfterUpgrade
$this->updateRoles(
$container->getByClass(EntityManager::class)
);
$this->updateMetadata(
$container->getByClass(Metadata::class)
);
}
private function updateRoles(EntityManager $entityManager): void
@@ -51,4 +60,43 @@ class AfterUpgrade
$entityManager->getQueryExecutor()->execute($query);
}
private function updateMetadata(Metadata $metadata): void
{
$defs = $metadata->get(['scopes']);
foreach ($defs as $entityType => $item) {
$isCustom = $item['isCustom'] ?? false;
$type = $item['type'] ?? false;
if (!$isCustom) {
continue;
}
if (
!in_array($type, [
BasePlus::TEMPLATE_TYPE,
Base::TEMPLATE_TYPE,
Company::TEMPLATE_TYPE,
Person::TEMPLATE_TYPE
])
) {
continue;
}
$recordDefs = $metadata->getCustom('recordDefs', $entityType) ?? (object) [];
$scopes = $metadata->getCustom('scopes', $entityType) ?? (object) [];
$recordDefs->duplicateWhereBuilderClassName = "Espo\\Classes\\DuplicateWhereBuilders\\General";
$scopes->duplicateCheckFieldList = [];
if ($type === Company::TEMPLATE_TYPE || $type === Person::TEMPLATE_TYPE) {
$scopes->duplicateCheckFieldList = ['name', 'emailAddress'];
}
$metadata->saveCustom('recordDefs', $entityType, $recordDefs);
$metadata->saveCustom('scopes', $entityType, $scopes);
}
}
}