From d30edee7907386f1df82af95ce6cc507859e80c4 Mon Sep 17 00:00:00 2001 From: Taras Machyshyn Date: Thu, 21 Jan 2021 16:14:14 +0200 Subject: [PATCH 01/10] Upgrade bug fixes --- .../Espo/Core/Console/Commands/Upgrade.php | 8 +++++--- .../Espo/Core/Upgrades/Actions/Base.php | 19 ++++++++++++------- .../Core/Upgrades/Actions/Base/Install.php | 16 ++++++++++------ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/application/Espo/Core/Console/Commands/Upgrade.php b/application/Espo/Core/Console/Commands/Upgrade.php index bbd920d142..2a7a523409 100644 --- a/application/Espo/Core/Console/Commands/Upgrade.php +++ b/application/Espo/Core/Console/Commands/Upgrade.php @@ -41,6 +41,7 @@ use Espo\Core\{ use Symfony\Component\Process\PhpExecutableFinder; use Exception; +use Throwable; class Upgrade implements Command { @@ -127,7 +128,7 @@ class Upgrade implements Command try { $this->runUpgradeProcess($upgradeId, $params); } - catch (Exception $e) { + catch (Throwable $e) { $errorMessage = $e->getMessage(); } @@ -137,7 +138,8 @@ class Upgrade implements Command $this->fileManager->unlink($packageFile); } - if (!empty($errorMessage)) { + if (isset($errorMessage)) { + $errorMessage = !empty($errorMessage) ? $errorMessage : "Error: An unexpected error occurred."; fwrite(\STDOUT, $errorMessage . "\n"); return; @@ -287,7 +289,7 @@ class Upgrade implements Command $upgradeManager = $this->getUpgradeManager(true); $upgradeManager->runInstallStep($stepName, ['id' => $upgradeId]); } - } catch (Exception $e) { + } catch (Throwable $e) { $GLOBALS['log']->error('Upgrade Error: ' . $e->getMessage()); throw new Error($e->getMessage()); diff --git a/application/Espo/Core/Upgrades/Actions/Base.php b/application/Espo/Core/Upgrades/Actions/Base.php index aad1a9b6a3..b93b051bd9 100644 --- a/application/Espo/Core/Upgrades/Actions/Base.php +++ b/application/Espo/Core/Upgrades/Actions/Base.php @@ -462,7 +462,7 @@ abstract class Base { if (!isset($this->data['restoreFileList'])) { $backupPath = $this->getPath('backupPath'); - $this->data['restoreFileList'] = $this->getFileList($backupPath); + $this->data['restoreFileList'] = $this->getFileList($backupPath, true); } return $this->data['restoreFileList']; @@ -496,7 +496,7 @@ abstract class Base * * @return array */ - protected function getFileList($dirPath) + protected function getFileList($dirPath, $skipVendorFileList = false) { $fileList = array(); @@ -508,10 +508,11 @@ abstract class Base } } - //vendor file list - $vendorFileList = $this->getVendorFileList('copy'); - if (!empty($vendorFileList)) { - $fileList = array_merge($fileList, $vendorFileList); + if (!$skipVendorFileList) { + $vendorFileList = $this->getVendorFileList('copy'); + if (!empty($vendorFileList)) { + $fileList = array_merge($fileList, $vendorFileList); + } } return $fileList; @@ -738,7 +739,11 @@ abstract class Base return true; } catch (Throwable $e) { - $GLOBALS['log']->error('Database rebuild failure, details: '. $e->getMessage() .'.'); + + try { + $GLOBALS['log']->error('Database rebuild failure, details: '. $e->getMessage() .'.'); + } + catch (Throwable $e) {} } return false; diff --git a/application/Espo/Core/Upgrades/Actions/Base/Install.php b/application/Espo/Core/Upgrades/Actions/Base/Install.php index 77b4e20f59..2e018741a5 100644 --- a/application/Espo/Core/Upgrades/Actions/Base/Install.php +++ b/application/Espo/Core/Upgrades/Actions/Base/Install.php @@ -150,19 +150,23 @@ class Install extends \Espo\Core\Upgrades\Actions\Base $GLOBALS['log']->info('Installation process ['. $this->getProcessId() .']: Start "copy" step.'); - /* remove files defined in a manifest "deleteBeforeCopy" */ - $this->deleteFiles('deleteBeforeCopy', true); + /* remove files defined in a manifest */ + if (!$this->deleteFiles('delete', true)) { + $this->throwErrorAndRemovePackage('Cannot delete files.'); + } /* copy files from directory "Files" to EspoCRM files */ if (!$this->copyFiles()) { $this->throwErrorAndRemovePackage('Cannot copy files.'); } - /* remove files defined in a manifest */ - $this->deleteFiles('delete', true); + if (!$this->deleteFiles('vendor')) { + $this->throwErrorAndRemovePackage('Cannot delete vendor files.'); + } - $this->deleteFiles('vendor'); - $this->copyFiles('vendor'); + if (!$this->copyFiles('vendor')) { + $this->throwErrorAndRemovePackage('Cannot copy vendor files.'); + } $GLOBALS['log']->info('Installation process ['. $this->getProcessId() .']: End "copy" step.'); } From 3b0c7f1d5b4ffc54cd1c762d9ef2eb9962222bad Mon Sep 17 00:00:00 2001 From: Taras Machyshyn Date: Thu, 21 Jan 2021 17:39:44 +0200 Subject: [PATCH 02/10] Upgrade bug fixes --- application/Espo/Core/Console/Commands/Upgrade.php | 10 ++++++++-- .../Espo/Core/Upgrades/Actions/Base/Install.php | 2 -- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/application/Espo/Core/Console/Commands/Upgrade.php b/application/Espo/Core/Console/Commands/Upgrade.php index 2a7a523409..dc976b2fc1 100644 --- a/application/Espo/Core/Console/Commands/Upgrade.php +++ b/application/Espo/Core/Console/Commands/Upgrade.php @@ -290,7 +290,10 @@ class Upgrade implements Command $upgradeManager->runInstallStep($stepName, ['id' => $upgradeId]); } } catch (Throwable $e) { - $GLOBALS['log']->error('Upgrade Error: ' . $e->getMessage()); + try { + $GLOBALS['log']->error('Upgrade Error: ' . $e->getMessage()); + } + catch (Throwable $t) {} throw new Error($e->getMessage()); } @@ -310,7 +313,10 @@ class Upgrade implements Command $shellResult = shell_exec($command); if ($shellResult !== 'true') { - $GLOBALS['log']->error('Upgrade Error: ' . $shellResult); + try { + $GLOBALS['log']->error('Upgrade Error: ' . $shellResult); + } + catch (Throwable $t) {} throw new Error($shellResult); } diff --git a/application/Espo/Core/Upgrades/Actions/Base/Install.php b/application/Espo/Core/Upgrades/Actions/Base/Install.php index 2e018741a5..e2d00005e8 100644 --- a/application/Espo/Core/Upgrades/Actions/Base/Install.php +++ b/application/Espo/Core/Upgrades/Actions/Base/Install.php @@ -77,8 +77,6 @@ class Install extends \Espo\Core\Upgrades\Actions\Base protected function initPackage(array $data) { - $GLOBALS['log']->setLevel('info'); - $processId = $data['id']; if (empty($processId)) { From 1f17d79e2dc027685f6d6fc5b9c27cb07645e18e Mon Sep 17 00:00:00 2001 From: Taras Machyshyn Date: Thu, 21 Jan 2021 17:53:54 +0200 Subject: [PATCH 03/10] Upgrade fixes --- application/Espo/Core/Console/Commands/UpgradeStep.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/Espo/Core/Console/Commands/UpgradeStep.php b/application/Espo/Core/Console/Commands/UpgradeStep.php index f4715a269c..881de60863 100644 --- a/application/Espo/Core/Console/Commands/UpgradeStep.php +++ b/application/Espo/Core/Console/Commands/UpgradeStep.php @@ -73,7 +73,7 @@ class UpgradeStep implements Command try { $result = $upgradeManager->runInstallStep($stepName, $params); // throw Exception on error } catch (\Exception $e) { - die("Error: " . $e->getMessage() . "\n"); + die("Error: " . $e->getMessage()); } if (is_bool($result)) { From 654741261bce818bb2f501e345b722976b8c1a79 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 23 Jan 2021 15:11:56 +0200 Subject: [PATCH 04/10] Address formatter --- .../Classes/AddressFormatters/Formatter1.php | 89 ++++++++++ .../Classes/AddressFormatters/Formatter2.php | 91 ++++++++++ .../Classes/AddressFormatters/Formatter3.php | 89 ++++++++++ .../Classes/AddressFormatters/Formatter4.php | 89 ++++++++++ .../Espo/Core/FieldUtils/Address/Address.php | 156 +++++++++++++++++ .../FieldUtils/Address/AddressBuilder.php | 104 ++++++++++++ .../FieldUtils/Address/AddressFormatter.php | 38 +++++ .../Address/AddressFormatterFactory.php | 74 ++++++++ .../AddressFormatterMetadataProvider.php | 49 ++++++ .../metadata/app/addressFormats.json | 14 ++ .../Espo/Resources/metadata/app/metadata.json | 1 + .../Espo/Tools/Export/Formats/Xlsx.php | 104 ++++-------- .../Address/AddressFormatterTest.php | 62 +++++++ .../Address/AddressFormattersTest.php | 159 ++++++++++++++++++ .../Core/FieldUtils/Address/AddressTest.php | 122 ++++++++++++++ 15 files changed, 1169 insertions(+), 72 deletions(-) create mode 100644 application/Espo/Classes/AddressFormatters/Formatter1.php create mode 100644 application/Espo/Classes/AddressFormatters/Formatter2.php create mode 100644 application/Espo/Classes/AddressFormatters/Formatter3.php create mode 100644 application/Espo/Classes/AddressFormatters/Formatter4.php create mode 100644 application/Espo/Core/FieldUtils/Address/Address.php create mode 100644 application/Espo/Core/FieldUtils/Address/AddressBuilder.php create mode 100644 application/Espo/Core/FieldUtils/Address/AddressFormatter.php create mode 100644 application/Espo/Core/FieldUtils/Address/AddressFormatterFactory.php create mode 100644 application/Espo/Core/FieldUtils/Address/AddressFormatterMetadataProvider.php create mode 100644 application/Espo/Resources/metadata/app/addressFormats.json create mode 100644 tests/integration/Espo/Core/FieldUtils/Address/AddressFormatterTest.php create mode 100644 tests/unit/Espo/Core/FieldUtils/Address/AddressFormattersTest.php create mode 100644 tests/unit/Espo/Core/FieldUtils/Address/AddressTest.php diff --git a/application/Espo/Classes/AddressFormatters/Formatter1.php b/application/Espo/Classes/AddressFormatters/Formatter1.php new file mode 100644 index 0000000000..2a1050e15e --- /dev/null +++ b/application/Espo/Classes/AddressFormatters/Formatter1.php @@ -0,0 +1,89 @@ +getStreet(); + $city = $address->getCity(); + $country = $address->getCountry(); + $state = $address->getState(); + $postalCode = $address->getPostalCode(); + + if ($street) { + $result .= $street; + } + + if ($city || $state || $postalCode) { + if ($result) { + $result .= "\n"; + } + + if ($city) { + $result .= $city; + } + + if ($state && $city) { + $result .= ', '; + } + + if ($state) { + $result .= $state; + } + + if ($postalCode && ($state || $city)) { + $result .= ' '; + } + + if ($postalCode) { + $result .= $postalCode; + } + } + + if ($country) { + if ($result) { + $result .= "\n"; + } + + $result .= $country; + } + + return $result; + } +} diff --git a/application/Espo/Classes/AddressFormatters/Formatter2.php b/application/Espo/Classes/AddressFormatters/Formatter2.php new file mode 100644 index 0000000000..e9093c9a63 --- /dev/null +++ b/application/Espo/Classes/AddressFormatters/Formatter2.php @@ -0,0 +1,91 @@ +getStreet(); + $city = $address->getCity(); + $country = $address->getCountry(); + $state = $address->getState(); + $postalCode = $address->getPostalCode(); + + if ($street) { + $result .= $street; + } + + if ($city || $postalCode) { + if ($result) { + $result .= "\n"; + } + + if ($postalCode) { + $result .= $postalCode; + } + + if ($postalCode && $city) { + $result .= ' '; + } + + if ($city) { + $result .= $city; + } + } + + if ($state || $country) { + if ($result) { + $result .= "\n"; + } + + if ($state) { + $result .= $state; + } + + if ($state && $country) { + $result .= ' '; + } + + if ($country) { + $result .= $country; + } + } + + return $result; + } +} diff --git a/application/Espo/Classes/AddressFormatters/Formatter3.php b/application/Espo/Classes/AddressFormatters/Formatter3.php new file mode 100644 index 0000000000..0514a03ac7 --- /dev/null +++ b/application/Espo/Classes/AddressFormatters/Formatter3.php @@ -0,0 +1,89 @@ +getStreet(); + $city = $address->getCity(); + $country = $address->getCountry(); + $state = $address->getState(); + $postalCode = $address->getPostalCode(); + + if ($country) { + $result .= $country; + } + + if ($city || $state || $postalCode) { + if ($result) { + $result .= "\n"; + } + + if ($state) { + $result .= $state; + } + + if ($state && $postalCode) { + $result .= ' '; + } + + if ($postalCode) { + $result .= $postalCode; + } + + if ($city && ($state || $postalCode)) { + $result .= ' '; + } + + if ($city) { + $result .= $city; + } + } + + if ($street) { + if ($result) { + $result .= "\n"; + } + + $result .= $street; + } + + return $result; + } +} diff --git a/application/Espo/Classes/AddressFormatters/Formatter4.php b/application/Espo/Classes/AddressFormatters/Formatter4.php new file mode 100644 index 0000000000..9282179012 --- /dev/null +++ b/application/Espo/Classes/AddressFormatters/Formatter4.php @@ -0,0 +1,89 @@ +getStreet(); + $city = $address->getCity(); + $country = $address->getCountry(); + $state = $address->getState(); + $postalCode = $address->getPostalCode(); + + if ($street) { + $result .= $street; + } + + if ($city) { + if ($result) { + $result .= "\n"; + } + + $result .= $city; + } + + if ($country || $state || $postalCode) { + if ($result) { + $result .= "\n"; + } + + if ($country) { + $result .= $country; + } + + if ($state && $country) { + $result .= ' - '; + } + + if ($state) { + $result .= $state; + } + + if ($postalCode && ($state || $country)) { + $result .= ' '; + } + + if ($postalCode) { + $result .= $postalCode; + } + } + + return $result; + } +} diff --git a/application/Espo/Core/FieldUtils/Address/Address.php b/application/Espo/Core/FieldUtils/Address/Address.php new file mode 100644 index 0000000000..f4d1dad7a4 --- /dev/null +++ b/application/Espo/Core/FieldUtils/Address/Address.php @@ -0,0 +1,156 @@ +street; + } + + public function getCity() : ?string + { + return $this->city; + } + + public function getCountry() : ?string + { + return $this->country; + } + + public function getState() : ?string + { + return $this->state; + } + + public function getPostalCode() : ?string + { + return $this->postalCode; + } + + public function withStreet(?string $street) : self + { + $newAddress = self::createBuilder() + ->clone($this) + ->setStreet($street) + ->build(); + + return $newAddress; + } + + public function withCity(?string $city) : self + { + $newAddress = self::createBuilder() + ->clone($this) + ->setCity($city) + ->build(); + + return $newAddress; + } + + public function withCountry(?string $country) : self + { + $newAddress = self::createBuilder() + ->clone($this) + ->setCountry($country) + ->build(); + + return $newAddress; + } + + public function withState(?string $state) : self + { + $newAddress = self::createBuilder() + ->clone($this) + ->setState($state) + ->build(); + + return $newAddress; + } + + public function withPostalCode(?string $postalCode) : self + { + $newAddress = self::createBuilder() + ->clone($this) + ->setPostalCode($postalCode) + ->build(); + + return $newAddress; + } + + public static function fromEntity(Entity $entity, string $field) : self + { + $obj = new self(); + + $obj->street = $entity->get($field . 'Street'); + $obj->city = $entity->get($field . 'City'); + $obj->country = $entity->get($field . 'Country'); + $obj->state = $entity->get($field . 'State'); + $obj->postalCode = $entity->get($field . 'PostalCode'); + + return $obj; + } + + public static function fromRaw(array $raw) : self + { + $obj = new self(); + + $obj->street = $raw['street']; + $obj->city = $raw['city']; + $obj->country = $raw['country']; + $obj->state = $raw['state']; + $obj->postalCode = $raw['postalCode']; + + return $obj; + } + + public static function createBuilder() : AddressBuilder + { + return new AddressBuilder(); + } +} diff --git a/application/Espo/Core/FieldUtils/Address/AddressBuilder.php b/application/Espo/Core/FieldUtils/Address/AddressBuilder.php new file mode 100644 index 0000000000..de93d94e99 --- /dev/null +++ b/application/Espo/Core/FieldUtils/Address/AddressBuilder.php @@ -0,0 +1,104 @@ +setStreet($address->getStreet()); + $this->setCity($address->getCity()); + $this->setCountry($address->getCountry()); + $this->setState($address->getState()); + $this->setPostalCode($address->getPostalCode()); + + return $this; + } + + public function setStreet(?string $street) : self + { + $this->street = $street; + + return $this; + } + + public function setCity(?string $city) : self + { + $this->city = $city; + + return $this; + } + + public function setCountry(?string $country) : self + { + $this->country = $country; + + return $this; + } + + public function setState(?string $state) : self + { + $this->state = $state; + + return $this; + } + + public function setPostalCode(?string $postalCode) : self + { + $this->postalCode = $postalCode; + + return $this; + } + + public function build() : Address + { + return Address::fromRaw([ + 'street' => $this->street, + 'city' => $this->city, + 'country' => $this->country, + 'state' => $this->state, + 'postalCode' => $this->postalCode, + ]); + } + +} diff --git a/application/Espo/Core/FieldUtils/Address/AddressFormatter.php b/application/Espo/Core/FieldUtils/Address/AddressFormatter.php new file mode 100644 index 0000000000..afcfc8e104 --- /dev/null +++ b/application/Espo/Core/FieldUtils/Address/AddressFormatter.php @@ -0,0 +1,38 @@ +metadataProvider = $metadataProvider; + $this->injectableFactory = $injectableFactory; + $this->config = $config; + } + + public function create(int $format) : AddressFormatter + { + $className = $this->metadataProvider->getFormatterClassName($format); + + if (!$className) { + throw new RuntimeException("Unknown address format '{$format}'."); + } + + return $this->injectableFactory->create($className); + } + + public function createDefault() : AddressFormatter + { + $format = $this->config->get('addressFormat') ?? 1; + + return $this->create($format); + } +} diff --git a/application/Espo/Core/FieldUtils/Address/AddressFormatterMetadataProvider.php b/application/Espo/Core/FieldUtils/Address/AddressFormatterMetadataProvider.php new file mode 100644 index 0000000000..0ccd53743e --- /dev/null +++ b/application/Espo/Core/FieldUtils/Address/AddressFormatterMetadataProvider.php @@ -0,0 +1,49 @@ +metadata = $metadata; + } + + public function getFormatterClassName(int $format) : ?string + { + return $this->metadata->get([ + 'app', 'addressFormats', strval($format), 'formatterClassName', + ]); + } +} diff --git a/application/Espo/Resources/metadata/app/addressFormats.json b/application/Espo/Resources/metadata/app/addressFormats.json new file mode 100644 index 0000000000..b64b5bba2b --- /dev/null +++ b/application/Espo/Resources/metadata/app/addressFormats.json @@ -0,0 +1,14 @@ +{ + "1": { + "formatterClassName": "Espo\\Classes\\AddressFormatters\\Formatter1" + }, + "2": { + "formatterClassName": "Espo\\Classes\\AddressFormatters\\Formatter2" + }, + "3": { + "formatterClassName": "Espo\\Classes\\AddressFormatters\\Formatter3" + }, + "4": { + "formatterClassName": "Espo\\Classes\\AddressFormatters\\Formatter4" + } +} \ No newline at end of file diff --git a/application/Espo/Resources/metadata/app/metadata.json b/application/Espo/Resources/metadata/app/metadata.json index 4b24c62ec2..7cd2818246 100644 --- a/application/Espo/Resources/metadata/app/metadata.json +++ b/application/Espo/Resources/metadata/app/metadata.json @@ -14,6 +14,7 @@ ["app", "appParams"], ["app", "cleanup"], ["app", "pdfEngines", "__ANY__", "implementationClassNameMap"], + ["app", "addressFormats", "__ANY__", "formatterClassName"], ["app", "auth2FAMethods", "__ANY__", "implementationClassName"], ["app", "auth2FAMethods", "__ANY__", "implementationUserClassName"], ["authenticationMethods", "__ANY__", "implementationClassName"] diff --git a/application/Espo/Tools/Export/Formats/Xlsx.php b/application/Espo/Tools/Export/Formats/Xlsx.php index c089f9cdcc..e8a841649e 100644 --- a/application/Espo/Tools/Export/Formats/Xlsx.php +++ b/application/Espo/Tools/Export/Formats/Xlsx.php @@ -40,6 +40,8 @@ use Espo\Core\{ FileStorage\Manager as FileStorageManager, Utils\File\Manager as FileManager, ORM\EntityManager, + FieldUtils\Address\Address, + FieldUtils\Address\AddressFormatterFactory, }; use PhpOffice\PhpSpreadsheet\Cell\DataType; @@ -62,6 +64,7 @@ class Xlsx protected $entityManager; protected $fileStorageManager; protected $fileManager; + protected $addressFormatterFactory; public function __construct( Config $config, @@ -70,7 +73,8 @@ class Xlsx DateTimeUtil $dateTime, EntityManager $entityManager, FileStorageManager $fileStorageManager, - FileManager $fileManager + FileManager $fileManager, + AddressFormatterFactory $addressFormatterFactory ) { $this->config = $config; $this->metadata = $metadata; @@ -79,21 +83,7 @@ class Xlsx $this->entityManager = $entityManager; $this->fileStorageManager = $fileStorageManager; $this->fileManager = $fileManager; - } - - protected function getConfig() - { - return $this->config; - } - - protected function getMetadata() - { - return $this->metadata; - } - - protected function getEntityManager() - { - return $this->entityManager; + $this->addressFormatterFactory = $addressFormatterFactory; } public function loadAdditionalFields(Entity $entity, $fieldList) @@ -124,7 +114,7 @@ class Xlsx } } foreach ($fieldList as $field) { - $fieldType = $this->getMetadata()->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']); + $fieldType = $this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']); if ($fieldType === 'linkMultiple' || $fieldType === 'attachmentMultiple') { if (!$entity->has($field . 'Ids')) { @@ -138,7 +128,7 @@ class Xlsx { if ($exportAllFields) { foreach ($fieldList as $i => $field) { - $type = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field, 'type']); + $type = $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']); if (in_array($type, ['linkMultiple', 'attachmentMultiple'])) { unset($fieldList[$i]); } @@ -156,7 +146,7 @@ class Xlsx $attributeList[] = 'id'; } - $linkDefs = $this->getMetadata()->get(['entityDefs', $entityType, 'links']); + $linkDefs = $this->metadata->get(['entityDefs', $entityType, 'links']); if (is_array($linkDefs)) { foreach ($linkDefs as $link => $defs) { @@ -168,7 +158,7 @@ class Xlsx $linkList[] = $link; } else if ($defs['type'] === 'belongsTo' && !empty($defs['noJoin'])) { - if ($this->getMetadata()->get(['entityDefs', $entityType, 'fields', $link])) { + if ($this->metadata->get(['entityDefs', $entityType, 'fields', $link])) { $linkList[] = $link; } } @@ -182,7 +172,7 @@ class Xlsx } foreach ($fieldList as $field) { - $type = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field, 'type']); + $type = $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']); if ($type === 'currencyConverted') { if (!in_array($field, $attributeList)) { @@ -402,7 +392,7 @@ class Xlsx if (array_key_exists($name.'Currency', $row) && array_key_exists($name, $row)) { $sheet->setCellValue("$col$rowNumber", $row[$name] ? $row[$name] : ''); - $currency = $row[$name . 'Currency'] ?? $this->getConfig()->get('defaultCurrency'); + $currency = $row[$name . 'Currency'] ?? $this->config->get('defaultCurrency'); $sheet->getStyle("$col$rowNumber") ->getNumberFormat() @@ -413,7 +403,7 @@ class Xlsx } else if ($type == 'currencyConverted') { if (array_key_exists($name, $row)) { - $currency = $this->getConfig()->get('defaultCurrency'); + $currency = $this->config->get('defaultCurrency'); $sheet->getStyle("$col$rowNumber") ->getNumberFormat() @@ -481,7 +471,7 @@ class Xlsx } else if ($type == 'image') { if (isset($row[$name . 'Id']) && $row[$name . 'Id']) { - $attachment = $this->getEntityManager()->getEntity('Attachment', $row[$name . 'Id']); + $attachment = $this->entityManager->getEntity('Attachment', $row[$name . 'Id']); if ($attachment) { $objDrawing = new Drawing(); @@ -533,47 +523,17 @@ class Xlsx } } else if ($type == 'address') { - $value = ''; + $address = Address::createBuilder() + ->setStreet($row[$name . 'Street'] ?? null) + ->setCity($row[$name . 'City'] ?? null) + ->setState($row[$name . 'State'] ?? null) + ->setCountry($row[$name . 'Country'] ?? null) + ->setPostalCode($row[$name . 'PostalCode'] ?? null) + ->build(); - if (!empty($row[$name . 'Street'])) { - $value = $value .= $row[$name.'Street']; - } + $formatter = $this->addressFormatterFactory->createDefault(); - if (!empty($row[$name.'City']) || !empty($row[$name.'State']) || !empty($row[$name.'PostalCode'])) { - if ($value) { - $value .= "\n"; - } - - if (!empty($row[$name.'City'])) { - $value .= $row[$name.'City']; - - if ( - !empty($row[$name.'State']) || !empty($row[$name.'PostalCode']) - ) { - $value .= ', '; - } - } - - if (!empty($row[$name.'State'])) { - $value .= $row[$name.'State']; - - if (!empty($row[$name.'PostalCode'])) { - $value .= ' '; - } - } - - if (!empty($row[$name.'PostalCode'])) { - $value .= $row[$name.'PostalCode']; - } - } - - if (!empty($row[$name.'Country'])) { - if ($value) { - $value .= "\n"; - } - - $value .= $row[$name.'Country']; - } + $value = $formatter->format($address); $sheet->setCellValue("$col$rowNumber", $value); } @@ -656,7 +616,7 @@ class Xlsx if ($name == 'name') { if (array_key_exists('id', $row)) { - $link = $this->getConfig()->getSiteUrl() . "/#".$entityType . "/view/" . $row['id']; + $link = $this->config->getSiteUrl() . "/#".$entityType . "/view/" . $row['id']; } } else if ($type == 'url') { @@ -669,33 +629,33 @@ class Xlsx $foreignEntity = null; if (!$isForeign) { - $foreignEntity = $this->getMetadata()->get( + $foreignEntity = $this->metadata->get( ['entityDefs', $entityType, 'links', $name, 'entity'] ); } else { - $foreignEntity1 = $this->getMetadata()->get( + $foreignEntity1 = $this->metadata->get( ['entityDefs', $entityType, 'links', $foreignLink, 'entity'] ); - $foreignEntity = $this->getMetadata()->get( + $foreignEntity = $this->metadata->get( ['entityDefs', $foreignEntity1, 'links', $foreignField, 'entity'] ); } if ($foreignEntity) { - $link = $this->getConfig()->getSiteUrl() . "/#" . $foreignEntity. "/view/". $row[$name.'Id']; + $link = $this->config->getSiteUrl() . "/#" . $foreignEntity. "/view/". $row[$name.'Id']; } } } else if ($type == 'file') { if (array_key_exists($name.'Id', $row)) { - $link = $this->getConfig()->getSiteUrl() . "/?entryPoint=download&id=" . $row[$name.'Id']; + $link = $this->config->getSiteUrl() . "/?entryPoint=download&id=" . $row[$name.'Id']; } } else if ($type == 'linkParent') { if (array_key_exists($name.'Id', $row) && array_key_exists($name.'Type', $row)) { - $link = $this->getConfig()->getSiteUrl() . "/#".$row[$name.'Type']."/view/". $row[$name.'Id']; + $link = $this->config->getSiteUrl() . "/#".$row[$name.'Type']."/view/". $row[$name.'Id']; } } else if ($type == 'phone') { @@ -809,9 +769,9 @@ class Xlsx protected function getCurrencyFormatCode(string $currency) : string { - $currencySymbol = $this->getMetadata()->get(['app', 'currency', 'symbolMap', $currency], ''); + $currencySymbol = $this->metadata->get(['app', 'currency', 'symbolMap', $currency], ''); - $currencyFormat = $this->getConfig()->get('currencyFormat') ?? 2; + $currencyFormat = $this->config->get('currencyFormat') ?? 2; if ($currencyFormat == 3) { return '#,##0.00_-"' . $currencySymbol . '"'; diff --git a/tests/integration/Espo/Core/FieldUtils/Address/AddressFormatterTest.php b/tests/integration/Espo/Core/FieldUtils/Address/AddressFormatterTest.php new file mode 100644 index 0000000000..fed9def420 --- /dev/null +++ b/tests/integration/Espo/Core/FieldUtils/Address/AddressFormatterTest.php @@ -0,0 +1,62 @@ +getContainer()->get('injectableFactory')->create(AddressFormatterFactory::class); + + $formatter = $formatterFactory->create(1); + + $address = Address::createBuilder() + ->setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState('state') + ->setPostalCode('postalCode') + ->build(); + + $expected = + "street\n" . + "city, state postalCode\n" . + "country"; + + $result = $formatter->format($address); + + $this->assertEquals($expected, $result); + } +} diff --git a/tests/unit/Espo/Core/FieldUtils/Address/AddressFormattersTest.php b/tests/unit/Espo/Core/FieldUtils/Address/AddressFormattersTest.php new file mode 100644 index 0000000000..ee436c9134 --- /dev/null +++ b/tests/unit/Espo/Core/FieldUtils/Address/AddressFormattersTest.php @@ -0,0 +1,159 @@ +setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState('state') + ->setPostalCode('postalCode') + ->build(); + + $formatter = new Formatter1(); + + $expected = + "street\n" . + "city, state postalCode\n" . + "country"; + + $result = $formatter->format($address); + + $this->assertEquals($expected, $result); + } + + public function testFormat1NoState() + { + $address = Address::createBuilder() + ->setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState(null) + ->setPostalCode('postalCode') + ->build(); + + $formatter = new Formatter1(); + + $expected = + "street\n" . + "city postalCode\n" . + "country"; + + $result = $formatter->format($address); + + $this->assertEquals($expected, $result); + } + + public function testFormat2All() + { + $address = Address::createBuilder() + ->setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState('state') + ->setPostalCode('postalCode') + ->build(); + + $formatter = new Formatter2(); + + $expected = + "street\n" . + "postalCode city\n" . + "state country"; + + $result = $formatter->format($address); + + $this->assertEquals($expected, $result); + } + + public function testFormat3All() + { + $address = Address::createBuilder() + ->setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState('state') + ->setPostalCode('postalCode') + ->build(); + + $formatter = new Formatter3(); + + $expected = + "country\n" . + "state postalCode city\n" . + "street"; + + $result = $formatter->format($address); + + $this->assertEquals($expected, $result); + } + + public function testFormat4All() + { + $address = Address::createBuilder() + ->setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState('state') + ->setPostalCode('postalCode') + ->build(); + + $formatter = new Formatter4(); + + $expected = + "street\n" . + "city\n" . + "country - state postalCode"; + + $result = $formatter->format($address); + + $this->assertEquals($expected, $result); + } +} diff --git a/tests/unit/Espo/Core/FieldUtils/Address/AddressTest.php b/tests/unit/Espo/Core/FieldUtils/Address/AddressTest.php new file mode 100644 index 0000000000..7694dcc070 --- /dev/null +++ b/tests/unit/Espo/Core/FieldUtils/Address/AddressTest.php @@ -0,0 +1,122 @@ +setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState('state') + ->setPostalCode('postalCode') + ->build(); + + $this->assertEquals('street', $address->getStreet()); + $this->assertEquals('city', $address->getCity()); + $this->assertEquals('country', $address->getCountry()); + $this->assertEquals('state', $address->getState()); + $this->assertEquals('postalCode', $address->getPostalCode()); + } + + public function testBuilderClone() + { + $addressOriginal = Address::createBuilder() + ->setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState('state') + ->setPostalCode('postalCode') + ->build(); + + $address = Address::createBuilder() + ->clone($addressOriginal) + ->build(); + + $this->assertEquals('street', $address->getStreet()); + $this->assertEquals('city', $address->getCity()); + $this->assertEquals('country', $address->getCountry()); + $this->assertEquals('state', $address->getState()); + $this->assertEquals('postalCode', $address->getPostalCode()); + } + + public function testAddressWith() + { + $addressOriginal = Address::createBuilder() + ->setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState('state') + ->setPostalCode('postalCode') + ->build(); + + $address = $addressOriginal->withStreet('new street'); + + $this->assertEquals('new street', $address->getStreet()); + $this->assertEquals('city', $address->getCity()); + } + + public function testFromEntity() + { + $entity = $this->createMock(Entity::class); + + $entity + ->expects($this->any()) + ->method('get') + ->willReturnMap([ + ['addressStreet', 'street'], + ['addressCity', 'city'], + ['addressCountry', 'country'], + ['addressState', null], + ['addressPostalCode', null], + ]); + + $address = Address::fromEntity($entity, 'address'); + + $this->assertEquals('street', $address->getStreet()); + $this->assertEquals('city', $address->getCity()); + $this->assertEquals('country', $address->getCountry()); + $this->assertEquals(null, $address->getState()); + $this->assertEquals(null, $address->getPostalCode()); + } +} From e2b6f0c70f870ccb2038b9f3844673be578806a9 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 23 Jan 2021 21:22:21 +0200 Subject: [PATCH 05/10] cs fix --- application/Espo/Core/FieldUtils/Address/AddressBuilder.php | 1 - application/Espo/Resources/metadata/app/addressFormats.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/application/Espo/Core/FieldUtils/Address/AddressBuilder.php b/application/Espo/Core/FieldUtils/Address/AddressBuilder.php index de93d94e99..f3b59a4880 100644 --- a/application/Espo/Core/FieldUtils/Address/AddressBuilder.php +++ b/application/Espo/Core/FieldUtils/Address/AddressBuilder.php @@ -100,5 +100,4 @@ class AddressBuilder 'postalCode' => $this->postalCode, ]); } - } diff --git a/application/Espo/Resources/metadata/app/addressFormats.json b/application/Espo/Resources/metadata/app/addressFormats.json index b64b5bba2b..9add753f77 100644 --- a/application/Espo/Resources/metadata/app/addressFormats.json +++ b/application/Espo/Resources/metadata/app/addressFormats.json @@ -11,4 +11,4 @@ "4": { "formatterClassName": "Espo\\Classes\\AddressFormatters\\Formatter4" } -} \ No newline at end of file +} From 89ff8949170519b264946f46d1993355e4a74f26 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 24 Jan 2021 15:39:29 +0200 Subject: [PATCH 06/10] fix address --- application/Espo/Core/FieldUtils/Address/Address.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/application/Espo/Core/FieldUtils/Address/Address.php b/application/Espo/Core/FieldUtils/Address/Address.php index f4d1dad7a4..433ddb4b18 100644 --- a/application/Espo/Core/FieldUtils/Address/Address.php +++ b/application/Espo/Core/FieldUtils/Address/Address.php @@ -38,15 +38,15 @@ use Espo\{ */ class Address { - protected $street; + protected $street = null; - protected $city; + protected $city = null; - protected $country; + protected $country = null; - protected $state; + protected $state = null; - protected $portalCode; + protected $portalCode = null; public function getStreet() : ?string { From 5610ed6e15204af4930f527aea862f496afca803 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 24 Jan 2021 17:13:03 +0200 Subject: [PATCH 07/10] Currency field utils --- .../Currency/CurrencyConfigDataProvider.php | 94 +++++++++++ .../FieldUtils/Currency/CurrencyConverter.php | 111 ++++++++++++ .../FieldUtils/Currency/CurrencyRates.php | 67 ++++++++ .../FieldUtils/Currency/CurrencyValue.php | 134 +++++++++++++++ .../CurrencyConfigDataProviderTest.php | 139 +++++++++++++++ .../Currency/CurrencyConverterTest.php | 159 ++++++++++++++++++ .../FieldUtils/Currency/CurrencyValueTest.php | 117 +++++++++++++ 7 files changed, 821 insertions(+) create mode 100644 application/Espo/Core/FieldUtils/Currency/CurrencyConfigDataProvider.php create mode 100644 application/Espo/Core/FieldUtils/Currency/CurrencyConverter.php create mode 100644 application/Espo/Core/FieldUtils/Currency/CurrencyRates.php create mode 100644 application/Espo/Core/FieldUtils/Currency/CurrencyValue.php create mode 100644 tests/unit/Espo/Core/FieldUtils/Currency/CurrencyConfigDataProviderTest.php create mode 100644 tests/unit/Espo/Core/FieldUtils/Currency/CurrencyConverterTest.php create mode 100644 tests/unit/Espo/Core/FieldUtils/Currency/CurrencyValueTest.php diff --git a/application/Espo/Core/FieldUtils/Currency/CurrencyConfigDataProvider.php b/application/Espo/Core/FieldUtils/Currency/CurrencyConfigDataProvider.php new file mode 100644 index 0000000000..8eeee4f3e2 --- /dev/null +++ b/application/Espo/Core/FieldUtils/Currency/CurrencyConfigDataProvider.php @@ -0,0 +1,94 @@ +config = $config; + } + + /** + * Get a system default currency. + */ + public function getDefaultCurrency() : string + { + return $this->config->get('defaultCurrency'); + } + + /** + * Get a base currency, used for conversion. + */ + public function getBaseCurrency() : string + { + return $this->config->get('baseCurrency'); + } + + /** + * Get a list of available currencies. + * + * @return array + */ + public function getCurrencyList() : array + { + return $this->config->get('currencyList') ?? []; + } + + /** + * Whether a currency is available in the system. + */ + public function hasCurrency(string $currencyCode) : bool + { + return in_array($currencyCode, $this->getCurrencyList()); + } + + /** + * Get a rate of a specific currency related to the base currency. + */ + public function getCurrencyRate(string $currencyCode) : float + { + $rates = $this->config->get('currencyRates') ?? []; + + if (!$this->hasCurrency($currencyCode)) { + throw new RuntimeException("Can't get currency rate of '{$currencyCode}' currency."); + } + + $rate = $rates[$currencyCode] ?? 1.0; + + return $rate; + } +} diff --git a/application/Espo/Core/FieldUtils/Currency/CurrencyConverter.php b/application/Espo/Core/FieldUtils/Currency/CurrencyConverter.php new file mode 100644 index 0000000000..f00f72ae47 --- /dev/null +++ b/application/Espo/Core/FieldUtils/Currency/CurrencyConverter.php @@ -0,0 +1,111 @@ +configDataProvider = $configDataProvider; + } + + /** + * Convert a currency value to a specific currency. + * + * @throws RuntimeException + */ + public function convert(CurrencyValue $value, string $targetCurrencyCode) : CurrencyValue + { + $amount = $value->getAmount(); + + if (!$this->configDataProvider->hasCurrency($targetCurrencyCode)) { + throw new RuntimeException("Can't convert currency to unknown currency '{$targetCurrencyCode}."); + } + + $rate = $this->configDataProvider->getCurrencyRate($value->getCode()); + + $targetRate = $this->configDataProvider->getCurrencyRate($targetCurrencyCode); + + $amount *= $rate; + + $amount /= $targetRate; + + return new CurrencyValue($amount, $targetCurrencyCode); + } + + /** + * Convert a currency value to the system default currency. + */ + public function convertToDefault(CurrencyValue $value) : CurrencyValue + { + $targetCurrencyCode = $this->configDataProvider->getDefaultCurrency(); + + return $this->convert($value, $targetCurrencyCode); + } + + /** + * Convert a currency value to a specific currency with specific rates. + * + * @throws RuntimeException + */ + public function convertWithRates( + CurrencyValue $value, string $targetCurrencyCode, CurrencyRates $rates + ) : CurrencyValue { + + $amount = $value->getAmount(); + + $currencyCode = $value->getCode(); + + if (!$rates->hasRate($currencyCode)) { + throw new RuntimeException("No rate for the currency '{$currencyCode}."); + } + + if (!$rates->hasRate($targetCurrencyCode)) { + throw new RuntimeException("No rate for the currency '{$targetCurrencyCode}."); + } + + $rate = $rates->getRate($currencyCode); + + $targetRate = $rates->getRate($targetCurrencyCode); + + $amount *= $rate; + + $amount /= $targetRate; + + return new CurrencyValue($amount, $targetCurrencyCode); + } +} diff --git a/application/Espo/Core/FieldUtils/Currency/CurrencyRates.php b/application/Espo/Core/FieldUtils/Currency/CurrencyRates.php new file mode 100644 index 0000000000..b7a5909b62 --- /dev/null +++ b/application/Espo/Core/FieldUtils/Currency/CurrencyRates.php @@ -0,0 +1,67 @@ +data); + } + + public function getRate(string $currencyCode) : float + { + if (!$this->hasRate($currencyCode)) { + throw new RuntimeException("No currency rate for '{$currencyCode}'."); + } + + return $this->data[$currencyCode]; + } + + public static function fromArray(array $data) : self + { + $obj = new self(); + + $obj->data = $data; + + return $obj; + } +} diff --git a/application/Espo/Core/FieldUtils/Currency/CurrencyValue.php b/application/Espo/Core/FieldUtils/Currency/CurrencyValue.php new file mode 100644 index 0000000000..57f3239074 --- /dev/null +++ b/application/Espo/Core/FieldUtils/Currency/CurrencyValue.php @@ -0,0 +1,134 @@ +amount = $amount; + $this->code = $code; + } + + /** + * Get an amount. + */ + public function getAmount() : float + { + return $this->amount; + } + + /** + * Get a currency code. + */ + public function getCode() : string + { + return $this->code; + } + + /** + * Add a currency value. + */ + public function add(self $value) : self + { + $amount = $this->getAmount(); + + if ($this->getCode() !== $value->getCode()) { + throw new RuntimeException("Can't add a currency value with a different code."); + } + + $amount += $value->getAmount(); + + return new self($amount, $this->getCode()); + } + + /** + * Subtract a currency value. + */ + public function subtract(self $value) : self + { + $amount = $this->getAmount(); + + if ($this->getCode() !== $value->getCode()) { + throw new RuntimeException("Can't substract a currency value with a different code."); + } + + $amount -= $value->getAmount(); + + return new self($amount, $this->getCode()); + } + + /** + * Multiply by a multiplier. + */ + public function multiply(float $multiplier) : self + { + $amount = $this->getAmount(); + + $amount *= $multiplier; + + return new self($amount, $this->getCode()); + } + + /** + * Divide by a divider. + */ + public function divide(float $divider) : self + { + $amount = $this->getAmount(); + + $amount /= $divider; + + return new self($amount, $this->getCode()); + } + + /** + * Round with a precision. + */ + public function round(int $precision = 0) : self + { + $amount = round($this->getAmount(), $precision); + + return new self($amount, $this->getCode()); + } +} diff --git a/tests/unit/Espo/Core/FieldUtils/Currency/CurrencyConfigDataProviderTest.php b/tests/unit/Espo/Core/FieldUtils/Currency/CurrencyConfigDataProviderTest.php new file mode 100644 index 0000000000..7744f61655 --- /dev/null +++ b/tests/unit/Espo/Core/FieldUtils/Currency/CurrencyConfigDataProviderTest.php @@ -0,0 +1,139 @@ +config = $this->createMock(Config::class); + + $this->provider = new CurrencyConfigDataProvider($this->config); + } + + public function testDefaultCurrency() + { + $this->config + ->expects($this->once()) + ->method('get') + ->with('defaultCurrency') + ->willReturn('USD'); + + $currency = $this->provider->getDefaultCurrency(); + + $this->assertEquals('USD', $currency); + } + + public function testBaseCurrency() + { + $this->config + ->expects($this->once()) + ->method('get') + ->with('baseCurrency') + ->willReturn('USD'); + + $currency = $this->provider->getBaseCurrency(); + + $this->assertEquals('USD', $currency); + } + + public function testCurrencyList() + { + $this->config + ->expects($this->once()) + ->method('get') + ->with('currencyList') + ->willReturn(['USD', 'EUR']); + + $result = $this->provider->getCurrencyList(); + + $this->assertEquals(['USD', 'EUR'], $result); + } + + public function testHasCurrency() + { + $this->config + ->expects($this->once()) + ->method('get') + ->with('currencyList') + ->willReturn(['USD', 'EUR']); + + $result = $this->provider->hasCurrency('EUR'); + + $this->assertTrue($result); + } + + public function testCurrencyRate1() + { + $this->config + ->expects($this->at(0)) + ->method('get') + ->with('currencyRates') + ->willReturn([ + 'EUR' => 1.2, + ]); + + $this->config + ->expects($this->at(1)) + ->method('get') + ->with('currencyList') + ->willReturn(['USD', 'EUR']); + + $result = $this->provider->getCurrencyRate('EUR'); + + $this->assertEquals(1.2, $result); + } + + public function testCurrencyRate2() + { + $this->config + ->expects($this->at(0)) + ->method('get') + ->with('currencyRates') + ->willReturn([ + 'EUR' => 1.2, + ]); + + $this->config + ->expects($this->at(1)) + ->method('get') + ->with('currencyList') + ->willReturn(['USD', 'EUR']); + + $result = $this->provider->getCurrencyRate('USD'); + + $this->assertEquals(1.0, $result); + } +} diff --git a/tests/unit/Espo/Core/FieldUtils/Currency/CurrencyConverterTest.php b/tests/unit/Espo/Core/FieldUtils/Currency/CurrencyConverterTest.php new file mode 100644 index 0000000000..a0d570f9ab --- /dev/null +++ b/tests/unit/Espo/Core/FieldUtils/Currency/CurrencyConverterTest.php @@ -0,0 +1,159 @@ +createMock(CurrencyConfigDataProvider::class); + + $currencyConfigDataProvider + ->expects($this->any()) + ->method('hasCurrency') + ->with('EUR') + ->willReturn(true); + + $currencyConfigDataProvider + ->expects($this->any()) + ->method('getCurrencyRate') + ->willReturnMap([ + ['USD', 1.0], + ['EUR', 1.2], + ]); + + $value = new CurrencyValue(2.0, 'USD'); + + $converer = new CurrencyConverter($currencyConfigDataProvider); + + $convertedValue = $converer->convert($value, 'EUR'); + + $this->assertEquals('EUR', $convertedValue->getCode()); + + $this->assertEquals(2.0 / 1.2, $convertedValue->getAmount()); + } + + public function testConvert2() + { + $currencyConfigDataProvider = $this->createMock(CurrencyConfigDataProvider::class); + + $currencyConfigDataProvider + ->expects($this->any()) + ->method('hasCurrency') + ->with('EUR') + ->willReturn(true); + + $currencyConfigDataProvider + ->expects($this->any()) + ->method('getCurrencyRate') + ->willReturnMap([ + ['USD', 1.0], + ['EUR', 1.2], + ['UAH', 0.035], + ]); + + $value = new CurrencyValue(2.0, 'UAH'); + + $converer = new CurrencyConverter($currencyConfigDataProvider); + + $convertedValue = $converer->convert($value, 'EUR'); + + $this->assertEquals('EUR', $convertedValue->getCode()); + + $this->assertEquals(2.0 * 0.035 / 1.2, $convertedValue->getAmount()); + } + + public function testConvertToDefault() + { + $currencyConfigDataProvider = $this->createMock(CurrencyConfigDataProvider::class); + + $currencyConfigDataProvider + ->expects($this->any()) + ->method('getDefaultCurrency') + ->willReturn('USD'); + + $currencyConfigDataProvider + ->expects($this->any()) + ->method('hasCurrency') + ->with('USD') + ->willReturn(true); + + $currencyConfigDataProvider + ->expects($this->any()) + ->method('getCurrencyRate') + ->willReturnMap([ + ['USD', 1.0], + ['EUR', 1.2], + ]); + + $value = new CurrencyValue(2.0, 'EUR'); + + $converer = new CurrencyConverter($currencyConfigDataProvider); + + $convertedValue = $converer->convertToDefault($value); + + $this->assertEquals('USD', $convertedValue->getCode()); + + $this->assertEquals(2.0 * 1.2, $convertedValue->getAmount()); + } + + public function testConvertWithRates() + { + $currencyConfigDataProvider = $this->createMock(CurrencyConfigDataProvider::class); + + $rates = CurrencyRates::fromArray([ + 'USD' => 1.0, + 'EUR' => 1.2, + 'UAH' => 0.035, + ]); + + $value = new CurrencyValue(2.0, 'UAH'); + + $converer = new CurrencyConverter($currencyConfigDataProvider); + + $convertedValue = $converer->convertWithRates($value, 'EUR', $rates); + + $this->assertEquals('EUR', $convertedValue->getCode()); + + $this->assertEquals(2.0 * 0.035 / 1.2, $convertedValue->getAmount()); + } +} diff --git a/tests/unit/Espo/Core/FieldUtils/Currency/CurrencyValueTest.php b/tests/unit/Espo/Core/FieldUtils/Currency/CurrencyValueTest.php new file mode 100644 index 0000000000..74db431231 --- /dev/null +++ b/tests/unit/Espo/Core/FieldUtils/Currency/CurrencyValueTest.php @@ -0,0 +1,117 @@ +assertEquals(2.0, $value->getAmount()); + + $this->assertEquals('USD', $value->getCode()); + } + + public function testAdd() + { + $value = (new CurrencyValue(2.0, 'USD'))->add( + new CurrencyValue(1.0, 'USD') + ); + + $this->assertEquals(3.0, $value->getAmount()); + + $this->assertEquals('USD', $value->getCode()); + } + + public function testSubtract() + { + $value = (new CurrencyValue(2.0, 'USD'))->subtract( + new CurrencyValue(3.0, 'USD') + ); + + $this->assertEquals(-1.0, $value->getAmount()); + + $this->assertEquals('USD', $value->getCode()); + } + + public function testMultiply() + { + $value = (new CurrencyValue(2.0, 'USD'))->multiply(3.0); + + $this->assertEquals(6.0, $value->getAmount()); + + $this->assertEquals('USD', $value->getCode()); + } + + public function testDivide() + { + $value = (new CurrencyValue(6.0, 'USD'))->divide(3.0); + + $this->assertEquals(2.0, $value->getAmount()); + + $this->assertEquals('USD', $value->getCode()); + } + + public function testRound() + { + $value = (new CurrencyValue(2.306, 'USD'))->round(2); + + $this->assertEquals(2.31, $value->getAmount()); + + $this->assertEquals('USD', $value->getCode()); + } + + public function testBadAdd() + { + $this->expectException(RuntimeException::class); + + (new CurrencyValue(2.0, 'USD'))->add( + new CurrencyValue(1.0, 'EUR') + ); + } + + public function testGetBadCode() + { + $this->expectException(RuntimeException::class); + + new CurrencyValue(2.0, ''); + } +} From d0273febf9870f00884f03504d6ae88f5c7d2cb8 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 24 Jan 2021 17:34:58 +0200 Subject: [PATCH 08/10] Rename Address class --- .../Espo/Classes/AddressFormatters/Formatter1.php | 4 ++-- .../Espo/Classes/AddressFormatters/Formatter2.php | 4 ++-- .../Espo/Classes/AddressFormatters/Formatter3.php | 4 ++-- .../Espo/Classes/AddressFormatters/Formatter4.php | 4 ++-- .../Core/FieldUtils/Address/AddressBuilder.php | 6 +++--- .../Core/FieldUtils/Address/AddressFormatter.php | 2 +- .../Address/{Address.php => AddressValue.php} | 4 ++-- application/Espo/Tools/Export/Formats/Xlsx.php | 4 ++-- .../FieldUtils/Address/AddressFormatterTest.php | 4 ++-- .../FieldUtils/Address/AddressFormattersTest.php | 12 ++++++------ .../{AddressTest.php => AddressValueTest.php} | 14 +++++++------- 11 files changed, 31 insertions(+), 31 deletions(-) rename application/Espo/Core/FieldUtils/Address/{Address.php => AddressValue.php} (99%) rename tests/unit/Espo/Core/FieldUtils/Address/{AddressTest.php => AddressValueTest.php} (91%) diff --git a/application/Espo/Classes/AddressFormatters/Formatter1.php b/application/Espo/Classes/AddressFormatters/Formatter1.php index 2a1050e15e..96faf311cf 100644 --- a/application/Espo/Classes/AddressFormatters/Formatter1.php +++ b/application/Espo/Classes/AddressFormatters/Formatter1.php @@ -31,12 +31,12 @@ namespace Espo\Classes\AddressFormatters; use Espo\Core\FieldUtils\Address\{ AddressFormatter, - Address, + AddressValue, }; class Formatter1 implements AddressFormatter { - public function format(Address $address) : string + public function format(AddressValue $address) : string { $result = ''; diff --git a/application/Espo/Classes/AddressFormatters/Formatter2.php b/application/Espo/Classes/AddressFormatters/Formatter2.php index e9093c9a63..6cc9bc2e8d 100644 --- a/application/Espo/Classes/AddressFormatters/Formatter2.php +++ b/application/Espo/Classes/AddressFormatters/Formatter2.php @@ -31,12 +31,12 @@ namespace Espo\Classes\AddressFormatters; use Espo\Core\FieldUtils\Address\{ AddressFormatter, - Address, + AddressValue, }; class Formatter2 implements AddressFormatter { - public function format(Address $address) : string + public function format(AddressValue $address) : string { $result = ''; diff --git a/application/Espo/Classes/AddressFormatters/Formatter3.php b/application/Espo/Classes/AddressFormatters/Formatter3.php index 0514a03ac7..30e70ec71f 100644 --- a/application/Espo/Classes/AddressFormatters/Formatter3.php +++ b/application/Espo/Classes/AddressFormatters/Formatter3.php @@ -31,12 +31,12 @@ namespace Espo\Classes\AddressFormatters; use Espo\Core\FieldUtils\Address\{ AddressFormatter, - Address, + AddressValue, }; class Formatter3 implements AddressFormatter { - public function format(Address $address) : string + public function format(AddressValue $address) : string { $result = ''; diff --git a/application/Espo/Classes/AddressFormatters/Formatter4.php b/application/Espo/Classes/AddressFormatters/Formatter4.php index 9282179012..f5e0ba12b2 100644 --- a/application/Espo/Classes/AddressFormatters/Formatter4.php +++ b/application/Espo/Classes/AddressFormatters/Formatter4.php @@ -31,12 +31,12 @@ namespace Espo\Classes\AddressFormatters; use Espo\Core\FieldUtils\Address\{ AddressFormatter, - Address, + AddressValue, }; class Formatter4 implements AddressFormatter { - public function format(Address $address) : string + public function format(AddressValue $address) : string { $result = ''; diff --git a/application/Espo/Core/FieldUtils/Address/AddressBuilder.php b/application/Espo/Core/FieldUtils/Address/AddressBuilder.php index f3b59a4880..d9f8de6012 100644 --- a/application/Espo/Core/FieldUtils/Address/AddressBuilder.php +++ b/application/Espo/Core/FieldUtils/Address/AddressBuilder.php @@ -44,7 +44,7 @@ class AddressBuilder protected $portalCode; - public function clone(Address $address) : self + public function clone(AddressValue $address) : self { $this->setStreet($address->getStreet()); $this->setCity($address->getCity()); @@ -90,9 +90,9 @@ class AddressBuilder return $this; } - public function build() : Address + public function build() : AddressValue { - return Address::fromRaw([ + return AddressValue::fromRaw([ 'street' => $this->street, 'city' => $this->city, 'country' => $this->country, diff --git a/application/Espo/Core/FieldUtils/Address/AddressFormatter.php b/application/Espo/Core/FieldUtils/Address/AddressFormatter.php index afcfc8e104..cacaf24c92 100644 --- a/application/Espo/Core/FieldUtils/Address/AddressFormatter.php +++ b/application/Espo/Core/FieldUtils/Address/AddressFormatter.php @@ -34,5 +34,5 @@ namespace Espo\Core\FieldUtils\Address; */ interface AddressFormatter { - public function format(Address $address) : string; + public function format(AddressValue $address) : string; } diff --git a/application/Espo/Core/FieldUtils/Address/Address.php b/application/Espo/Core/FieldUtils/Address/AddressValue.php similarity index 99% rename from application/Espo/Core/FieldUtils/Address/Address.php rename to application/Espo/Core/FieldUtils/Address/AddressValue.php index 433ddb4b18..30b1e07a69 100644 --- a/application/Espo/Core/FieldUtils/Address/Address.php +++ b/application/Espo/Core/FieldUtils/Address/AddressValue.php @@ -34,9 +34,9 @@ use Espo\{ }; /** - * An address. + * An address value. */ -class Address +class AddressValue { protected $street = null; diff --git a/application/Espo/Tools/Export/Formats/Xlsx.php b/application/Espo/Tools/Export/Formats/Xlsx.php index e8a841649e..5e74f3617f 100644 --- a/application/Espo/Tools/Export/Formats/Xlsx.php +++ b/application/Espo/Tools/Export/Formats/Xlsx.php @@ -40,7 +40,7 @@ use Espo\Core\{ FileStorage\Manager as FileStorageManager, Utils\File\Manager as FileManager, ORM\EntityManager, - FieldUtils\Address\Address, + FieldUtils\Address\AddressValue, FieldUtils\Address\AddressFormatterFactory, }; @@ -523,7 +523,7 @@ class Xlsx } } else if ($type == 'address') { - $address = Address::createBuilder() + $address = AddressValue::createBuilder() ->setStreet($row[$name . 'Street'] ?? null) ->setCity($row[$name . 'City'] ?? null) ->setState($row[$name . 'State'] ?? null) diff --git a/tests/integration/Espo/Core/FieldUtils/Address/AddressFormatterTest.php b/tests/integration/Espo/Core/FieldUtils/Address/AddressFormatterTest.php index fed9def420..d814eb2789 100644 --- a/tests/integration/Espo/Core/FieldUtils/Address/AddressFormatterTest.php +++ b/tests/integration/Espo/Core/FieldUtils/Address/AddressFormatterTest.php @@ -31,7 +31,7 @@ namespace tests\integration\Espo\Core\FieldUtils\Address; use Espo\Core\FieldUtils\Address\{ AddressFormatterFactory, - Address, + AddressValue, }; class AddressFormatterTest extends \tests\integration\Core\BaseTestCase @@ -42,7 +42,7 @@ class AddressFormatterTest extends \tests\integration\Core\BaseTestCase $formatter = $formatterFactory->create(1); - $address = Address::createBuilder() + $address = AddressValue::createBuilder() ->setStreet('street') ->setCity('city') ->setCountry('country') diff --git a/tests/unit/Espo/Core/FieldUtils/Address/AddressFormattersTest.php b/tests/unit/Espo/Core/FieldUtils/Address/AddressFormattersTest.php index ee436c9134..92bbf2fa4b 100644 --- a/tests/unit/Espo/Core/FieldUtils/Address/AddressFormattersTest.php +++ b/tests/unit/Espo/Core/FieldUtils/Address/AddressFormattersTest.php @@ -30,7 +30,7 @@ namespace tests\unit\Espo\Core\FieldUtils\Address; use Espo\Core\{ - FieldUtils\Address\Address, + FieldUtils\Address\AddressValue, }; use Espo\Classes\{ @@ -49,7 +49,7 @@ class AddressFormattersTest extends \PHPUnit\Framework\TestCase public function testFormat1All() { - $address = Address::createBuilder() + $address = AddressValue::createBuilder() ->setStreet('street') ->setCity('city') ->setCountry('country') @@ -71,7 +71,7 @@ class AddressFormattersTest extends \PHPUnit\Framework\TestCase public function testFormat1NoState() { - $address = Address::createBuilder() + $address = AddressValue::createBuilder() ->setStreet('street') ->setCity('city') ->setCountry('country') @@ -93,7 +93,7 @@ class AddressFormattersTest extends \PHPUnit\Framework\TestCase public function testFormat2All() { - $address = Address::createBuilder() + $address = AddressValue::createBuilder() ->setStreet('street') ->setCity('city') ->setCountry('country') @@ -115,7 +115,7 @@ class AddressFormattersTest extends \PHPUnit\Framework\TestCase public function testFormat3All() { - $address = Address::createBuilder() + $address = AddressValue::createBuilder() ->setStreet('street') ->setCity('city') ->setCountry('country') @@ -137,7 +137,7 @@ class AddressFormattersTest extends \PHPUnit\Framework\TestCase public function testFormat4All() { - $address = Address::createBuilder() + $address = AddressValue::createBuilder() ->setStreet('street') ->setCity('city') ->setCountry('country') diff --git a/tests/unit/Espo/Core/FieldUtils/Address/AddressTest.php b/tests/unit/Espo/Core/FieldUtils/Address/AddressValueTest.php similarity index 91% rename from tests/unit/Espo/Core/FieldUtils/Address/AddressTest.php rename to tests/unit/Espo/Core/FieldUtils/Address/AddressValueTest.php index 7694dcc070..f1ae8eddc2 100644 --- a/tests/unit/Espo/Core/FieldUtils/Address/AddressTest.php +++ b/tests/unit/Espo/Core/FieldUtils/Address/AddressValueTest.php @@ -30,12 +30,12 @@ namespace tests\unit\Espo\Core\FieldUtils\Address; use Espo\Core\{ - FieldUtils\Address\Address, + FieldUtils\Address\AddressValue, }; use Espo\ORM\Entity; -class AddressTest extends \PHPUnit\Framework\TestCase +class AddressValueTest extends \PHPUnit\Framework\TestCase { protected function setUp() : void { @@ -44,7 +44,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase public function testAddress1() { - $address = Address::createBuilder() + $address = AddressValue::createBuilder() ->setStreet('street') ->setCity('city') ->setCountry('country') @@ -61,7 +61,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase public function testBuilderClone() { - $addressOriginal = Address::createBuilder() + $addressOriginal = AddressValue::createBuilder() ->setStreet('street') ->setCity('city') ->setCountry('country') @@ -69,7 +69,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase ->setPostalCode('postalCode') ->build(); - $address = Address::createBuilder() + $address = AddressValue::createBuilder() ->clone($addressOriginal) ->build(); @@ -82,7 +82,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase public function testAddressWith() { - $addressOriginal = Address::createBuilder() + $addressOriginal = AddressValue::createBuilder() ->setStreet('street') ->setCity('city') ->setCountry('country') @@ -111,7 +111,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase ['addressPostalCode', null], ]); - $address = Address::fromEntity($entity, 'address'); + $address = AddressValue::fromEntity($entity, 'address'); $this->assertEquals('street', $address->getStreet()); $this->assertEquals('city', $address->getCity()); From 9fe61d234222ef3e392b9bab9036adc61051c794 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Mon, 25 Jan 2021 11:12:38 +0200 Subject: [PATCH 09/10] v --- package-lock.json | 2 +- package.json | 64 +++++++++++++++++++++++------------------------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index aca6cbfc7e..acdd8607e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "espocrm", - "version": "6.0.9", + "version": "6.0.10", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4004600dd7..4310f3c305 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,32 @@ -{ - "name": "espocrm", - "version": "6.0.9", - "description": "", - "main": "index.php", - "repository": { - "type": "git", - "url": "git://github.com/espocrm/espocrm.git" - }, - "author": "", - "license": "GPL-3.0", - "devDependencies": { - "archiver": "^3.1.1", - "fstream": ">=1.0.12", - "grunt": "^1.3.0", - "grunt-chmod": "~1.1.0", - "grunt-contrib-clean": "~2.0.0", - "grunt-contrib-concat": "~1.0.1", - "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-cssmin": "~3.0.0", - "grunt-contrib-less": "^2.0.0", - "grunt-contrib-uglify": "~4.0.0", - "grunt-mkdir": "~1.0.0", - "grunt-replace": "~1.0.1", - "js-yaml": "^3.13.1", - "lodash": "^4.17.20", - "minimist": ">=1.2.2", - "pofile": "~1.0.11", - "tar": "^6.0.5" - }, - "dependencies": {} -} +{ + "name": "espocrm", + "version": "6.0.10", + "description": "", + "main": "index.php", + "repository": { + "type": "git", + "url": "git://github.com/espocrm/espocrm.git" + }, + "author": "", + "license": "GPL-3.0", + "devDependencies": { + "archiver": "^3.1.1", + "fstream": ">=1.0.12", + "grunt": "^1.3.0", + "grunt-chmod": "~1.1.0", + "grunt-contrib-clean": "~2.0.0", + "grunt-contrib-concat": "~1.0.1", + "grunt-contrib-copy": "~1.0.0", + "grunt-contrib-cssmin": "~3.0.0", + "grunt-contrib-less": "^2.0.0", + "grunt-contrib-uglify": "~4.0.0", + "grunt-mkdir": "~1.0.0", + "grunt-replace": "~1.0.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.20", + "minimist": ">=1.2.2", + "pofile": "~1.0.11", + "tar": "^6.0.5" + }, + "dependencies": {} +} From 4d755d3795fa30548fc09010091858227b33cfa5 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Mon, 25 Jan 2021 11:46:56 +0200 Subject: [PATCH 10/10] fix tests --- .../Espo/Extension/GeneralTest.php | 2 +- .../testData/Extension/General.zip | Bin 5833 -> 6179 bytes .../integration/testData/Upgrade/General.zip | Bin 1213 -> 1307 bytes 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/Espo/Extension/GeneralTest.php b/tests/integration/Espo/Extension/GeneralTest.php index c14634bc05..7c8e85ef8a 100644 --- a/tests/integration/Espo/Extension/GeneralTest.php +++ b/tests/integration/Espo/Extension/GeneralTest.php @@ -27,7 +27,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace tests\integration\Espo\Extensions; +namespace tests\integration\Espo\Extension; class GeneralTest extends \tests\integration\Core\BaseTestCase { diff --git a/tests/integration/testData/Extension/General.zip b/tests/integration/testData/Extension/General.zip index 58549ef4131729eb54dcbcbc37d05bd7e6def947..047f086840725208c43cbf8735427a2dc3ea782e 100644 GIT binary patch literal 6179 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 YlmP;~S=m6^*?`c2iGkrL5Hm0U0HY5|q5uE@ literal 1213 zcmWIWW@h1H00Eoyd`~a~O0Y4=Fr;PXq!#OkhHx@4o4nGB*ep3SsjBCF)f0Kbg%lkiEhaxv# zeR0r*->xsN=VOvhh{TbOEjPt#j{eWf4D>r&XFT_2EqkKG)JJU@Qf}56!e6D5=AD}8 zHP>^=hYf5myLF_`Np`y%evVP$oFcfKsrvneYegHnu0GpbcxKH-KlgMs5dkUf#F&E< zK6=joY$v}xa_UJ{(^FB}JMZM#_$?IPrE&S%{{+TnRwK^{udi>SvZt(Ea`3dBsjKt!<*&JJnf~ARp!yG{0B=SnIc8i5Qvw(sARxf-*AYa+k|`@B znWBX-vRRmk6xl2XxLNQd3p5gxWbqh@nP8EPY)3Z|ntXxgf|4&Db1@SyvbmSBnhQ_L zghK)|Aw!G@CNqX5jcPy&5~|RU06QCMJR(^Gt-+F{Apwpn2_vl8(l{At4O)T*8VO3! X7y-k|26Pw$8xRUGF)&OAVg?2P)^#nc