diff --git a/application/Espo/Classes/FieldSanitizers/Decimal.php b/application/Espo/Classes/FieldSanitizers/Decimal.php
new file mode 100644
index 0000000000..6d49e7f9e8
--- /dev/null
+++ b/application/Espo/Classes/FieldSanitizers/Decimal.php
@@ -0,0 +1,55 @@
+.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of this program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU Affero General Public License version 3.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
+ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
+ ************************************************************************/
+
+namespace Espo\Classes\FieldSanitizers;
+
+use Espo\Core\FieldSanitize\Sanitizer;
+use Espo\Core\FieldSanitize\Sanitizer\Data;
+
+class Decimal implements Sanitizer
+{
+ public function sanitize(Data $data, string $field): void
+ {
+ if (!$data->has($field)) {
+ return;
+ }
+
+ $value = $data->get($field);
+
+ if (is_string($value) || $value === null) {
+ return;
+ }
+
+ if (is_float($value) || is_int($value)) {
+ $value = (string) $value;
+ }
+
+ $data->set($field, $value);
+ }
+}
diff --git a/application/Espo/Classes/FieldValidators/DecimalType.php b/application/Espo/Classes/FieldValidators/DecimalType.php
new file mode 100644
index 0000000000..7f31b077fc
--- /dev/null
+++ b/application/Espo/Classes/FieldValidators/DecimalType.php
@@ -0,0 +1,103 @@
+.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of this program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU Affero General Public License version 3.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
+ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
+ ************************************************************************/
+
+namespace Espo\Classes\FieldValidators;
+
+use Espo\Core\Currency\CalculatorUtil;
+use Espo\ORM\Entity;
+use stdClass;
+
+/**
+ * @noinspection PhpUnused
+ */
+class DecimalType
+{
+ public function __construct()
+ {}
+
+ public function checkRequired(Entity $entity, string $field): bool
+ {
+ return $this->isNotEmpty($entity, $field);
+ }
+
+ /**
+ * @param mixed $validationValue
+ * @noinspection PhpUnused
+ */
+ public function checkMax(Entity $entity, string $field, $validationValue): bool
+ {
+ if (!$this->isNotEmpty($entity, $field)) {
+ return true;
+ }
+
+ return CalculatorUtil::compare($entity->get($field), $validationValue) <= 0;
+ }
+
+ /**
+ * @param mixed $validationValue
+ * @noinspection PhpUnused
+ */
+ public function checkMin(Entity $entity, string $field, $validationValue): bool
+ {
+ if (!$this->isNotEmpty($entity, $field)) {
+ return true;
+ }
+
+ return CalculatorUtil::compare($entity->get($field), $validationValue) >= 0;
+ }
+
+ /** @noinspection PhpUnused */
+ public function rawCheckValid(stdClass $data, string $field): bool
+ {
+ $value = $data->$field ?? null;
+
+ if ($value === null) {
+ return true;
+ }
+
+ if (!is_string($value)) {
+ return false;
+ }
+
+ if ($value === '') {
+ return false;
+ }
+
+ if (preg_match('/^-?[0-9]+\.?[0-9]*$/', $value)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function isNotEmpty(Entity $entity, string $field): bool
+ {
+ return $entity->has($field) && $entity->get($field) !== null;
+ }
+}
diff --git a/application/Espo/Core/ORM/Type/FieldType.php b/application/Espo/Core/ORM/Type/FieldType.php
index 88c6958b7d..5b442f5d9f 100644
--- a/application/Espo/Core/ORM/Type/FieldType.php
+++ b/application/Espo/Core/ORM/Type/FieldType.php
@@ -64,4 +64,9 @@ class FieldType
public const JSON_ARRAY = 'jsonArray';
public const JSON_OBJECT = 'jsonObject';
public const PASSWORD = 'password';
+
+ /**
+ * @since 9.3.0
+ */
+ public const DECIMAL = 'decimal';
}
diff --git a/application/Espo/Core/Utils/Database/Orm/FieldConverters/Decimal.php b/application/Espo/Core/Utils/Database/Orm/FieldConverters/Decimal.php
new file mode 100644
index 0000000000..b51b258a62
--- /dev/null
+++ b/application/Espo/Core/Utils/Database/Orm/FieldConverters/Decimal.php
@@ -0,0 +1,63 @@
+.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of this program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU Affero General Public License version 3.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
+ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
+ ************************************************************************/
+
+namespace Espo\Core\Utils\Database\Orm\FieldConverters;
+
+use Doctrine\DBAL\Types\Types;
+use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs;
+use Espo\Core\Utils\Database\Orm\Defs\EntityDefs;
+use Espo\Core\Utils\Database\Orm\FieldConverter;
+use Espo\ORM\Defs\FieldDefs;
+use Espo\ORM\Defs\Params\AttributeParam;
+use Espo\ORM\Defs\Params\FieldParam;
+use Espo\ORM\Type\AttributeType;
+
+class Decimal implements FieldConverter
+{
+ private const DEFAULT_PRECISION = 13;
+ private const DEFAULT_SCALE = 4;
+
+ public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs
+ {
+ $name = $fieldDefs->getName();
+
+ $dbType = $fieldDefs->getParam(FieldParam::DB_TYPE) ?? Types::DECIMAL;
+ $precision = $fieldDefs->getParam(FieldParam::PRECISION) ?? self::DEFAULT_PRECISION;
+ $scale = $fieldDefs->getParam(FieldParam::SCALE) ?? self::DEFAULT_SCALE;
+
+ $defs = AttributeDefs::create($name)
+ ->withType(AttributeType::VARCHAR)
+ ->withDbType($dbType)
+ ->withParam(AttributeParam::PRECISION, $precision)
+ ->withParam(AttributeParam::SCALE, $scale);
+
+ return EntityDefs::create()
+ ->withAttribute($defs);
+ }
+}
diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json
index d6ddfdae9b..27f84d71f5 100644
--- a/application/Espo/Resources/i18n/en_US/Admin.json
+++ b/application/Espo/Resources/i18n/en_US/Admin.json
@@ -124,6 +124,7 @@
"personName": "Person Name",
"autoincrement": "Auto-increment",
"bool": "Boolean",
+ "decimal": "Decimal",
"currency": "Currency",
"currencyConverted": "Currency (Converted)",
"date": "Date",
diff --git a/application/Espo/Resources/i18n/en_US/FieldManager.json b/application/Espo/Resources/i18n/en_US/FieldManager.json
index c1890bd94d..9858a123a2 100644
--- a/application/Espo/Resources/i18n/en_US/FieldManager.json
+++ b/application/Espo/Resources/i18n/en_US/FieldManager.json
@@ -137,6 +137,7 @@
},
"fieldInfo": {
"varchar": "A single-line text.",
+ "decimal": "A decimal number with fixed-point precision.",
"enum": "Selectbox, only one value can be selected.",
"text": "A multiline text with markdown support.",
"date": "Date w/o time.",
diff --git a/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json b/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json
index da3159d7cd..403343ef0a 100644
--- a/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json
+++ b/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json
@@ -179,6 +179,17 @@
"lessThanOrEquals"
]
},
+ "decimal": {
+ "view": "views/admin/dynamic-logic/conditions/field-types/base",
+ "typeList": [
+ "isEmpty",
+ "isNotEmpty",
+ "greaterThan",
+ "lessThan",
+ "greaterThanOrEquals",
+ "lessThanOrEquals"
+ ]
+ },
"currency": {
"view": "views/admin/dynamic-logic/conditions/field-types/base",
"typeList": [
diff --git a/application/Espo/Resources/metadata/fields/decimal.json b/application/Espo/Resources/metadata/fields/decimal.json
new file mode 100644
index 0000000000..4d1ff5d3c7
--- /dev/null
+++ b/application/Espo/Resources/metadata/fields/decimal.json
@@ -0,0 +1,60 @@
+{
+ "params": [
+ {
+ "name": "required",
+ "type": "bool",
+ "default": false
+ },
+ {
+ "name": "default",
+ "type": "decimal"
+ },
+ {
+ "name": "min",
+ "type": "decimal"
+ },
+ {
+ "name": "max",
+ "type": "decimal"
+ },
+ {
+ "name": "decimalPlaces",
+ "type": "int"
+ },
+ {
+ "name": "audited",
+ "type": "bool"
+ },
+ {
+ "name": "readOnly",
+ "type": "bool"
+ },
+ {
+ "name": "readOnlyAfterCreate",
+ "type": "bool"
+ },
+ {
+ "name": "precision",
+ "type": "int",
+ "hidden": true
+ },
+ {
+ "name": "scale",
+ "type": "int",
+ "hidden": true
+ }
+ ],
+ "filter": true,
+ "validationList": [
+ "required",
+ "min",
+ "max"
+ ],
+ "mandatoryValidationList": [
+ "valid"
+ ],
+ "sanitizerClassNameList": [
+ "Espo\\Classes\\FieldSanitizers\\Decimal"
+ ],
+ "converterClassName": "Espo\\Core\\Utils\\Database\\Orm\\FieldConverters\\Decimal"
+}
diff --git a/application/Espo/Tools/DynamicLogic/ConditionChecker.php b/application/Espo/Tools/DynamicLogic/ConditionChecker.php
index 749f827d92..4e7d218d0d 100644
--- a/application/Espo/Tools/DynamicLogic/ConditionChecker.php
+++ b/application/Espo/Tools/DynamicLogic/ConditionChecker.php
@@ -29,6 +29,7 @@
namespace Espo\Tools\DynamicLogic;
+use Espo\Core\Currency\CalculatorUtil;
use Espo\Core\Field\Date;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\DateTime\SystemClock;
@@ -214,18 +215,34 @@ class ConditionChecker
}
if ($type === Type::GreaterThan) {
+ if (is_string($setValue) || is_string($value)) {
+ return $this->compare($setValue, $value) > 0;
+ }
+
return $setValue > $value;
}
if ($type === Type::LessThan) {
+ if (is_string($setValue) || is_string($value)) {
+ return $this->compare($setValue, $value) < 0;
+ }
+
return $setValue < $value;
}
if ($type === Type::GreaterThanOrEquals) {
+ if (is_string($setValue) || is_string($value)) {
+ return $this->compare($setValue, $value) >= 0;
+ }
+
return $setValue >= $value;
}
if ($type === Type::LessThanOrEquals) {
+ if (is_string($setValue) || is_string($value)) {
+ return $this->compare($setValue, $value) <= 0;
+ }
+
return $setValue <= $value;
}
@@ -346,4 +363,20 @@ class ConditionChecker
return Date::fromDateTime($dateTime);
}
+
+
+ /**
+ * @throws BadCondition
+ */
+ private function compare(mixed $arg1, mixed $arg2): int
+ {
+ $arg1 = (string) $arg1;
+ $arg2 = (string) $arg2;
+
+ if (!is_numeric($arg1) || !is_numeric($arg2)) {
+ throw new BadCondition();
+ }
+
+ return CalculatorUtil::compare($arg1, $arg2);
+ }
}
diff --git a/client/src/views/fields/decimal.js b/client/src/views/fields/decimal.js
new file mode 100644
index 0000000000..0e60ff39ce
--- /dev/null
+++ b/client/src/views/fields/decimal.js
@@ -0,0 +1,271 @@
+/************************************************************************
+ * This file is part of EspoCRM.
+ *
+ * EspoCRM – Open Source CRM application.
+ * Copyright (C) 2014-2025 EspoCRM, Inc.
+ * Website: https://www.espocrm.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of this program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU Affero General Public License version 3.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
+ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
+ ************************************************************************/
+
+/** @module views/fields/decimal */
+
+import IntFieldView from 'views/fields/int';
+
+/**
+ * A decimal field.
+ *
+ * @extends IntFieldView
+ */
+class DecimalFieldView extends IntFieldView {
+
+ /**
+ * @typedef {Object} module:views/fields/decimal~options
+ * @property {
+ * module:views/fields/decimal~params &
+ * module:views/fields/base~params &
+ * Record
+ * } [params] Parameters.
+ */
+
+ /**
+ * @typedef {Object} module:views/fields/decimal~params
+ * @property {string} [min] A max value.
+ * @property {string} [max] A max value.
+ * @property {boolean} [required] Required.
+ * @property {boolean} [disableFormatting] Disable formatting.
+ * @property {number|null} [decimalPlaces] A number of decimal places.
+ */
+
+ /**
+ * @param {
+ * module:views/fields/float~options &
+ * module:views/fields/base~options
+ * } options Options.
+ */
+ constructor(options) {
+ super(options);
+ }
+
+ type = 'decimal'
+
+ editTemplate = 'fields/float/edit'
+
+ decimalMark = '.'
+ decimalPlacesRawValue = 10
+
+ /**
+ * @inheritDoc
+ * @type {Array<(function (): boolean)|string>}
+ */
+ validations = [
+ 'required',
+ 'range',
+ ]
+
+ /**
+ * @private
+ * @type {number|null}
+ */
+ decimalPlaces
+
+ setup() {
+ super.setup();
+
+ if (this.getPreferences().has('decimalMark')) {
+ this.decimalMark = this.getPreferences().get('decimalMark');
+ } else if (this.getConfig().has('decimalMark')) {
+ this.decimalMark = this.getConfig().get('decimalMark');
+ }
+
+ if (!this.decimalMark) {
+ this.decimalMark = '.';
+ }
+
+ if (this.decimalMark === this.thousandSeparator) {
+ this.thousandSeparator = '';
+ }
+
+ this.decimalPlaces = this.params.decimalPlaces ?? null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ setupAutoNumericOptions() {
+ // noinspection JSValidateTypes
+ this.autoNumericOptions = {
+ digitGroupSeparator: this.thousandSeparator || '',
+ decimalCharacter: this.decimalMark,
+ modifyValueOnWheel: false,
+ selectOnFocus: false,
+ decimalPlaces: this.decimalPlaces,
+ decimalPlacesRawValue: this.decimalPlacesRawValue,
+ allowDecimalPadding: true,
+ showWarnings: false,
+ formulaMode: true,
+ };
+
+ if (this.decimalPlaces === null) {
+ this.autoNumericOptions.decimalPlaces = this.decimalPlacesRawValue;
+ this.autoNumericOptions.decimalPlacesRawValue = this.decimalPlacesRawValue;
+ this.autoNumericOptions.allowDecimalPadding = false;
+ }
+ }
+
+ getValueForDisplay() {
+ const value = isNaN(this.model.get(this.name)) ? null : this.model.get(this.name);
+
+ return this.formatNumber(value);
+ }
+
+ formatNumber(value) {
+ if (this.disableFormatting) {
+ return value;
+ }
+
+ return this.formatNumberDetail(value);
+ }
+
+ /**
+ *
+ * @param {string|null} value
+ * @return {string}
+ */
+ formatNumberDetail(value) {
+ if (value === null) {
+ return '';
+ }
+
+ const decimalPlaces = this.decimalPlaces;
+
+ const parts = value.toString().split('.');
+
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandSeparator);
+
+ if (decimalPlaces === 0) {
+ return parts[0];
+ }
+
+ if (decimalPlaces) {
+ let decimalPartLength = 0;
+
+ if (parts.length > 1) {
+ parts[1] = parts[1].replace(/0+$/, '');
+
+ decimalPartLength = parts[1].length;
+ } else {
+ parts[1] = '';
+ }
+
+ if (decimalPlaces && decimalPartLength < decimalPlaces) {
+ const limit = decimalPlaces - decimalPartLength;
+
+ for (let i = 0; i < limit; i++) {
+ parts[1] += '0';
+ }
+ }
+ }
+
+ return parts.join(this.decimalMark);
+ }
+
+ /**
+ * @param {string|null} value
+ * @return {string|null}
+ */
+ parse(value) {
+ value = (value !== '') ? value : null;
+
+ if (value === null) {
+ return null;
+ }
+
+ value = value
+ .split(this.thousandSeparator)
+ .join('')
+ .split(this.decimalMark)
+ .join('.');
+
+ return value;
+ }
+
+ /**
+ * @return {Record}
+ */
+ fetch() {
+ const rawValue = this.mainInputElement?.value ?? null;
+
+ const value = this.parse(rawValue);
+
+ return {[this.name]: value};
+ }
+
+ // noinspection JSUnusedGlobalSymbols
+ validateRange() {
+ const value = this.model.get(this.name);
+
+ if (value === null) {
+ return false;
+ }
+
+ const minValue = this.params.min;
+ const maxValue = this.params.max;
+
+ if (minValue !== null && maxValue !== null) {
+ if (Number(value) < Number(minValue) || Number(value) > Number(maxValue)) {
+ const msg = this.translate('fieldShouldBeBetween', 'messages')
+ .replace('{field}', this.getLabelText())
+ .replace('{min}', minValue)
+ .replace('{max}', maxValue);
+
+ this.showValidationMessage(msg);
+
+ return true;
+ }
+ } else {
+ if (minValue !== null) {
+ if (Number(value) < Number(minValue)) {
+ const msg = this.translate('fieldShouldBeGreater', 'messages')
+ .replace('{field}', this.getLabelText())
+ .replace('{value}', minValue);
+
+ this.showValidationMessage(msg);
+
+ return true;
+ }
+ } else if (maxValue !== null) {
+ if (Number(value) > Number(maxValue)) {
+ const msg = this.translate('fieldShouldBeLess', 'messages')
+ .replace('{field}', this.getLabelText())
+ .replace('{value}', maxValue);
+ this.showValidationMessage(msg);
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+}
+
+export default DecimalFieldView;
diff --git a/schema/metadata/entityDefs.json b/schema/metadata/entityDefs.json
index 6ad4d546e8..835b6f50a4 100644
--- a/schema/metadata/entityDefs.json
+++ b/schema/metadata/entityDefs.json
@@ -600,6 +600,15 @@
]
}
}
+ },
+ {
+ "properties": {
+ "type": {
+ "anyOf": [
+ {"const": "decimal"}
+ ]
+ }
+ }
}
]
},
diff --git a/tests/integration/Espo/Record/FieldValidationTest.php b/tests/integration/Espo/Record/FieldValidationTest.php
index fafaa2f5bf..075531b736 100644
--- a/tests/integration/Espo/Record/FieldValidationTest.php
+++ b/tests/integration/Espo/Record/FieldValidationTest.php
@@ -32,6 +32,7 @@ namespace tests\integration\Espo\Record;
use Espo\Core\Application;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\FieldValidation\Type;
+use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Record\CreateParams;
use Espo\Core\Record\ServiceContainer;
use Espo\Core\Record\UpdateParams;
@@ -575,4 +576,145 @@ class FieldValidationTest extends BaseTestCase
$this->assertFalse($isThrown);
}
+
+ /**
+ * @noinspection PhpUnhandledExceptionInspection
+ */
+ public function testDecimal(): void
+ {
+ $metadata = $this->getMetadata();
+ $metadata->set('entityDefs', 'Account', [
+ 'fields' => [
+ 'test' => [
+ 'type' => FieldType::DECIMAL,
+ 'min' => '-1.0',
+ 'max' => '100.0',
+ ]
+ ],
+ ]);
+ $metadata->save();
+
+ $this->getDataManager()->rebuild();
+
+ $this->reCreateApplication();
+
+ $service = $this->getContainer()
+ ->getByClass(ServiceContainer::class)
+ ->getByClass(Account::class);
+
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-0',
+ 'test' => null,
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertFalse($isThrown);
+
+ /** @noinspection PhpConditionAlreadyCheckedInspection */
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-1',
+ 'test' => '10.0',
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertFalse($isThrown);
+
+ /** @noinspection PhpConditionAlreadyCheckedInspection */
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-2',
+ 'test' => '-1.0',
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertFalse($isThrown);
+
+ /** @noinspection PhpConditionAlreadyCheckedInspection */
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-3',
+ 'test' => '100.0',
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertFalse($isThrown);
+
+ /** @noinspection PhpConditionAlreadyCheckedInspection */
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-4',
+ 'test' => '-10.0',
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertTrue($isThrown);
+
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-5',
+ 'test' => '100.1',
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertTrue($isThrown);
+
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-6',
+ 'test' => 'abc',
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertTrue($isThrown);
+
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-7',
+ 'test' => ['test'],
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertTrue($isThrown);
+
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-8',
+ 'test' => 10.0,
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertFalse($isThrown);
+
+ /** @noinspection PhpConditionAlreadyCheckedInspection */
+ $isThrown = false;
+ try {
+ $service->create((object) [
+ 'name' => 'Test-9',
+ 'test' => 10,
+ ], CreateParams::create());
+ } catch (BadRequest) {
+ $isThrown = true;
+ }
+ $this->assertFalse($isThrown);
+ }
}
diff --git a/tests/unit/Espo/Tools/DynamicLogic/CheckerTest.php b/tests/unit/Espo/Tools/DynamicLogic/CheckerTest.php
index 380a596730..a5f96aabaa 100644
--- a/tests/unit/Espo/Tools/DynamicLogic/CheckerTest.php
+++ b/tests/unit/Espo/Tools/DynamicLogic/CheckerTest.php
@@ -1295,4 +1295,196 @@ class CheckerTest extends TestCase
$this->assertTrue($checker->check($item));
}
+
+ /**
+ * @noinspection PhpUnhandledExceptionInspection
+ */
+ public function testDecimalGreaterThan(): void
+ {
+ $map = [
+ 'value' => '1.0',
+ ];
+
+ $checker = new ConditionChecker($this->initEntity($map));
+
+ $this->assertTrue(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'greaterThan',
+ 'attribute' => 'value',
+ 'value' => '0.0',
+ ]
+ ])
+ )
+ );
+
+ $this->assertFalse(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'greaterThan',
+ 'attribute' => 'value',
+ 'value' => '1.0',
+ ]
+ ])
+ )
+ );
+
+ $this->assertFalse(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'greaterThan',
+ 'attribute' => 'value',
+ 'value' => '2.0',
+ ]
+ ])
+ )
+ );
+ }
+
+ /**
+ * @noinspection PhpUnhandledExceptionInspection
+ */
+ public function testDecimalLessThan(): void
+ {
+ $map = [
+ 'value' => '1.0',
+ ];
+
+ $checker = new ConditionChecker($this->initEntity($map));
+
+ $this->assertTrue(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'lessThan',
+ 'attribute' => 'value',
+ 'value' => '2.0',
+ ]
+ ])
+ )
+ );
+
+ $this->assertFalse(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'lessThan',
+ 'attribute' => 'value',
+ 'value' => '1.0',
+ ]
+ ])
+ )
+ );
+
+ $this->assertFalse(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'lessThan',
+ 'attribute' => 'value',
+ 'value' => '0.0',
+ ]
+ ])
+ )
+ );
+ }
+
+ /**
+ * @noinspection PhpUnhandledExceptionInspection
+ */
+ public function testDecimalGreaterThanOrEquals(): void
+ {
+ $map = [
+ 'value' => '1.0',
+ ];
+
+ $checker = new ConditionChecker($this->initEntity($map));
+
+ $this->assertTrue(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'greaterThanOrEquals',
+ 'attribute' => 'value',
+ 'value' => '0.0',
+ ]
+ ])
+ )
+ );
+
+ $this->assertTrue(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'greaterThanOrEquals',
+ 'attribute' => 'value',
+ 'value' => '1.0',
+ ]
+ ])
+ )
+ );
+
+ $this->assertFalse(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'greaterThanOrEquals',
+ 'attribute' => 'value',
+ 'value' => '2.0',
+ ]
+ ])
+ )
+ );
+ }
+
+ /**
+ * @noinspection PhpUnhandledExceptionInspection
+ */
+ public function testDecimalLessThanOrEquals(): void
+ {
+ $map = [
+ 'value' => '1',
+ ];
+
+ $checker = new ConditionChecker($this->initEntity($map));
+
+ $this->assertTrue(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'lessThanOrEquals',
+ 'attribute' => 'value',
+ 'value' => '2.0',
+ ]
+ ])
+ )
+ );
+
+ $this->assertTrue(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'lessThanOrEquals',
+ 'attribute' => 'value',
+ 'value' => '1.0',
+ ]
+ ])
+ )
+ );
+
+ $this->assertFalse(
+ $checker->check(
+ Item::fromGroupDefinition([
+ (object) [
+ 'type' => 'lessThanOrEquals',
+ 'attribute' => 'value',
+ 'value' => '0.0',
+ ]
+ ])
+ )
+ );
+ }
}