diff --git a/application/Espo/Classes/AddressFormatters/Formatter1.php b/application/Espo/Classes/AddressFormatters/Formatter1.php new file mode 100644 index 0000000000..96faf311cf --- /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..6cc9bc2e8d --- /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..30e70ec71f --- /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..f5e0ba12b2 --- /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/Console/Commands/Upgrade.php b/application/Espo/Core/Console/Commands/Upgrade.php index bbd920d142..dc976b2fc1 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,8 +289,11 @@ class Upgrade implements Command $upgradeManager = $this->getUpgradeManager(true); $upgradeManager->runInstallStep($stepName, ['id' => $upgradeId]); } - } catch (Exception $e) { - $GLOBALS['log']->error('Upgrade Error: ' . $e->getMessage()); + } catch (Throwable $e) { + try { + $GLOBALS['log']->error('Upgrade Error: ' . $e->getMessage()); + } + catch (Throwable $t) {} throw new Error($e->getMessage()); } @@ -308,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/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)) { diff --git a/application/Espo/Core/FieldUtils/Address/AddressBuilder.php b/application/Espo/Core/FieldUtils/Address/AddressBuilder.php new file mode 100644 index 0000000000..d9f8de6012 --- /dev/null +++ b/application/Espo/Core/FieldUtils/Address/AddressBuilder.php @@ -0,0 +1,103 @@ +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() : AddressValue + { + return AddressValue::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..cacaf24c92 --- /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/Core/FieldUtils/Address/AddressValue.php b/application/Espo/Core/FieldUtils/Address/AddressValue.php new file mode 100644 index 0000000000..30b1e07a69 --- /dev/null +++ b/application/Espo/Core/FieldUtils/Address/AddressValue.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/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/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 8a684f676d..e2d00005e8 100644 --- a/application/Espo/Core/Upgrades/Actions/Base/Install.php +++ b/application/Espo/Core/Upgrades/Actions/Base/Install.php @@ -148,19 +148,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.'); } diff --git a/application/Espo/Resources/metadata/app/addressFormats.json b/application/Espo/Resources/metadata/app/addressFormats.json new file mode 100644 index 0000000000..9add753f77 --- /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" + } +} 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..5e74f3617f 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\AddressValue, + 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 = AddressValue::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/package-lock.json b/package-lock.json index efbf9b29fd..b66e4e7961 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": {} +} 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..d814eb2789 --- /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 = AddressValue::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/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 58549ef413..047f086840 100644 Binary files a/tests/integration/testData/Extension/General.zip and b/tests/integration/testData/Extension/General.zip differ diff --git a/tests/integration/testData/Upgrade/General.zip b/tests/integration/testData/Upgrade/General.zip index e7660a84c2..79e9322d30 100644 Binary files a/tests/integration/testData/Upgrade/General.zip and b/tests/integration/testData/Upgrade/General.zip differ 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..92bbf2fa4b --- /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 = AddressValue::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 = AddressValue::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 = AddressValue::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 = AddressValue::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/AddressValueTest.php b/tests/unit/Espo/Core/FieldUtils/Address/AddressValueTest.php new file mode 100644 index 0000000000..f1ae8eddc2 --- /dev/null +++ b/tests/unit/Espo/Core/FieldUtils/Address/AddressValueTest.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 = AddressValue::createBuilder() + ->setStreet('street') + ->setCity('city') + ->setCountry('country') + ->setState('state') + ->setPostalCode('postalCode') + ->build(); + + $address = AddressValue::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 = AddressValue::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 = AddressValue::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()); + } +} 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, ''); + } +}