diff --git a/application/Espo/Controllers/User.php b/application/Espo/Controllers/User.php index acb1aef297..a862e344ee 100644 --- a/application/Espo/Controllers/User.php +++ b/application/Espo/Controllers/User.php @@ -106,6 +106,13 @@ class User extends \Espo\Core\Controllers\Record return $this->getService('User')->passwordChangeRequest($userName, $emailAddress, $url); } + public function postActionGenerateNewApiKey($params, $data, $request) + { + if (empty($data->id)) throw new BadRequest(); + if (!$this->getUser()->isAdmin()) throw new Forbidden(); + return $this->getRecordService()->generateNewApiKeyForEntity($data->id)->getValueMap(); + } + public function actionCreateLink($params, $data, $request) { if (!$this->getUser()->isAdmin()) throw new Forbidden(); diff --git a/application/Espo/Core/SelectManagers/Base.php b/application/Espo/Core/SelectManagers/Base.php index 668e8fbf90..a75467dfb3 100644 --- a/application/Espo/Core/SelectManagers/Base.php +++ b/application/Espo/Core/SelectManagers/Base.php @@ -843,7 +843,7 @@ class Base return $this->userTimeZone; } - public function convertDateTimeWhere($item) + public function transformDateTimeWhereItem($item) { $format = 'Y-m-d H:i:s'; @@ -997,6 +997,7 @@ class Base $dt->setTimezone(new \DateTimeZone('UTC')); $where['value'] = $dt->format($format); break; + case 'between': $where['type'] = 'between'; if (is_array($value)) { @@ -1012,11 +1013,143 @@ class Base $where['value'] = [$from, $to]; } break; + + case 'currentMonth': + case 'lastMonth': + case 'nextMonth': + $where['type'] = 'between'; + $dtFrom = new \DateTime('now', new \DateTimeZone($timeZone)); + $dtFrom = $dt->modify('first day of this month')->setTime(0, 0, 0); + + if ($type == 'lastMonth') { + $dtFrom->modify('-1 month'); + } else if ($type == 'nextMonth') { + $dtFrom->modify('+1 month'); + } + + $dtTo = clone $dtFrom; + $dtTo->modify('+1 month'); + + $dtFrom->setTimezone(new \DateTimeZone('UTC')); + $dtTo->setTimezone(new \DateTimeZone('UTC')); + + $where['value'] = [$dtFrom->format($format), $dtTo->format($format)]; + break; + + case 'currentQuarter': + case 'lastQuarter': + $where['type'] = 'between'; + $dt = new \DateTime('now', new \DateTimeZone($timeZone)); + $quarter = ceil($dt->format('m') / 3); + + $dtFrom = clone $dt; + $dtFrom->modify('first day of January this year')->setTime(0, 0, 0); + + if ($type === 'lastQuarter') { + $quarter--; + if ($quarter == 0) { + $quarter = 4; + $dtFrom->modify('-1 year'); + } + } + + $dtFrom->add(new \DateInterval('P'.(($quarter - 1) * 3).'M')); + $dtTo = clone $dtFrom; + $dtTo->add(new \DateInterval('P3M')); + $dtFrom->setTimezone(new \DateTimeZone('UTC')); + $dtTo->setTimezone(new \DateTimeZone('UTC')); + $where['value'] = [ + $dtFrom->format($format), + $dtTo->format($format) + ]; + break; + + case 'currentYear': + case 'lastYear': + $where['type'] = 'between'; + $dtFrom = new \DateTime('now', new \DateTimeZone($timeZone)); + $dtFrom->modify('first day of January this year')->setTime(0, 0, 0); + if ($type == 'lastYear') { + $dtFrom->modify('-1 year'); + } + $dtTo = clone $dtFrom; + $dtTo = $dtTo->modify('+1 year'); + $dtFrom->setTimezone(new \DateTimeZone('UTC')); + $dtTo->setTimezone(new \DateTimeZone('UTC')); + $where['value'] = [ + $dtFrom->format($format), + $dtTo->format($format) + ]; + break; + + case 'currentFiscalYear': + case 'lastFiscalYear': + $where['type'] = 'between'; + $dtToday = new \DateTime('now', new \DateTimeZone($timeZone)); + $dt = clone $dtToday; + $fiscalYearShift = $this->getConfig()->get('fiscalYearShift', 0); + $dt->modify('first day of January this year')->modify('+' . $fiscalYearShift . ' months')->setTime(0, 0, 0); + if (intval($dtToday->format('m')) < $fiscalYearShift + 1) { + $dt->modify('-1 year'); + } + if ($type === 'lastFiscalYear') { + $dt->modify('-1 year'); + } + $dtFrom = clone $dt; + $dtTo = clone $dt; + $dtTo = $dtTo->modify('+1 year'); + $dtFrom->setTimezone(new \DateTimeZone('UTC')); + $dtTo->setTimezone(new \DateTimeZone('UTC')); + $where['value'] = [ + $dtFrom->format($format), + $dtTo->format($format) + ]; + break; + + case 'currentFiscalQuarter': + case 'lastFiscalQuarter': + $where['type'] = 'between'; + $dtToday = new \DateTime('now', new \DateTimeZone($timeZone)); + $dt = clone $dtToday; + $fiscalYearShift = $this->getConfig()->get('fiscalYearShift', 0); + $dt->modify('first day of January this year')->modify('+' . $fiscalYearShift . ' months')->setTime(0, 0, 0); + $month = intval($dtToday->format('m')); + $quarterShift = floor(($month - $fiscalYearShift - 1) / 3); + if ($quarterShift) { + if ($quarterShift >= 0) { + $dt->add(new \DateInterval('P'.($quarterShift * 3).'M')); + } else { + $quarterShift *= -1; + $dt->sub(new \DateInterval('P'.($quarterShift * 3).'M')); + } + } + if ($type === 'lastFiscalQuarter') { + $dt->modify('-3 months'); + } + $dtFrom = clone $dt; + $dtTo = clone $dt; + $dtTo = $dtTo->modify('+3 months'); + $dtFrom->setTimezone(new \DateTimeZone('UTC')); + $dtTo->setTimezone(new \DateTimeZone('UTC')); + $where['value'] = [ + $dtFrom->format($format), + $dtTo->format($format) + ]; + break; + default: $where['type'] = $type; } - $result = $this->getWherePart($where); + $where['originalType'] = $type; + + return $where; + } + + public function convertDateTimeWhere($item) + { + $where = $this->transformDateTimeWhereItem($item); + $result = $this->getWherePart($where); return $result; } @@ -1056,6 +1189,11 @@ class Base } $value = $item['value']; + $timeZone = null; + if (isset($item['timeZone'])) { + $timeZone = $item['timeZone']; + } + if (!empty($item['type'])) { $type = $item['type']; diff --git a/application/Espo/Core/Utils/DateTime.php b/application/Espo/Core/Utils/DateTime.php index 065c0bfe58..b79eae670d 100644 --- a/application/Espo/Core/Utils/DateTime.php +++ b/application/Espo/Core/Utils/DateTime.php @@ -37,6 +37,10 @@ class DateTime protected $timezone; + public static $systemDateTimeFormat = 'Y-m-d H:i:s'; + + public static $systemDateFormat = 'Y-m-d'; + protected $internalDateTimeFormat = 'Y-m-d H:i:s'; protected $internalDateFormat = 'Y-m-d'; diff --git a/application/Espo/Core/Utils/ScheduledJob.php b/application/Espo/Core/Utils/ScheduledJob.php index 9fe7c467f2..b108897b20 100644 --- a/application/Espo/Core/Utils/ScheduledJob.php +++ b/application/Espo/Core/Utils/ScheduledJob.php @@ -208,30 +208,33 @@ class ScheduledJob */ public function isCronConfigured() { - $startDate = new \DateTime('-' . $this->checkingCronPeriod, new \DateTimeZone("UTC")); - $endDate = new \DateTime('+' . $this->checkingCronPeriod, new \DateTimeZone("UTC")); + $r1From = new \DateTime('-' . $this->checkingCronPeriod); + $r1To = new \DateTime('+' . $this->checkingCronPeriod); - $query = " - SELECT job.id FROM scheduled_job - LEFT JOIN job ON job.scheduled_job_id = scheduled_job.id AND job.deleted = 0 - WHERE - scheduled_job.job = 'Dummy' - AND scheduled_job.deleted = 0 - AND job.execute_time BETWEEN '". $startDate->format('Y-m-d H:i:s') ."' AND '". $endDate->format('Y-m-d H:i:s') ."' - AND job.status IN ('Success', 'Failed', 'Pending') - ORDER BY job.execute_time DESC - "; + $r2From = new \DateTime('- 1 hour'); + $r2To = new \DateTime(); - $pdo = $this->getEntityManager()->getPDO(); - $sth = $pdo->prepare($query); - $sth->execute(); + $format = \Espo\Core\Utils\DateTime::$systemDateTimeFormat; - $row = $sth->fetch(\PDO::FETCH_ASSOC); + $selectParams = [ + 'select' => ['id'], + 'leftJoins' => ['scheduledJob'], + 'whereClause' => [ + 'OR' => [ + [ + ['executedAt>=' => $r2From->format($format)] , + ['executedAt<=' => $r2To->format($format)], + ], + [ + ['executeTime>=' => $r1From->format($format)], + ['executeTime<='=> $r1To->format($format)], + 'scheduledJob.job' => 'Dummy' + ] + ] + ] + ]; - if (!empty($row['id'])) { - return true; - } - return false; + return !!$this->getEntityManager()->getRepository('Job')->findOne($selectParams); } } diff --git a/application/Espo/Modules/Crm/Services/Task.php b/application/Espo/Modules/Crm/Services/Task.php index a51fdc5f9b..7114b8e508 100644 --- a/application/Espo/Modules/Crm/Services/Task.php +++ b/application/Espo/Modules/Crm/Services/Task.php @@ -36,6 +36,10 @@ use \Espo\ORM\Entity; class Task extends \Espo\Services\Record { + protected $selectAttributesDependancyMap = [ + 'dateEnd' => ['status'] + ]; + public function loadAdditionalFields(Entity $entity) { parent::loadAdditionalFields($entity); diff --git a/application/Espo/ORM/DB/Query/Base.php b/application/Espo/ORM/DB/Query/Base.php index c9a7d9a746..8dd1931b37 100644 --- a/application/Espo/ORM/DB/Query/Base.php +++ b/application/Espo/ORM/DB/Query/Base.php @@ -296,7 +296,7 @@ abstract class Base throw new \Exception("Not allowed function '".$function."'."); } - if (strpos($function, 'YEAR_') === 0) { + if (strpos($function, 'YEAR_') === 0 && $function !== 'YEAR_NUMBER') { $fiscalShift = substr($function, 5); if (is_numeric($fiscalShift)) { $fiscalShift = intval($fiscalShift); @@ -309,7 +309,7 @@ abstract class Base } } - if (strpos($function, 'QUARTER_') === 0) { + if (strpos($function, 'QUARTER_') === 0 && $function !== 'QUARTER_NUMBER') { $fiscalShift = substr($function, 8); if (is_numeric($fiscalShift)) { $fiscalShift = intval($fiscalShift); diff --git a/application/Espo/Resources/i18n/en_US/EmailTemplate.json b/application/Espo/Resources/i18n/en_US/EmailTemplate.json index 6cd44b9eb9..05e25115ba 100644 --- a/application/Espo/Resources/i18n/en_US/EmailTemplate.json +++ b/application/Espo/Resources/i18n/en_US/EmailTemplate.json @@ -29,6 +29,7 @@ "placeholderTexts": { "today": "Today's date", "now": "Current date & time", + "currentYear": "Current Year", "optOutUrl": "URL for an unsubsbribe link", "optOutLink": "an unsubscribe link" } diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index 5d03468739..1e000bf961 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -106,6 +106,7 @@ "outboundEmailBccAddress": "BCC address for external clients", "cleanupDeletedRecords": "Clean up deleted records", "addressCountryList": "Address Country Autocomplete List", + "addressCityList": "Address City Autocomplete List", "fiscalYearShift": "Fiscal Year Start", "jobRunInParallel": "Jobs Run in Parallel", "jobMaxPortion": "Jobs Max Portion", diff --git a/application/Espo/Resources/layouts/Settings/settings.json b/application/Espo/Resources/layouts/Settings/settings.json index 5f6e077397..a23128c4b7 100644 --- a/application/Espo/Resources/layouts/Settings/settings.json +++ b/application/Espo/Resources/layouts/Settings/settings.json @@ -16,9 +16,9 @@ [{"name": "language"}, {"name": "timeZone"}], [{"name": "dateFormat"}, {"name": "weekStart"}], [{"name": "timeFormat"}, {"name": "thousandSeparator"}], - [{"name": "addressFormat"}, {"name": "decimalMark"}], - [{"name": "addressPreview"}, {"name": "addressCountryList"}], - [{"name": "fiscalYearShift"}, false] + [{"name": "fiscalYearShift"}, {"name": "decimalMark"}], + [{"name": "addressFormat"}, {"name": "addressPreview"}], + [{"name": "addressCountryList"}, {"name": "addressCityList"}] ] }, { diff --git a/application/Espo/Resources/metadata/clientDefs/EmailTemplate.json b/application/Espo/Resources/metadata/clientDefs/EmailTemplate.json index dcde024af5..5a1d7cd5ac 100644 --- a/application/Espo/Resources/metadata/clientDefs/EmailTemplate.json +++ b/application/Espo/Resources/metadata/clientDefs/EmailTemplate.json @@ -43,6 +43,6 @@ "filterList": [ "actual" ], - "placeholderList": ["today", "now", "optOutUrl", "optOutLink"], + "placeholderList": ["today", "now", "currentYear", "optOutUrl", "optOutLink"], "iconClass": "fas fa-envelope-square" } diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index 19030128c6..284586e63e 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -505,6 +505,9 @@ "addressCountryList": { "type": "multiEnum" }, + "addressCityList": { + "type": "multiEnum" + }, "jobRunInParallel": { "type": "bool", "tooltip": true diff --git a/application/Espo/Resources/metadata/fields/address.json b/application/Espo/Resources/metadata/fields/address.json index 2eda441c2b..c25a508dc3 100644 --- a/application/Espo/Resources/metadata/fields/address.json +++ b/application/Espo/Resources/metadata/fields/address.json @@ -14,7 +14,9 @@ }, "city":{ "type":"varchar", - "trim": true + "trim": true, + "view": "views/fields/address-city", + "customizationOptionsDisabled": true }, "state":{ "type":"varchar", diff --git a/application/Espo/Services/EmailTemplate.php b/application/Espo/Services/EmailTemplate.php index 41a557d750..d5227bc2d1 100644 --- a/application/Espo/Services/EmailTemplate.php +++ b/application/Espo/Services/EmailTemplate.php @@ -276,6 +276,11 @@ class EmailTemplate extends Record $replaceData['today'] = $this->getDateTime()->getTodayString(); $replaceData['now'] = $this->getDateTime()->getNowString(); + $timeZone = $this->getConfig()->get('timeZone'); + $now = new \DateTime('now', new \DateTimezone($timeZone)); + + $replaceData['currentYear'] = $now->format('Y'); + foreach ($replaceData as $key => $value) { $text = str_replace('{' . $key . '}', $value, $text); } diff --git a/application/Espo/Services/Note.php b/application/Espo/Services/Note.php index 1fc24279f2..35a6307054 100644 --- a/application/Espo/Services/Note.php +++ b/application/Espo/Services/Note.php @@ -82,6 +82,11 @@ class Note extends Record protected function beforeCreateEntity(Entity $entity, $data) { parent::beforeCreateEntity($entity, $data); + + if ($entity->get('type') === 'Post') { + $this->handlePostText($entity); + } + $targetType = $entity->get('targetType'); $entity->clear('isGlobal'); @@ -119,6 +124,10 @@ class Note extends Record { parent::beforeUpdateEntity($entity, $data); + if ($entity->get('type') === 'Post') { + $this->handlePostText($entity); + } + $entity->clear('targetType'); $entity->clear('usersIds'); $entity->clear('teamsIds'); @@ -126,6 +135,19 @@ class Note extends Record $entity->clear('isGlobal'); } + protected function handlePostText(Entity $entity) + { + $post = $entity->get('post'); + if (empty($post)) return; + + $siteUrl = $this->getConfig()->getSiteUrl(); + + $regexp = '/' . preg_quote($siteUrl, '/') . '(\/portal|\/portal\/[a-zA-Z0-9]*)?\/#([A-Z][a-zA-Z0-9]*)\/view\/([a-zA-Z0-9]*)/'; + $post = preg_replace($regexp, '[\2/\3](#\2/view/\3)', $post); + + $entity->set('post', $post); + } + public function checkAssignment(Entity $entity) { if ($entity->isNew()) { diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index 638e93ce21..1f48b26ce2 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -117,6 +117,8 @@ class Record extends \Espo\Core\Services\Base protected $forceSelectAllAttributes = false; + protected $selectAttributesDependancyMap = []; + const MAX_SELECT_TEXT_ATTRIBUTE_LENGTH = 5000; const FOLLOWERS_LIMIT = 4; @@ -2073,7 +2075,7 @@ class Record extends \Espo\Core\Services\Base break; } $value = $entity->get($attribute); - return \Espo\Core\Utils\Json::encode($value); + return \Espo\Core\Utils\Json::encode($value, \JSON_UNESCAPED_UNICODE); break; case 'jsonArray': if (!empty($defs[$attribute]['isLinkMultipleIdList'])) { @@ -2081,7 +2083,7 @@ class Record extends \Espo\Core\Services\Base } $value = $entity->get($attribute); if (is_array($value)) { - return \Espo\Core\Utils\Json::encode($value); + return \Espo\Core\Utils\Json::encode($value, \JSON_UNESCAPED_UNICODE); } else { return null; } @@ -2479,6 +2481,16 @@ class Record extends \Espo\Core\Services\Base } } + foreach ($this->selectAttributesDependancyMap as $attribute => $dependantAttributeList) { + if (in_array($attribute, $attributeList)) { + foreach ($dependantAttributeList as $dependantAttribute) { + if (!in_array($dependantAttribute, $attributeList)) { + $attributeList[] = $dependantAttribute; + } + } + } + } + return $attributeList; } diff --git a/client/res/templates/fields/email/edit.tpl b/client/res/templates/fields/email/edit.tpl index 742d888191..e6270e601d 100644 --- a/client/res/templates/fields/email/edit.tpl +++ b/client/res/templates/fields/email/edit.tpl @@ -2,7 +2,7 @@