Compare commits

...

273 Commits

Author SHA1 Message Date
yuri 0daeed49c1 version 2018-03-15 15:00:27 +02:00
Taras Machyshyn 34540e12f3 getMaxIndexLength() changes 2018-03-15 14:55:27 +02:00
yuri b954a81630 template: page orientation and format 2018-03-15 12:23:59 +02:00
yuri 56472d6746 textFilterContainsMinLength config param 2018-03-15 11:03:14 +02:00
yuri 2721649313 Merge branch 'hotfix/5.1.1' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.1.1 2018-03-14 17:17:40 +02:00
yuri 24d7de60ab inline edit skip exit 2018-03-14 17:15:44 +02:00
Taras Machyshyn 441f9296ad Fixed file permissions after upgrading 2018-03-14 15:44:24 +02:00
yuri 3116a152e7 fix currency converted 2018-03-14 14:39:54 +02:00
yuri 77784c4224 fix currency converted 2018-03-14 13:02:40 +02:00
yuri bf13c7ca8c textFilterUseContainsForVarchar on admin ui 2018-03-14 11:19:14 +02:00
yuri 26c779a2c5 fix store sent email 2018-03-14 11:03:03 +02:00
yuri 1bfaf6c3ad model get entity type 2018-03-13 15:11:54 +02:00
yuri 69229ff6af Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-03-13 14:54:12 +02:00
yuri 719164524b naming fix 2018-03-13 13:45:25 +02:00
yuri 5a67b17916 email fix 2018-03-13 13:37:37 +02:00
Taras Machyshyn db4968b1ca Upgrade: Renamed vendorFiles 2018-03-13 13:11:58 +02:00
Taras Machyshyn e50f46e38a Installer: fix installation warning 2018-03-13 13:02:30 +02:00
Taras Machyshyn adc8988f6c Upgrade: copy vendor directory 2018-03-13 12:33:13 +02:00
yuri a480ad9c11 optimization 2018-03-13 11:35:20 +02:00
Taras Machyshyn d9be685f89 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-03-13 11:07:45 +02:00
Taras Machyshyn bd9db140de Added 'deleteAndCopy' directory for upgrade 2018-03-13 10:56:14 +02:00
yuri 74c0204590 keep rootUrl for edit view 2018-03-12 14:59:37 +02:00
yuri ce9d3299b1 skipTextColumns for email 2018-03-12 12:27:06 +02:00
yuri 8b27bc194d proper naming 2018-03-12 11:19:21 +02:00
yuri 1a6b4a6683 set fetched email address data 2018-03-12 11:11:28 +02:00
yuri 2ae4d6e090 orm converter skip fields w/o type 2018-03-12 10:57:41 +02:00
yuri 9467171c84 fix entity manager labels 2018-03-09 16:37:40 +02:00
yuri 402db729b7 field manager dont resave labels if not changed 2018-03-09 16:05:41 +02:00
yuri e16be12438 fix field manager 2018-03-09 14:58:24 +02:00
yuri 605912443d fix tests 2018-03-09 14:50:50 +02:00
yuri 62acc14f4a drop php mime mail parser by default 2018-03-09 14:25:28 +02:00
yuri 017066a311 currency part label 2018-03-09 14:21:35 +02:00
yuri 273b122ca8 fix labels in entity manager saving 2018-03-09 13:58:26 +02:00
yuri ce2ec3dd6c field manager utils get fields by type 2018-03-09 12:28:20 +02:00
yuri fda22605cc fix repository 2018-03-09 12:16:12 +02:00
yuri 1076fa6230 repositories bypass 2018-03-09 12:01:39 +02:00
yuri 019af59c8a default currency if empty 2018-03-09 12:01:27 +02:00
yuri 08bb644b58 fix notice 2018-03-08 16:36:52 +02:00
yuri 6ec1273e61 update mail parser libs 2018-03-08 16:30:25 +02:00
yuri ba5df9a4ae fix export time 2018-03-08 15:53:42 +02:00
yuri 050d77ef12 fix activities dashlet 2018-03-08 15:25:31 +02:00
yuri f8ccaeab08 select manager user 2018-03-08 14:46:42 +02:00
yuri 3c4c464e4a fix email flicker 2018-03-08 12:37:06 +02:00
yuri 2a5730def9 iframe css fix 2018-03-08 11:36:35 +02:00
yuri 48fe3e6b86 fix email size 2018-03-07 16:15:26 +02:00
yuri eecbc3aa2c lang 2018-03-07 13:52:26 +02:00
yuri e177f14446 having count 2018-03-07 12:45:29 +02:00
yuri 493ab0396a fix currency 2018-03-07 11:52:31 +02:00
yuri c776150ce0 rebuild after upgrade 2018-03-06 16:01:50 +02:00
yuri b6bca982bb default side panel client defs params 2018-03-06 15:25:45 +02:00
yuri aa8a243bf1 order fixes 2018-03-06 12:36:02 +02:00
yuri 75da037656 fix stream note 2018-03-06 11:25:39 +02:00
yuri 56a06a5581 remove dynamic logic from bottom and side 2018-03-06 11:03:23 +02:00
yuri 0af67bea3a fix dynamic logic 2018-03-06 11:01:38 +02:00
yuri 08b8d899b7 fix typo 2018-03-05 17:03:33 +02:00
yuri cf07cb39c4 wysiwyg height fix 2018-03-05 17:03:24 +02:00
yuri 810d10c1c2 bottom panels order 2018-03-05 15:24:24 +02:00
yuri 342bdcfaac fix typo 2018-03-05 13:15:28 +02:00
yuri d933dbb31a fix dynamic logic 2018-03-05 12:41:54 +02:00
yuri fe61364178 fix panel dynamic logic 2018-03-05 12:32:09 +02:00
yuri 88c3c03f99 dynamic logic detail view 2018-03-05 12:12:41 +02:00
yuri c21b16f5c7 panel defs dynamic logic 2018-03-05 11:23:11 +02:00
yuri 2481a2504f fix auth token ip length 2018-03-02 16:57:25 +02:00
Taras Machyshyn dcb1e8040a MySQL charset change improvements 2018-03-02 15:42:26 +02:00
yuri 0e1c611034 update composer 2018-03-02 14:11:33 +02:00
yuri fccd9d642c ditch php imap dependency 2018-03-02 14:10:48 +02:00
yuri 849f1becc7 version 2018-03-02 13:44:14 +02:00
yuri be6c4a63ed do not remove mysql character service 2018-03-02 13:02:41 +02:00
yuri 4e48483276 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-03-02 13:01:32 +02:00
Taras Machyshyn 6817a54aa0 Job for changing mysql column characters 2018-03-02 13:00:25 +02:00
yuri 482f235dcc Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-03-02 11:54:04 +02:00
yuri 90b863c4ae update composer 2018-03-02 11:53:57 +02:00
Taras Machyshyn 8541a87d1f Integration tests fixes 2018-03-02 11:35:52 +02:00
Taras Machyshyn 002adeb00e Integration test fixes 2018-03-02 11:15:01 +02:00
Taras Machyshyn 30e7d1ba7c Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-03-02 10:49:35 +02:00
yuri 0923f2e120 Merge branch 'master' of github.com:espocrm/espocrm 2018-03-02 10:37:50 +02:00
Andra Fetele 38a0c8e19f Romanian translation (#823) 2018-03-02 09:31:01 +02:00
yuri facf90d964 fix event Set Held label 2018-03-01 18:04:00 +02:00
yuri cca3482bc0 fix query stringifyValue 2018-03-01 17:41:39 +02:00
yuri 6178ca550b import fix 2018-03-01 16:37:22 +02:00
yuri dc51e467d4 excel export: migrate to phpspreadsheet 2018-03-01 16:01:37 +02:00
yuri 300827c1f0 vendor updates 2018-03-01 15:47:26 +02:00
yuri aacf09a444 email move to trash from detail view updates list 2018-03-01 15:14:24 +02:00
yuri 0123488828 import revert remove change 2018-03-01 14:50:45 +02:00
yuri a43b188f36 import action history 2018-02-28 17:06:50 +02:00
Taras Machyshyn f0285ef8f2 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-02-28 16:19:05 +02:00
yuri 70a0ffbf3d fix list tpl 2018-02-28 11:48:29 +02:00
yuri f2e6a4fc55 import improvement 2018-02-28 11:43:11 +02:00
yuri df5fd76ae2 update-all event for model 2018-02-28 11:42:58 +02:00
yuri f34d258b45 header button title 2018-02-28 11:09:34 +02:00
yuri 28c52763c8 Merge branch 'hotfix/5.0.6' 2018-02-27 17:46:45 +02:00
yuri ebe22fa7f0 fix htmlizer 2018-02-27 17:44:56 +02:00
yuri 160327e471 Merge branch 'hotfix/5.0.6' 2018-02-27 17:32:51 +02:00
yuri dd080dbff2 Merge branch 'hotfix/5.0.6' of ssh://172.20.0.1/var/git/espo/backend into hotfix/5.0.6 2018-02-27 17:32:33 +02:00
yuri d509886cd1 sql fixes 2018-02-27 17:31:50 +02:00
Taras Machyshyn 239a8f0af8 Code improvements 2018-02-27 17:14:19 +02:00
Taras Machyshyn c0565306c3 Code improvements 2018-02-27 17:13:15 +02:00
yuri 79d159511d fix category tree repository 2018-02-27 17:06:53 +02:00
yuri e123fc7e6f target list layout change 2018-02-27 16:26:33 +02:00
yuri 52fa30f389 event confirmation change 2018-02-27 16:19:23 +02:00
yuri 1d744d5abc cleanup fixes 2018-02-27 16:16:30 +02:00
yuri 2629c12ade fix link and link multiple empty name 2018-02-27 14:27:15 +02:00
yuri 8fb6b1a5a9 fix parent name load 2018-02-27 14:12:48 +02:00
yuri d4a23c9a4f fix parent 2018-02-27 14:07:58 +02:00
yuri 05d0f982a7 fix select manager 2018-02-26 15:30:42 +02:00
yuri e3c8a3fcda fix applyFilter 2018-02-26 15:07:41 +02:00
yuri 8cd56d2705 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-02-26 14:23:21 +02:00
Taras Machyshyn f94d1f3cd8 Metadata: get metadata in object format 2018-02-26 14:22:14 +02:00
yuri 5f64b74fad fix ro_RO 2018-02-26 13:35:54 +02:00
Taras Machyshyn b422cd6f52 Metadata: getCustom() changes 2018-02-26 13:03:25 +02:00
Andra Fetele 17698409b9 Approx 700 lines, translated into Romanian (#813)
* Romanian translation

* Approx 700 lines, translated into Romanian
2018-02-24 20:07:34 +02:00
Andra Fetele fb0dabebe1 Romanian translation (#812) 2018-02-23 20:59:55 +02:00
yuri f81805fbf4 fix dynamic handler 2018-02-23 17:00:48 +02:00
Taras Machyshyn 596c9d4996 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-02-23 15:50:59 +02:00
Taras Machyshyn a7530dcce9 Bug fixes in File Manager 2018-02-23 15:50:49 +02:00
yuri 55d59eea80 checkAccessDataList allowAllForAdmin 2018-02-23 15:14:22 +02:00
yuri 0c66a1fb7b dynamic handler extend 2018-02-23 13:55:00 +02:00
yuri ad7ca5d287 dynamic handler 2018-02-23 13:44:24 +02:00
yuri b34be4730d Merge branch 'master' of github.com:espocrm/espocrm 2018-02-23 11:10:16 +02:00
yuri 1c264055eb Merge branch 'hotfix/5.0.6' 2018-02-23 11:09:59 +02:00
yuri 12f41baff5 file filters 2018-02-23 11:09:42 +02:00
yuri 9a9ad1baa7 lang 2018-02-23 11:03:28 +02:00
Yuri Kuznetsov 46c31278e7 Update README.md 2018-02-22 18:34:37 +02:00
yuri a08dc3d422 bottom panels isBottom 2018-02-22 15:51:07 +02:00
yuri 158a74f79b fix tests 2018-02-22 15:37:19 +02:00
Taras Machyshyn bacf85ccf8 Metadata: saveCustom() fixes 2018-02-22 15:33:56 +02:00
Taras Machyshyn ef683bd7fa Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-02-22 15:02:37 +02:00
yuri 20c233fb8c more user acl tests 2018-02-22 15:01:37 +02:00
Taras Machyshyn ac21313190 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-02-22 14:56:29 +02:00
Taras Machyshyn 8ba6918139 Metadata: added possibility to work with metadata defined in custom directory 2018-02-22 14:56:20 +02:00
yuri 5e357b460d cleanup 2018-02-22 14:51:04 +02:00
yuri d660abe1f6 acl tests 2018-02-22 14:48:56 +02:00
yuri fd9617ec8f Merge branch 'hotfix/5.0.6' 2018-02-22 12:38:41 +02:00
yuri 48ccdaab5e calendar responcive fixes 2018-02-22 12:38:27 +02:00
yuri 52d9548333 select manager: external primary filters 2018-02-22 12:16:25 +02:00
yuri 4706d28560 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-02-21 16:34:58 +02:00
yuri 033f5de85d Merge branch 'hotfix/5.0.6' 2018-02-21 16:34:48 +02:00
yuri 6760f6f03d note type enum 2018-02-21 15:39:29 +02:00
yuri e1a7862194 xlsx fix 2018-02-21 14:00:46 +02:00
Taras Machyshyn 8ef53d229d Required MySQL version is 5.5.3 2018-02-21 13:01:57 +02:00
yuri c6d215f87c Merge branch 'hotfix/5.0.6' 2018-02-21 12:10:12 +02:00
yuri dccaed124c fix duplicate one-to-many issue 2018-02-21 12:10:04 +02:00
yuri ccfca4a61a activity account contact fiexes 2018-02-21 12:00:10 +02:00
Taras Machyshyn 417a05f24f Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-02-20 17:54:14 +02:00
Taras Machyshyn 7a85f7576d Unit test for copy() of FileManager 2018-02-20 17:54:04 +02:00
yuri 8407ab3a74 Merge branch 'master' of ssh://172.20.0.1/var/git/espo/backend 2018-02-20 16:58:13 +02:00
yuri c120ebd3ca header fixes 2018-02-20 16:50:24 +02:00
Taras Machyshyn d0635484a2 Database: default column charset is utf8mb4 2018-02-20 15:56:34 +02:00
yuri d3c487a28a modal overlaid shadow none 2018-02-20 15:49:22 +02:00
yuri 14f46a5d5d Merge branch 'hotfix/5.0.6' 2018-02-20 15:00:28 +02:00
yuri 00a8ca8273 fix select manager 2018-02-20 14:58:51 +02:00
yuri deec457728 header fix 2018-02-20 14:34:27 +02:00
yuri 5a5508a973 accessDataList for dashlets 2018-02-20 12:42:38 +02:00
yuri 357b285346 accessDataList for list filters and panels 2018-02-20 12:40:38 +02:00
yuri f8444b9e67 Merge branch 'hotfix/5.0.6' 2018-02-20 12:18:46 +02:00
yuri e0a224e414 calendar fixes 2018-02-20 12:18:39 +02:00
yuri 9dfb128e1d calender decrease longPressDelay 2018-02-20 11:58:11 +02:00
yuri 462d1f0274 utils checkAccessDataList 2018-02-20 11:44:10 +02:00
yuri 2e3803eef1 cleanup 2018-02-19 15:32:08 +02:00
yuri 42b186eb94 Merge branch 'hotfix/5.0.6' 2018-02-19 15:31:20 +02:00
yuri ccdee9ec09 import email unlock tables 2018-02-19 15:00:49 +02:00
yuri 05eeb82efd attendees panel not refreshable 2018-02-19 12:39:13 +02:00
yuri 6eefe64f4c fix frontend tests 2018-02-19 12:26:00 +02:00
yuri 5b96b19735 jasmine test fixes 2018-02-16 16:49:33 +02:00
yuri 3764d877ce Merge branch 'hotfix/5.0.6' 2018-02-16 12:33:46 +02:00
yuri 30e713c161 reminders in detailSmall 2018-02-16 11:49:01 +02:00
yuri da3a76e441 hack for upgrade 2018-02-16 11:23:19 +02:00
yuri adff467b50 event confirmation and unsubscribe: use frontend 2018-02-15 14:56:36 +02:00
yuri d59f05ad8b event confirmation hook 2018-02-15 13:26:19 +02:00
yuri 9c3d124f48 Merge branch 'hotfix/5.0.6' 2018-02-14 15:16:59 +02:00
yuri 4404b0f02e select manager: convertWhere 2018-02-14 11:33:22 +02:00
Andra Fetele 77533fba72 translated ~700 lines into Romanian (#806)
* Partial Romanian translation

* translated ~700 lines into Romanian
2018-02-14 08:49:10 +02:00
yuri 62d36ed0c2 lang 2018-02-13 12:46:40 +02:00
yuri 94ad0123c7 css fix 2018-02-13 11:21:05 +02:00
yuri 109842f0fe contact: hide title if no account 2018-02-13 11:19:38 +02:00
yuri d931d65cd8 formatting 2018-02-13 11:18:19 +02:00
yuri b7861a7cb1 htmlizer enum translation 2018-02-13 11:07:19 +02:00
yuri a49ad3bb48 fix lang 2018-02-13 11:04:05 +02:00
yuri d94acf5b78 fix activities dashlet tasks 2018-02-12 16:34:45 +02:00
yuri fd0e1d1460 css fix 2018-02-12 16:21:43 +02:00
yuri 2ed9d5487e chart css fix 2018-02-12 16:12:22 +02:00
yuri 5dc44cb701 fix assignment permission 2018-02-12 14:41:42 +02:00
yuri 9e07c383fe collection abortLastFetch 2018-02-12 14:33:09 +02:00
yuri 9e1ec2768b fix email switch folders 2018-02-12 14:06:55 +02:00
yuri 39674e8c7f htmlspecialchars for unsubscribe page 2018-02-12 12:52:47 +02:00
yuri 5dca4b7061 template aclPortal 2018-02-12 12:44:45 +02:00
yuri 5d3cb25d14 css fix 2018-02-12 12:43:16 +02:00
yuri 6e337affbb fix chart 2018-02-12 11:26:49 +02:00
yuri 87ab8cc15f chart css fix 2018-02-12 10:23:55 +02:00
yuri b654e5cebd icon class 2018-02-10 15:37:48 +02:00
yuri 11958f2abf icon changes 2018-02-10 15:34:45 +02:00
yuri 2a329ba92d fix next/prev through collection 2018-02-10 14:38:03 +02:00
yuri b22487ef70 lang fixes 2018-02-10 14:25:21 +02:00
Andra Fetele bf03a2f540 Partial Romanian translation (#804) 2018-02-09 20:08:08 +02:00
yuri ece51a6f9f task and activities dashlet fixes 2018-02-09 17:41:34 +02:00
yuri 915dbffe72 activities dashlet: support tasks 2018-02-09 17:22:00 +02:00
yuri fda0d6df1f fix login appParams 2018-02-09 15:51:05 +02:00
yuri 25891f9dc0 print pdf change 2018-02-09 15:45:24 +02:00
yuri 35902adbdc orm having clause 2018-02-09 14:47:18 +02:00
yuri 981d15ee55 Merge branch 'hotfix/5.0.5' 2018-02-09 13:04:04 +02:00
yuri adfec3441b template tab 2018-02-09 12:05:01 +02:00
yuri a208aa051f Merge branch 'hotfix/5.0.5' 2018-02-08 17:44:06 +02:00
yuri 9068302357 version 2018-02-08 16:52:23 +02:00
yuri 1bb0439f19 fix model reset 2018-02-08 16:02:12 +02:00
yuri f28585e245 mass update ui improvement 2018-02-08 15:37:41 +02:00
yuri 99cfeb25ee fix print to pdf button 2018-02-08 14:18:58 +02:00
yuri 6b7da9a39e fix entity manger and field manager 2018-02-08 13:22:00 +02:00
yuri 18bba7baec entity manager fix 2018-02-08 13:15:49 +02:00
yuri ec8ffbc090 chart dashlets acl 2018-02-08 12:52:18 +02:00
yuri 60eb1ca236 fix sales by month chart 2018-02-08 12:40:49 +02:00
yuri 5e3756313c fix sales pipeline chart 2018-02-08 12:20:13 +02:00
yuri d303d5a423 Merge branch 'hotfix/5.0.5' 2018-02-08 11:56:30 +02:00
yuri 938415aa79 cleanup 2018-02-08 11:54:15 +02:00
yuri 9033710e56 Merge branch 'hotfix/5.0.5' 2018-02-07 15:25:24 +02:00
yuri 2fd77e4034 excel export duration 2018-02-07 15:23:06 +02:00
yuri b733d8205e relationship panel setup list layout method 2018-02-07 14:59:58 +02:00
yuri f725ad09d7 user: hide mass update and export for non admin users 2018-02-07 13:41:29 +02:00
yuri e9a53c5b25 list view acl delete check 2018-02-07 12:54:58 +02:00
yuri 43e94cfe68 user acl check 2018-02-07 12:13:23 +02:00
yuri 07d713734f Merge branch 'hotfix/5.0.5' 2018-02-07 11:29:18 +02:00
yuri 4f004b8402 note status styles change 2018-02-07 11:07:24 +02:00
yuri b1f4131af9 user acl check 2018-02-06 17:36:10 +02:00
yuri 1adb19f427 fix field manager 2018-02-06 17:28:37 +02:00
yuri fce4a4b1d9 skip additional select params 2018-02-06 17:13:48 +02:00
yuri 61f35bcb5e user editable 2018-02-06 17:04:42 +02:00
yuri 0db68917a1 Merge branch 'hotfix/5.0.5' 2018-02-06 15:13:51 +02:00
yuri 9da8ab9bdb chart fix 2018-02-06 13:46:59 +02:00
yuri 8d1724df84 less opp stages 2018-02-06 13:01:09 +02:00
yuri cd88d59937 po ability to build po for all languages 2018-02-06 12:28:20 +02:00
yuri cab4f5ce64 opp stages translation 2018-02-06 12:22:20 +02:00
yuri 62ab1f8d00 fix export 2018-02-06 11:29:17 +02:00
yuri f0e1439c79 fix chart legend 2018-02-06 11:03:10 +02:00
yuri 5060918087 print pdf for all entity types 2018-02-05 16:42:33 +02:00
yuri bf6b058152 reset page title on logout 2018-02-05 15:54:13 +02:00
yuri 40b607a73f template change 2018-02-05 15:48:35 +02:00
yuri 2bdbba0eff fix sales chart 2018-02-05 15:42:56 +02:00
yuri 058d821a7c fix chart 2018-02-05 15:32:22 +02:00
yuri 4575ff89ad chart fixes 2018-02-05 12:59:50 +02:00
yuri 608454fcf2 fix css 2018-02-05 12:54:26 +02:00
yuri 58c1bec2da css fix 2018-02-05 12:30:57 +02:00
yuri e1ce16c399 chart legend title 2018-02-05 12:16:05 +02:00
yuri 2d3a748286 chart fixes 2018-02-05 11:58:21 +02:00
yuri 06834dd8bf fix calendar 2018-02-05 10:18:31 +02:00
yuri e4c0f19b1c fix css 2018-02-02 16:03:29 +02:00
yuri 84faf7a782 entity manager: supporting templates in modules 2018-02-02 14:27:40 +02:00
yuri 61db675ab1 css fix 2018-02-01 16:48:59 +02:00
yuri 59e72796e3 css fix 2018-02-01 16:43:07 +02:00
yuri 76e61a6358 fix dropdown on small screens 2018-02-01 14:54:04 +02:00
yuri 455eceefa1 Merge branch 'hotfix/5.0.4' 2018-02-01 13:15:41 +02:00
yuri e8c171be92 Merge branch 'stable' 2018-01-19 14:14:25 +02:00
yuri 2c8094bf28 Merge branch 'hotfix/5.0.3' 2018-01-18 15:06:34 +02:00
yuri 2b5eed3a38 Merge branch 'hotfix/5.0.3' 2018-01-17 17:05:38 +02:00
yuri aa3d2f8787 Merge branch 'hotfix/5.0.3' 2018-01-17 14:39:22 +02:00
yuri d0cdefd972 Merge branch 'hotfix/5.0.3' 2018-01-17 13:34:38 +02:00
yuri 1b5346e86d Merge branch 'hotfix/5.0.3' 2018-01-17 12:23:35 +02:00
yuri 6deb336115 Merge branch 'hotfix/5.0.3' 2018-01-16 17:15:24 +02:00
yuri 115ae82b17 Merge branch 'hotfix/5.0.3' 2018-01-16 15:40:54 +02:00
yuri 16579ff1a8 Merge branch 'hotfix/5.0.3' 2018-01-16 11:12:48 +02:00
yuri a030c50780 Merge branch 'hotfix/5.0.3' 2018-01-15 16:17:00 +02:00
yuri e60d46b2ec Merge branch 'hotfix/5.0.3' 2018-01-15 14:44:30 +02:00
yuri 28c4651599 Merge branch 'hotfix/5.0.3' 2018-01-15 14:23:16 +02:00
yuri 6b458f3b55 file upload ready validation 2018-01-15 12:46:49 +02:00
yuri 874885671b fix css 2018-01-15 12:38:52 +02:00
yuri a75f4dacab Merge branch 'hotfix/5.0.3' 2018-01-15 11:56:26 +02:00
yuri 1b12cd46b2 Merge branch 'hotfix/5.0.3' 2018-01-12 16:46:15 +02:00
yuri 7389acd87f Merge branch 'hotfix/5.0.3' 2018-01-12 16:09:45 +02:00
yuri a290f7aba9 Merge branch 'hotfix/5.0.3' 2018-01-12 15:09:44 +02:00
yuri e9c0cd6d53 Merge branch 'hotfix/5.0.3' 2018-01-11 16:49:31 +02:00
yuri 7d847cf179 Merge branch 'hotfix/5.0.3' 2018-01-11 12:29:41 +02:00
yuri 74093976db Merge branch 'hotfix/5.0.3' 2018-01-10 15:02:01 +02:00
yuri 004dfc1687 Merge branch 'hotfix/5.0.2' 2018-01-09 16:23:56 +02:00
yuri e429bde5b8 Merge branch 'hotfix/5.0.2' 2018-01-09 16:15:07 +02:00
yuri 3d8bbdd01d Merge branch 'hotfix/5.0.2' of ssh://172.20.0.1/var/git/espo/backend 2018-01-09 11:49:45 +02:00
427 changed files with 23421 additions and 6823 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ Download the latest release from our [website](http://www.espocrm.com).
### Requirements
* PHP 5.6 or above (with pdo, json, gd, openssl, zip, imap, mbstring, curl extensions);
* MySQL 5.1 or above.
* MySQL 5.5.3 or above.
For more information about server configuration see [this article](https://www.espocrm.com/documentation/administration/server-configuration/).
+45
View File
@@ -0,0 +1,45 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Acl;
use \Espo\Entities\User as EntityUser;
use \Espo\ORM\Entity;
class Note extends \Espo\Core\Acl\Base
{
public function checkIsOwner(EntityUser $user, Entity $entity)
{
if ($entity->get('type') === 'Post' && $user->id === $entity->get('createdById')) {
return true;
}
return false;
}
}
+16
View File
@@ -39,11 +39,22 @@ class User extends \Espo\Core\Acl\Base
return $user->id === $entity->id;
}
public function checkEntityCreate(EntityUser $user, Entity $entity, $data)
{
if (!$user->isAdmin()) {
return false;
}
return $this->checkEntity($user, $entity, $data, 'create');
}
public function checkEntityDelete(EntityUser $user, Entity $entity, $data)
{
if ($entity->id === 'system') {
return false;
}
if (!$user->isAdmin()) {
return false;
}
return parent::checkEntityDelete($user, $entity, $data);
}
@@ -52,6 +63,11 @@ class User extends \Espo\Core\Acl\Base
if ($entity->id === 'system') {
return false;
}
if (!$user->isAdmin()) {
if ($user->id !== $entity->id) {
return false;
}
}
return $this->checkEntity($user, $entity, $data, 'edit');
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\AclPortal;
use \Espo\Entities\User as EntityUser;
use \Espo\ORM\Entity;
class Note extends \Espo\Core\AclPortal\Base
{
public function checkIsOwner(EntityUser $user, Entity $entity)
{
if ($entity->get('type') === 'Post' && $user->id === $entity->get('createdById')) {
return true;
}
return false;
}
}
@@ -245,7 +245,9 @@ class EntityManager extends \Espo\Core\Controllers\Base
$params = array();
foreach ($paramList as $item) {
$params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING);
if (array_key_exists($item, $data)) {
$params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING);
}
}
foreach ($additionalParamList as $item) {
@@ -77,6 +77,11 @@ class FieldManager extends \Espo\Core\Controllers\Base
return $fieldManager->read($params['scope'], $data->name);
}
public function patchActionUpdate($params, $data)
{
return $this->putActionUpdate($params, $data);
}
public function putActionUpdate($params, $data)
{
if (empty($params['scope']) || empty($params['name'])) {
+1 -3
View File
@@ -90,11 +90,9 @@ class Settings extends \Espo\Core\Controllers\Base
throw new Error('Cannot save settings');
}
/** Rebuild for Currency Settings */
if (isset($data->baseCurrency) || isset($data->currencyRates)) {
if (isset($data->defaultCurrency) || isset($data->baseCurrency) || isset($data->currencyRates)) {
$this->getContainer()->get('dataManager')->rebuildDatabase([]);
}
/** END Rebuild for Currency Settings */
return $this->getConfigData();
}
-20
View File
@@ -713,25 +713,5 @@ class Table
private function buildCache()
{
$this->fileManager->putPhpContents($this->cacheFilePath, $this->data, true);
/*$contents = '<' . '?'. 'php return ' . $this->varExport($this->data) . ';';
$this->fileManager->putContents($this->cacheFilePath, $contents);*/
}
/*private function varExport($variable)
{
if ($variable instanceof \StdClass) {
$result = '(object) ' . $this->varExport(get_object_vars($variable), true);
} else if (is_array($variable)) {
$array = array();
foreach ($variable as $key => $value) {
$array[] = var_export($key, true).' => ' . $this->varExport($value, true);
}
$result = '['.implode(', ', $array).']';
} else {
$result = var_export($variable, true);
}
return $result;
}*/
}
+9 -1
View File
@@ -47,6 +47,8 @@ class AclManager
protected $tableClassName = '\\Espo\\Core\\Acl\\Table';
protected $userAclClassName = '\\Espo\\Core\\Acl';
public function __construct(Container $container)
{
$this->container = $container;
@@ -279,5 +281,11 @@ class AclManager
{
return $this->checkUserPermission($user, $target, 'assignmentPermission');
}
}
public function createUserAcl(User $user)
{
$className = $this->userAclClassName;
$acl = new $className($this, $user);
return $acl;
}
}
+12 -3
View File
@@ -217,7 +217,8 @@ class Container
$this->get('acl'),
$this->get('aclManager'),
$this->get('metadata'),
$this->get('config')
$this->get('config'),
$this->get('injectableFactory')
);
}
@@ -295,6 +296,16 @@ class Container
);
}
protected function loadBaseLanguage()
{
return new \Espo\Core\Utils\Language(
'en_US',
$this->get('fileManager'),
$this->get('metadata'),
$this->get('useCache')
);
}
protected function loadDefaultLanguage()
{
return new \Espo\Core\Utils\Language(
@@ -329,8 +340,6 @@ class Container
protected function loadFieldManager()
{
return new \Espo\Core\Utils\FieldManager(
$this->get('metadata'),
$this->get('language'),
$this
);
}
+69 -11
View File
@@ -87,6 +87,20 @@ class Xlsx extends \Espo\Core\Injectable
}
}
public function filterFieldList($entityType, $fieldList, $exportAllFields)
{
if ($exportAllFields) {
foreach ($fieldList as $i => $field) {
$type = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field, 'type']);
if (in_array($type, ['linkMultiple', 'attachmentMultiple'])) {
unset($fieldList[$i]);
}
}
}
return array_values($fieldList);
}
public function addAdditionalAttributes($entityType, &$attributeList, $fieldList)
{
$linkList = [];
@@ -130,7 +144,7 @@ class Xlsx extends \Espo\Core\Injectable
throw new Error();
}
$phpExcel = new \PHPExcel();
$phpExcel = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $phpExcel->setActiveSheetIndex(0);
if (isset($params['exportName'])) {
@@ -162,10 +176,11 @@ class Xlsx extends \Espo\Core\Injectable
)
);
$now = new \DateTime();
$now->setTimezone(new \DateTimeZone($this->getInjection('config')->get('timeZone', 'UTC')));
$sheet->setCellValue('A1', $exportName);
$sheet->setCellValue('B1', \PHPExcel_Shared_Date::PHPToExcel(strtotime(date('Y-m-d H:i:s'))));
$sheet->setCellValue('B1', \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($now->format('Y-m-d H:i:s'))));
$sheet->getStyle('A1')->applyFromArray($titleStyle);
$sheet->getStyle('B1')->applyFromArray($dateStyle);
@@ -289,7 +304,7 @@ class Xlsx extends \Espo\Core\Injectable
}
} else if ($type == 'currencyConverted') {
if (array_key_exists($name, $row)) {
$currency = $this->getConfig()->get('baseCurrency');
$currency = $this->getConfig()->get('defaultCurrency');
$currencySymbol = $this->getMetadata()->get(['app', 'currency', 'symbolMap', $currency], '');
$sheet->getStyle("$col$rowNumber")
@@ -316,7 +331,7 @@ class Xlsx extends \Espo\Core\Injectable
}
} else if ($type == 'date') {
if (isset($row[$name])) {
$sheet->setCellValue("$col$rowNumber", \PHPExcel_Shared_Date::PHPToExcel(strtotime($row[$name])));
$sheet->setCellValue("$col$rowNumber", \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($row[$name])));
}
} else if ($type == 'datetime' || $type == 'datetimeOptional') {
$value = null;
@@ -341,14 +356,14 @@ class Xlsx extends \Espo\Core\Injectable
}
}
if ($value) {
$sheet->setCellValue("$col$rowNumber", \PHPExcel_Shared_Date::PHPToExcel(strtotime($value)));
$sheet->setCellValue("$col$rowNumber", \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($value)));
}
} else if ($type == 'image') {
if (isset($row[$name . 'Id']) && $row[$name . 'Id']) {
$attachment = $this->getEntityManager()->getEntity('Attachment', $row[$name . 'Id']);
if ($attachment) {
$objDrawing = new \PHPExcel_Worksheet_Drawing();
$objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
$filePath = $this->getInjection('fileStorageManager')->getLocalFilePath($attachment);
if ($filePath && file_exists($filePath)) {
@@ -420,6 +435,36 @@ class Xlsx extends \Espo\Core\Injectable
$value .= $row[$name.'Country'];
}
$sheet->setCellValue("$col$rowNumber", $value);
} else if ($type == 'duration') {
if (!empty($row[$name])) {
$seconds = intval($row[$name]);
$days = intval(floor($seconds / 86400));
$seconds = $seconds - $days * 86400;
$hours = intval(floor($seconds / 3600));
$seconds = $seconds - $hours * 3600;
$minutes = intval(floor($seconds / 60));
$value = '';
if ($days) {
$value .= (string) $days . $this->getInjection('language')->translate('d', 'durationUnits');
if ($minutes || $hours) {
$value .= ' ';
}
}
if ($hours) {
$value .= (string) $hours . $this->getInjection('language')->translate('h', 'durationUnits');
if ($minutes) {
$value .= ' ';
}
}
if ($minutes) {
$value .= (string) $minutes . $this->getInjection('language')->translate('m', 'durationUnits');
}
$sheet->setCellValue("$col$rowNumber", $value);
}
} else {
if (array_key_exists($name, $row)) {
$sheet->setCellValue("$col$rowNumber", $row[$name]);
@@ -428,6 +473,13 @@ class Xlsx extends \Espo\Core\Injectable
$link = false;
$foreignLink = null;
$isForeign = false;
if (strpos($name, '_')) {
$isForeign = true;
list($foreignLink, $foreignField) = explode('_', $name);
}
if ($name == 'name') {
if (array_key_exists('id', $row)) {
$link = $this->getConfig()->getSiteUrl() . "/#".$entityType . "/view/" . $row['id'];
@@ -438,7 +490,13 @@ class Xlsx extends \Espo\Core\Injectable
}
} else if ($type == 'link') {
if (array_key_exists($name.'Id', $row)) {
$foreignEntity = $this->getMetadata()->get(['entityDefs', $entityType, 'links', $name, 'entity']);
$foreignEntity = null;
if (!$isForeign) {
$foreignEntity = $this->getMetadata()->get(['entityDefs', $entityType, 'links', $name, 'entity']);
} else {
$foreignEntity1 = $this->getMetadata()->get(['entityDefs', $entityType, 'links', $foreignLink, 'entity']);
$foreignEntity = $this->getMetadata()->get(['entityDefs', $foreignEntity1, 'links', $foreignField, 'entity']);
}
if ($foreignEntity) {
$link = $this->getConfig()->getSiteUrl() . "/#" . $foreignEntity. "/view/". $row[$name.'Id'];
}
@@ -470,7 +528,7 @@ class Xlsx extends \Espo\Core\Injectable
$sheet->getStyle("A2:A$rowNumber")
->getNumberFormat()
->setFormatCode(\PHPExcel_Style_NumberFormat::FORMAT_TEXT);
->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_TEXT);
$startingRowNumber = 4;
@@ -521,7 +579,7 @@ class Xlsx extends \Espo\Core\Injectable
$sheet->getStyle($linkColumn.$startingRowNumber.':'.$linkColumn.$rowNumber)->applyFromArray($linkStyle);
}
$objWriter = \PHPExcel_IOFactory::createWriter($phpExcel, 'Excel2007');
$objWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpExcel, 'Xlsx');
if (!$this->getInjection('fileManager')->isDir('data/cache/')) {
$this->getInjection('fileManager')->mkdir('data/cache/');
@@ -36,10 +36,10 @@ class IncludesType extends \Espo\Core\Formula\Functions\Base
public function process(\StdClass $item)
{
if (!property_exists($item, 'value') || !is_array($item->value)) {
throw new Error('Value for \'Array\\Includses\' item is not array.');
throw new Error('Value for \'Array\\Includes\' item is not array.');
}
if (count($item->value) < 2) {
throw new Error('Bad arguments passed to \'Array\\Includses\'.');
throw new Error('Bad arguments passed to \'Array\\Includes\'.');
}
$list = $this->evaluate($item->value[0]);
$needle = $this->evaluate($item->value[1]);
+27 -8
View File
@@ -50,13 +50,19 @@ class Htmlizer
protected $entityManager;
public function __construct(FileManager $fileManager, DateTime $dateTime, NumberUtil $number, $acl = null, $entityManager = null)
protected $metadata;
protected $language;
public function __construct(FileManager $fileManager, DateTime $dateTime, NumberUtil $number, $acl = null, $entityManager = null, $metadata = null, $language = null)
{
$this->fileManager = $fileManager;
$this->dateTime = $dateTime;
$this->number = $number;
$this->acl = $acl;
$this->entityManager = $entityManager;
$this->metadata = $metadata;
$this->language = $language;
}
protected function getAcl()
@@ -117,14 +123,14 @@ class Htmlizer
if ($item instanceof \StdClass) {
$v = json_decode(json_encode($v), true);
}
if (!is_array($v)) {
$v = [];
}
foreach ($v as $k => $w) {
$keyRaw = $k . '_RAW';
$v[$keyRaw] = $v[$k];
$v[$k] = $this->format($v[$k]);
if (is_array($v)) {
foreach ($v as $k => $w) {
$keyRaw = $k . '_RAW';
$v[$keyRaw] = $v[$k];
$v[$k] = $this->format($v[$k]);
}
}
$newList[] = $v;
}
$data[$field] = $newList;
@@ -148,6 +154,14 @@ class Htmlizer
if (array_key_exists($field, $data)) {
$keyRaw = $field . '_RAW';
$data[$keyRaw] = $data[$field];
$fieldType = $this->getFieldType($entity->getEntityType(), $field);
if ($fieldType === 'enum') {
if ($this->language) {
$data[$field] = $this->language->translateOption($data[$field], $field, $entity->getEntityType());
}
}
$data[$field] = $this->format($data[$field]);
}
}
@@ -269,4 +283,9 @@ class Htmlizer
return $html;
}
protected function getFieldType($entityType, $field) {
if (!$this->metadata) return;
return $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']);
}
}
+1 -1
View File
@@ -351,7 +351,7 @@ class Importer
if ($email->get('messageId')) {
$duplicate = $this->getEntityManager()->getRepository('Email')->select(['id'])->where(array(
'messageId' => $email->get('messageId')
))->findOne();
))->findOne(['skipAdditionalSelectParams' => true]);
if ($duplicate) {
return $duplicate;
}
+1 -1
View File
@@ -93,7 +93,7 @@ class Base implements Injectable
public function process(Entity $entity)
{
if (!$entity->get('assignedUserId')) return;
if (!$entity->isFieldChanged('assignedUserId')) return;
if (!$entity->isAttributeChanged('assignedUserId')) return;
$assignedUserId = $entity->get('assignedUserId');
+37 -15
View File
@@ -52,7 +52,13 @@ class Entity extends \Espo\ORM\Entity
if (!$this->entityManager->hasRepository($parentType)) return;
$repository = $this->entityManager->getRepository($parentType);
$foreignEntity = $repository->select(['id', 'name'])->where(['id' => $parentId])->findOne();
$select = ['id', 'name'];
if ($parentType === 'Lead') {
$select[] = 'accountName';
$select[] = 'emailAddress';
$select[] = 'phoneNumber';
}
$foreignEntity = $repository->select($select)->where(['id' => $parentId])->findOne();
if ($foreignEntity) {
$this->set($field . 'Name', $foreignEntity->get('name'));
}
@@ -68,26 +74,42 @@ class Entity extends \Espo\ORM\Entity
$defs['additionalColumns'] = $columns;
}
$idsAttribute = $field . 'Ids';
$foreignEntityType = $this->getRelationParam($field, 'entity');
if ($foreignEntityType && $this->entityManager) {
$foreignEntityDefs = $this->entityManager->getMetadata()->get($foreignEntityType);
if ($foreignEntityDefs && !empty($foreignEntityDefs['collection'])) {
$collectionDefs = $foreignEntityDefs['collection'];
if (!empty($foreignEntityDefs['collection']['orderBy'])) {
$orderBy = $foreignEntityDefs['collection']['orderBy'];
$order = 'ASC';
if (array_key_exists('order', $foreignEntityDefs['collection'])) {
$order = $foreignEntityDefs['collection']['order'];
}
if (array_key_exists($orderBy, $foreignEntityDefs['fields'])) {
$defs['orderBy'] = $orderBy;
$defs['order'] = $order;
if ($this->getAttributeParam($idsAttribute, 'orderBy')) {
$defs['orderBy'] = $this->getAttributeParam($idsAttribute, 'orderBy');
$defs['order'] = 'ASC';
if ($this->getAttributeParam($idsAttribute, 'orderDirection')) {
$defs['order'] = $this->getAttributeParam($idsAttribute, 'orderDirection');
}
} else {
if ($foreignEntityType && $this->entityManager) {
$foreignEntityDefs = $this->entityManager->getMetadata()->get($foreignEntityType);
if ($foreignEntityDefs && !empty($foreignEntityDefs['collection'])) {
$collectionDefs = $foreignEntityDefs['collection'];
if (!empty($foreignEntityDefs['collection']['orderBy'])) {
$orderBy = $foreignEntityDefs['collection']['orderBy'];
$order = 'ASC';
if (array_key_exists('order', $foreignEntityDefs['collection'])) {
$order = $foreignEntityDefs['collection']['order'];
}
if (array_key_exists($orderBy, $foreignEntityDefs['fields'])) {
$defs['orderBy'] = $orderBy;
$defs['order'] = $order;
}
}
}
}
}
$defs['select'] = ['id', 'name'];
if ($foreignEntityType === 'Lead') {
$defs['select'][] = 'accountName';
$defs['select'][] = 'emailAddress';
$defs['select'][] = 'phoneNumber';
}
$hasType = false;
if ($this->hasField($field . 'Types')) {
@@ -120,7 +142,7 @@ class Entity extends \Espo\ORM\Entity
}
}
$this->set($field . 'Ids', $ids);
$this->set($idsAttribute, $ids);
$this->set($field . 'Names', $names);
if ($hasType) {
$this->set($field . 'Types', $types);
+43 -3
View File
@@ -40,7 +40,9 @@ use \Espo\Core\Interfaces\Injectable;
class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
{
protected $dependencies = array(
'metadata'
'metadata',
'config',
'fieldManagerUtil'
);
protected $injections = array();
@@ -51,6 +53,8 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
protected $processFieldsAfterSaveDisabled = false;
protected $processFieldsBeforeSaveDisabled = false;
protected function addDependency($name)
{
$this->dependencies[] = $name;
@@ -83,6 +87,16 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
return $this->getInjection('metadata');
}
protected function getConfig()
{
return $this->getInjection('config');
}
protected function getFieldManagerUtil()
{
return $this->getInjection('fieldManagerUtil');
}
public function __construct($entityType, EntityManager $entityManager, EntityFactory $entityFactory)
{
parent::__construct($entityType, $entityManager, $entityFactory);
@@ -254,6 +268,10 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
if (!$this->hooksDisabled && empty($options['skipHooks'])) {
$this->getEntityManager()->getHookManager()->process($this->entityType, 'beforeSave', $entity, $options);
}
if (!$this->processFieldsBeforeSaveDisabled) {
$this->processCurrencyFieldsBeforeSave($entity);
}
}
protected function afterSave(Entity $entity, array $options = array())
@@ -326,6 +344,28 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
return $result;
}
protected function getFieldByTypeList($type)
{
return $this->getFieldManagerUtil()->getFieldByTypeList($this->entityType, $type);
}
protected function processCurrencyFieldsBeforeSave(Entity $entity)
{
foreach ($this->getFieldByTypeList('currency') as $field) {
$currencyAttribute = $field . 'Currency';
$defaultCurrency = $this->getConfig()->get('defaultCurrency');
if ($entity->isNew()) {
if ($entity->get($field) && !$entity->get($currencyAttribute)) {
$entity->set($currencyAttribute, $defaultCurrency);
}
} else {
if ($entity->isAttributeChanged($field) && $entity->has($currencyAttribute) && !$entity->get($currencyAttribute)) {
$entity->set($currencyAttribute, $defaultCurrency);
}
}
}
}
protected function processFileFieldsSave(Entity $entity)
{
foreach ($entity->getRelations() as $name => $defs) {
@@ -368,14 +408,14 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable
protected function processEmailAddressSave(Entity $entity)
{
if ($entity->hasRelation('emailAddresses') && $entity->hasAttribute('emailAddress')) {
$emailAddressRepository = $this->getEntityManager()->getRepository('EmailAddress')->storeEntityEmailAddress($entity);
$this->getEntityManager()->getRepository('EmailAddress')->storeEntityEmailAddress($entity);
}
}
protected function processPhoneNumberSave(Entity $entity)
{
if ($entity->hasRelation('phoneNumbers') && $entity->hasAttribute('phoneNumber')) {
$emailAddressRepository = $this->getEntityManager()->getRepository('PhoneNumber')->storeEntityPhoneNumber($entity);
$this->getEntityManager()->getRepository('PhoneNumber')->storeEntityPhoneNumber($entity);
}
}
@@ -41,6 +41,8 @@ class AclManager extends \Espo\Core\AclManager
private $portal = null;
protected $userAclClassName = '\\Espo\\Core\\Portal\\Acl';
public function getImplementation($scope)
{
if (empty($this->implementationHashMap[$scope])) {
@@ -41,7 +41,7 @@ class CategoryTree extends \Espo\Core\ORM\Repositories\RDB
$query = $this->getEntityManager()->getQuery();
$parentId = $entity->get('parentId');
$pathsTableName = $query->toDb($entity->getEntityType() . 'Path');
$pathsTableName = $query->toDb($query->sanitize($entity->getEntityType()) . 'Path');
if ($entity->isNew()) {
if ($parentId) {
@@ -62,7 +62,7 @@ class CategoryTree extends \Espo\Core\ORM\Repositories\RDB
}
$pdo->query($sql);
} else {
if ($entity->isFieldChanged('parentId')) {
if ($entity->isAttributeChanged('parentId')) {
$sql = "
DELETE a FROM `".$pathsTableName."` AS a
JOIN `".$pathsTableName."` AS d ON a.descendor_id = d.descendor_id
@@ -93,7 +93,7 @@ class CategoryTree extends \Espo\Core\ORM\Repositories\RDB
$pdo = $this->getEntityManager()->getPDO();
$query = $this->getEntityManager()->getQuery();
$pathsTableName = $query->toDb($entity->getEntityType() . 'Path');
$pathsTableName = $query->toDb($query->sanitize($entity->getEntityType()) . 'Path');
$sql = "DELETE FROM `".$pathsTableName."` WHERE descendor_id = ".$pdo->quote($entity->id)."";
$pdo->query($sql);
+14 -3
View File
@@ -32,6 +32,7 @@ namespace Espo\Core;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Utils\Util;
use \Espo\Core\InjectableFactory;
class SelectManagerFactory
{
@@ -43,7 +44,9 @@ class SelectManagerFactory
private $metadata;
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Utils\Metadata $metadata, Utils\Config $config)
private $injectableFactory;
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Utils\Metadata $metadata, Utils\Config $config, InjectableFactory $injectableFactory)
{
$this->entityManager = $entityManager;
$this->user = $user;
@@ -51,9 +54,10 @@ class SelectManagerFactory
$this->aclManager = $aclManager;
$this->metadata = $metadata;
$this->config = $config;
$this->injectableFactory = $injectableFactory;
}
public function create($entityType)
public function create($entityType, $user = null)
{
$normalizedName = Util::normilizeClassName($entityType);
@@ -70,7 +74,14 @@ class SelectManagerFactory
}
}
$selectManager = new $className($this->entityManager, $this->user, $this->acl, $this->aclManager, $this->metadata, $this->config);
if ($user) {
$acl = $this->aclManager->createUserAcl($user);
} else {
$acl = $this->acl;
$user = $this->user;
}
$selectManager = new $className($this->entityManager, $user, $acl, $this->aclManager, $this->metadata, $this->config, $this->injectableFactory);
$selectManager->setEntityType($entityType);
return $selectManager;
+35 -9
View File
@@ -36,6 +36,7 @@ use \Espo\Core\Acl;
use \Espo\Core\AclManager;
use \Espo\Core\Utils\Metadata;
use \Espo\Core\Utils\Config;
use \Espo\Core\InjectableFactory;
class Base
{
@@ -63,15 +64,15 @@ class Base
const MIN_LENGTH_FOR_CONTENT_SEARCH = 4;
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Metadata $metadata, Config $config)
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Metadata $metadata, Config $config, InjectableFactory $injectableFactory)
{
$this->entityManager = $entityManager;
$this->user = $user;
$this->acl = $acl;
$this->aclManager = $aclManager;
$this->metadata = $metadata;
$this->config = $config;
$this->injectableFactory = $injectableFactory;
}
protected function getEntityManager()
@@ -104,6 +105,11 @@ class Base
return $this->aclManager;
}
protected function getInjectableFactory()
{
return $this->injectableFactory;
}
public function setEntityType($entityType)
{
$this->entityType = $entityType;
@@ -186,7 +192,6 @@ class Base
{
$this->prepareResult($result);
$whereClause = array();
foreach ($where as $item) {
if (!isset($item['type'])) continue;
@@ -207,10 +212,17 @@ class Base
}
}
$whereClause = $this->convertWhere($where, false, $result);
$result['whereClause'] = array_merge($result['whereClause'], $whereClause);
}
public function convertWhere(array $where, $ignoreAdditionaFilterTypes = false, &$result = null)
{
$whereClause = [];
$ignoreTypeList = array_merge(['bool', 'primary'], $this->additionalFilterTypeList);
$additionalFilters = array();
foreach ($where as $item) {
if (!isset($item['type'])) continue;
@@ -221,7 +233,7 @@ class Base
$whereClause[] = $part;
}
} else {
if (in_array($type, $this->additionalFilterTypeList)) {
if (!$ignoreAdditionaFilterTypes && in_array($type, $this->additionalFilterTypeList)) {
if (!empty($item['value'])) {
$methodName = 'apply' . ucfirst($type);
@@ -242,7 +254,7 @@ class Base
}
}
$result['whereClause'] = array_merge($result['whereClause'], $whereClause);
return $whereClause;
}
protected function applyLinkedWith($link, $idsValue, &$result)
@@ -1327,6 +1339,20 @@ class Base
$method = 'filter' . ucfirst($filterName);
if (method_exists($this, $method)) {
$this->$method($result);
} else {
$className = $this->getMetadata()->get(['entityDefs', $this->entityType, 'collection', 'filters', $filterName, 'className']);
if ($className) {
if (!class_exists($className)) {
$GLOBALS['log']->error("Could find class for filter {$filterName}.");
return;
}
$impl = $this->getInjectableFactory()->createByClassName($className);
if (!$impl) {
$GLOBALS['log']->error("Could not create filter {$filterName} implementation.");
return;
}
$impl->applyFilter($this->entityType, $filterName, $result, $this);
}
}
}
@@ -1480,10 +1506,11 @@ class Base
$fieldList = $this->getTextFilterFieldList();
$d = array();
$textFilterContainsMinLength = $this->getConfig()->get('textFilterContainsMinLength', self::MIN_LENGTH_FOR_CONTENT_SEARCH);
foreach ($fieldList as $field) {
$expression = $textFilter . '%';
if (
strlen($textFilter) >= self::MIN_LENGTH_FOR_CONTENT_SEARCH
strlen($textFilter) >= $textFilterContainsMinLength
&&
(
!empty($fieldDefs[$field]['type']) && $fieldDefs[$field]['type'] == 'text'
@@ -1563,4 +1590,3 @@ class Base
$this->filterFollowed($result);
}
}
@@ -49,7 +49,7 @@ class Person extends \Espo\Services\Record
if (
($entity->get('emailAddress') || $entity->get('emailAddressData'))
&&
($entity->isNew() || $entity->isFieldChanged('emailAddress') || $entity->isFieldChanged('emailAddressData'))
($entity->isNew() || $entity->isAttributeChanged('emailAddress') || $entity->isAttributeChanged('emailAddressData'))
) {
if ($entity->get('emailAddress')) {
$list = [$entity->get('emailAddress')];
@@ -1,5 +1,5 @@
{
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
@@ -1,5 +1,5 @@
{
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
@@ -1,5 +1,5 @@
{
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
@@ -1,5 +1,5 @@
{
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
@@ -1,5 +1,5 @@
{
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
"labels": {
"Create {entityType}": "Creare {entityTypeTranslated}"
}
}
+1
View File
@@ -47,6 +47,7 @@ class UpgradeManager extends Upgrades\Base
'customDirNames' => array(
'before' => 'beforeUpgradeFiles',
'after' => 'afterUpgradeFiles',
'vendor' => 'vendorFiles',
)
);
}
+74 -28
View File
@@ -28,6 +28,7 @@
************************************************************************/
namespace Espo\Core\Upgrades\Actions;
use Espo\Core\Utils\Util;
use Espo\Core\Utils\System;
use Espo\Core\Utils\Json;
@@ -83,6 +84,7 @@ abstract class Base
*/
protected $defaultPackageType = 'extension';
protected $vendorDirName = 'vendor';
public function __construct(\Espo\Core\Container $container, \Espo\Core\Upgrades\ActionManager $actionManager)
{
@@ -315,8 +317,17 @@ abstract class Base
{
$manifest = $this->getManifest();
if (isset($manifest[$type])) {
return $manifest[$type];
switch ($type) {
case 'delete':
case 'deleteBeforeCopy':
if (isset($manifest[$type])) {
return $manifest[$type];
}
break;
case 'vendor':
return $this->getVendorFileList('delete');
break;
}
return array();
@@ -332,7 +343,7 @@ abstract class Base
if (!isset($this->data['deleteFileList'])) {
$deleteFileList = array();
$deleteList = array_merge($this->getDeleteList('delete'), $this->getDeleteList('deleteBeforeCopy'));
$deleteList = array_merge($this->getDeleteList('delete'), $this->getDeleteList('deleteBeforeCopy'), $this->getDeleteList('vendor'));
foreach ($deleteList as $key => $itemPath) {
if (is_dir($itemPath)) {
$fileList = $this->getFileManager()->getFileList($itemPath, true, '', true, true);
@@ -356,9 +367,9 @@ abstract class Base
*
* @return boolen
*/
protected function deleteFiles($withEmptyDirs = false)
protected function deleteFiles($type = 'delete', $withEmptyDirs = false)
{
$deleteList = $this->getDeleteList('delete');
$deleteList = $this->getDeleteList($type);
if (!empty($deleteList)) {
return $this->getFileManager()->remove($deleteList, null, $withEmptyDirs);
@@ -367,24 +378,6 @@ abstract class Base
return true;
}
/**
* Deleted file/forder list before coppy the upgrade files
*
* @param boolean $withEmptyDirs
*
* @return boolean
*/
protected function deleteBeforeCopy($withEmptyDirs = false)
{
$deleteList = $this->getDeleteList('deleteBeforeCopy');
if (!empty($deleteList)) {
$this->getFileManager()->remove($deleteList, null, $withEmptyDirs);
}
return true;
}
protected function getCopyFileList()
{
if (!isset($this->data['fileList'])) {
@@ -445,6 +438,12 @@ abstract class Base
}
}
//vendor file list
$vendorFileList = $this->getVendorFileList('copy');
if (!empty($vendorFileList)) {
$fileList = array_merge($fileList, $vendorFileList);
}
return $fileList;
}
@@ -466,7 +465,7 @@ abstract class Base
*
* @return boolean
*/
protected function copyFiles($type = null)
protected function copyFiles($type = null, $dest = '')
{
switch ($type) {
case 'before':
@@ -475,21 +474,63 @@ abstract class Base
$dirPath = $dirNames[$type];
break;
case 'vendor':
$dirNames = $this->getParams('customDirNames');
if (isset($dirNames['vendor'])) {
$dirPath = $dirNames['vendor'];
$dest = $this->vendorDirName;
}
break;
default:
$dirPath = self::FILES;
break;
}
$packagePath = $this->getPackagePath();
$filesPath = Util::concatPath($packagePath, $dirPath);
if (isset($dirPath)) {
$packagePath = $this->getPackagePath();
$filesPath = Util::concatPath($packagePath, $dirPath);
if (file_exists($filesPath)) {
return $this->copy($filesPath, '', true);
if (file_exists($filesPath)) {
return $this->copy($filesPath, $dest, true);
}
}
return true;
}
protected function getVendorFileList($type = 'copy')
{
$list = [];
$packagePath = $this->getPackagePath();
$dirNames = $this->getParams('customDirNames');
if (!isset($dirNames['vendor'])) {
return $list;
}
$filesPath = Util::concatPath($packagePath, $dirNames['vendor']);
if (!file_exists($filesPath)) {
return $list;
}
switch ($type) {
case 'copy':
$list = $this->getFileManager()->getFileList($filesPath, true, '', true, true);
break;
case 'delete':
$list = $this->getFileManager()->getFileList($filesPath, false, '', null, true);
break;
}
foreach ($list as &$path) {
$path = Util::concatPath($this->vendorDirName, $path);
}
return $list;
}
public function getManifest()
{
if (!isset($this->data['manifest'])) {
@@ -515,6 +556,11 @@ abstract class Base
return $this->data['manifest'];
}
protected function setManifest()
{
}
/**
* Check if the manifest is correct
*
@@ -76,7 +76,7 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
$this->runScript('before');
/* remove files defined in a manifest "deleteBeforeCopy" */
$this->deleteBeforeCopy(true);
$this->deleteFiles('deleteBeforeCopy', true);
/* copy files from directory "Files" to EspoCRM files */
if (!$this->copyFiles()) {
@@ -84,7 +84,10 @@ class Install extends \Espo\Core\Upgrades\Actions\Base
}
/* remove files defined in a manifest */
$this->deleteFiles(true);
$this->deleteFiles('delete', true);
$this->deleteFiles('vendor');
$this->copyFiles('vendor');
if (!$this->systemRebuild()) {
$this->throwErrorAndRemovePackage('Error occurred while EspoCRM rebuild.');
@@ -66,7 +66,7 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
}
/* remove extension files, saved in fileList */
if (!$this->deleteFiles(true)) {
if (!$this->deleteFiles('delete', true)) {
$this->throwErrorAndRemovePackage('Permission denied to delete files.');
}
}
@@ -119,10 +119,10 @@ class Uninstall extends \Espo\Core\Upgrades\Actions\Base
return $res;
}
protected function copyFiles($type = null)
protected function copyFiles($type = null, $dest = '')
{
$backupPath = $this->getPath('backupPath');
$res = $this->copy(array($backupPath, self::FILES), '', true);
$res = $this->copy(array($backupPath, self::FILES), $dest, true);
return $res;
}
@@ -158,8 +158,8 @@ class AdminNotificationManager
$query = "
SELECT version FROM extension
WHERE name='". $extensionName ."'
AND deleted=0
WHERE name = ". $pdo->quote($extensionName) ."
AND deleted = 0
AND is_installed = 1
ORDER BY created_at DESC
";
@@ -133,7 +133,6 @@ class MySqlPlatform extends \Doctrine\DBAL\Platforms\MySqlPlatform
);
}
return array_merge($sql, $tableSql, $columnSql);
}
@@ -305,10 +304,23 @@ class MySqlPlatform extends \Doctrine\DBAL\Platforms\MySqlPlatform
protected function _getCreateTableSQL($tableName, array $columns, array $options = array())
{
if (!isset($options['engine'])) {
$options['engine'] = 'MyISAM';
$options['engine'] = 'InnoDB';
}
if (!isset($options['charset'])) {
$options['charset'] = 'utf8mb4';
}
if (!isset($options['collate'])) {
$options['collate'] = 'utf8mb4_unicode_ci';
}
return parent::_getCreateTableSQL($tableName, $columns, $options);
}
public function getColumnCollationDeclarationSQL($collation)
{
return $this->getCollationFieldDeclaration($collation);
}
//end: ESPO
}
@@ -275,7 +275,8 @@ class Converter
)
);
foreach($entityMetadata['fields'] as $fieldName => $fieldParams) {
foreach ($entityMetadata['fields'] as $fieldName => $fieldParams) {
if (empty($fieldParams['type'])) continue;
/** check if "fields" option exists in $fieldMeta */
$fieldTypeMetadata = $this->getMetadataHelper()->getFieldDefsByType($fieldParams);
@@ -44,17 +44,26 @@ class LinkMultiple extends Base
),
$fieldName.'Names' => array(
'type' => 'varchar',
'notStorable' => true,
),
),
'notStorable' => true
)
)
),
'unset' => array(
$entityName => array(
'fields.'.$fieldName,
),
),
'fields.'.$fieldName
)
)
);
$fieldParams = $this->getFieldParams();
if (array_key_exists('orderBy', $fieldParams)) {
$data[$entityName]['fields'][$fieldName . 'Ids']['orderBy'] = $fieldParams['orderBy'];
if (array_key_exists('orderDirection', $fieldParams)) {
$data[$entityName]['fields'][$fieldName . 'Ids']['orderDirection'] = $fieldParams['orderDirection'];
}
}
$columns = $this->getMetadata()->get("entityDefs.{$entityName}.fields.{$fieldName}.columns");
if (!empty($columns)) {
$data[$entityName]['fields'][$fieldName . 'Columns'] = array(
@@ -28,15 +28,18 @@
************************************************************************/
namespace Espo\Core\Utils\Database\Schema;
use Espo\Core\Utils\Util,
Espo\ORM\Entity,
Espo\Core\Exceptions\Error;
use Espo\Core\Utils\Util;
use Espo\ORM\Entity;
use Espo\Core\Exceptions\Error;
use Espo\Core\Utils\Database\Schema\Utils as SchemaUtils;
class Converter
{
private $dbalSchema;
private $databaseSchema;
private $fileManager;
private $metadata;
@@ -76,10 +79,11 @@ class Converter
'foreign'
);
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager)
public function __construct(\Espo\Core\Utils\Metadata $metadata, \Espo\Core\Utils\File\Manager $fileManager, \Espo\Core\Utils\Database\Schema\Schema $databaseSchema)
{
$this->metadata = $metadata;
$this->fileManager = $fileManager;
$this->databaseSchema = $databaseSchema;
$this->typeList = array_keys(\Doctrine\DBAL\Types\Type::getTypesMap());
}
@@ -110,6 +114,11 @@ class Converter
return $this->dbalSchema;
}
protected function getDatabaseSchema()
{
return $this->databaseSchema;
}
/**
* Schema convertation process
*
@@ -154,6 +163,9 @@ class Converter
$schema = $this->getSchema(true);
$indexList = SchemaUtils::getIndexList($ormMeta);
$fieldListExceededIndexMaxLength = SchemaUtils::getFieldListExceededIndexMaxLength($ormMeta, $this->getDatabaseSchema()->getMaxIndexLength());
$tables = array();
foreach ($ormMeta as $entityName => $entityParams) {
@@ -177,7 +189,7 @@ class Converter
$primaryColumns = array();
$uniqueColumns = array();
$indexList = array(); //list of indexes like array( array(comlumn1, column2), array(column3))
foreach ($entityParams['fields'] as $fieldName => $fieldParams) {
if ((isset($fieldParams['notStorable']) && $fieldParams['notStorable']) || in_array($fieldParams['type'], $this->notStorableTypes)) {
@@ -197,37 +209,26 @@ class Converter
continue;
}
if (isset($fieldListExceededIndexMaxLength[$entityName]) && in_array($fieldName, $fieldListExceededIndexMaxLength[$entityName])) {
$fieldParams['utf8mb3'] = true;
}
$columnName = Util::toUnderScore($fieldName);
if (!$tables[$entityName]->hasColumn($columnName)) {
$tables[$entityName]->addColumn($columnName, $fieldType, $this->getDbFieldParams($fieldParams));
}
//add unique
if ($fieldParams['type']!= 'id' && isset($fieldParams['unique'])) {
if ($fieldParams['type'] != 'id' && isset($fieldParams['unique'])) {
$uniqueColumns = $this->getKeyList($columnName, $fieldParams['unique'], $uniqueColumns);
} //END: add unique
//add index. It can be defined in entityDefs as "index"
if (isset($fieldParams['index'])) {
$indexList = $this->getKeyList($columnName, $fieldParams['index'], $indexList);
} //END: add index
}
$tables[$entityName]->setPrimaryKey($primaryColumns);
//add indexes
if (isset($entityParams['indexes']) && is_array($entityParams['indexes'])) {
foreach ($entityParams['indexes'] as $indexName => $indexParams) {
if (is_array($indexParams['columns'])) {
$tableIndexName = $this->generateIndexName($indexName, $entityName);
$indexList[$tableIndexName] = Util::toUnderScore($indexParams['columns']);
}
}
}
if (!empty($indexList)) {
foreach($indexList as $indexName => $indexItem) {
$tableIndexName = is_string($indexName) ? $indexName : null;
$tables[$entityName]->addIndex($indexItem, $tableIndexName);
if (!empty($indexList[$entityName])) {
foreach($indexList[$entityName] as $indexName => $indexColumnList) {
$tables[$entityName]->addIndex($indexColumnList, $indexName);
}
}
@@ -285,14 +286,21 @@ class Converter
}
$table = $this->getSchema()->createTable($tableName);
$table->addColumn('id', 'int', array('length'=>$this->defaultLength['int'], 'autoincrement' => true, 'notnull' => true,)); //'unique' => true,
$table->addColumn('id', 'int', $this->getDbFieldParams(array(
'type' => 'int',
'len' => $this->defaultLength['int'],
'autoincrement' => true,
)));
//add midKeys to a schema
$uniqueIndex = array();
foreach($relationParams['midKeys'] as $index => $midKey) {
$columnName = Util::toUnderScore($midKey);
$table->addColumn($columnName, $this->idParams['dbType'], array('length'=>$this->idParams['len']));
$table->addColumn($columnName, $this->idParams['dbType'], $this->getDbFieldParams(array(
'type' => 'foreignId',
'len' => $this->idParams['len'],
)));
$table->addIndex(array($columnName));
$uniqueIndex[] = $columnName;
@@ -306,7 +314,7 @@ class Converter
if (!isset($fieldParams['type'])) {
$fieldParams = array_merge($fieldParams, array(
'type' => 'varchar',
'length' => $this->defaultLength['varchar'],
'len' => $this->defaultLength['varchar'],
));
}
@@ -326,7 +334,11 @@ class Converter
}
//END: add unique indexes
$table->addColumn('deleted', 'bool', array('default' => 0));
$table->addColumn('deleted', 'bool', $this->getDbFieldParams(array(
'type' => 'bool',
'default' => false,
)));
$table->setPrimaryKey(array("id"));
return $table;
@@ -337,13 +349,18 @@ class Converter
$dbFieldParams = array();
foreach($this->allowedDbFieldParams as $espoName => $dbalName) {
if (isset($fieldParams[$espoName])) {
$dbFieldParams[$dbalName] = $fieldParams[$espoName];
}
}
switch ($fieldParams['type']) {
case 'id':
case 'foreignId':
case 'foreignType':
$fieldParams['utf8mb3'] = true;
break;
case 'array':
case 'jsonArray':
case 'text':
@@ -360,12 +377,17 @@ class Converter
break;
}
if ( isset($fieldParams['autoincrement']) && $fieldParams['autoincrement'] ) {
if (isset($fieldParams['autoincrement']) && $fieldParams['autoincrement']) {
$dbFieldParams['unique'] = true;
$dbFieldParams['notnull'] = true;
}
if (isset($fieldParams['utf8mb3']) && $fieldParams['utf8mb3']) {
$dbFieldParams['platformOptions'] = array(
'collation' => 'utf8_unicode_ci',
);
}
return $dbFieldParams;
}
@@ -378,9 +400,11 @@ class Converter
protected function getKeyList($columnName, $keyValue, array $keyList)
{
if ($keyValue === true) {
$keyList[] = array($columnName);
$tableIndexName = SchemaUtils::generateIndexName($columnName);
$keyList[$tableIndexName] = array($columnName);
} else if (is_string($keyValue)) {
$keyList[$keyValue][] = $columnName;
$tableIndexName = SchemaUtils::generateIndexName($keyValue);
$keyList[$tableIndexName][] = $columnName;
}
return $keyList;
@@ -451,23 +475,6 @@ class Converter
return $dependentEntities;
}
/**
* Generate index name
*
* @return string
*/
protected function generateIndexName($name, $entityName)
{
$names = array(
'IDX',
);
$names[] = strtoupper( Util::toUnderScore($entityName) );
$names[] = strtoupper( Util::toUnderScore($name) );
return implode('_', $names);
}
protected function loadData($path)
{
$tables = array();
@@ -487,4 +494,4 @@ class Converter
return $tables;
}
}
}
@@ -94,7 +94,7 @@ class Schema
$this->converter = new \Espo\Core\Utils\Database\Converter($this->metadata, $this->fileManager);
$this->schemaConverter = new Converter($this->metadata, $this->fileManager);
$this->schemaConverter = new Converter($this->metadata, $this->fileManager, $this);
$this->ormMetadata = $ormMetadata;
}
@@ -312,5 +312,45 @@ class Schema
}
}
public function getMaxIndexLength()
{
$connection = $this->getConnection();
$mysqlEngine = $this->getMysqlEngine();
switch ($mysqlEngine) {
case 'InnoDB':
$mysqlVersion = $this->getMysqlVersion();
if (version_compare($mysqlVersion, '10.0.0') >= 0) {
return 767; //InnoDB, MariaDB
}
if (version_compare($mysqlVersion, '5.7.0') >= 0) {
return 3072; //InnoDB, MySQL 5.7+
}
return 767; //InnoDB
break;
}
return 1000; //MyISAM
}
protected function getMysqlVersion()
{
$connection = $this->getConnection();
return $connection->fetchColumn("select version()");
}
protected function getMysqlEngine()
{
$connection = $this->getConnection();
$result = $connection->fetchColumn("SHOW TABLE STATUS WHERE Engine = 'MyISAM'");
if (!empty($result)) {
return 'MyISAM';
}
return 'InnoDB';
}
}
@@ -0,0 +1,166 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\Schema;
use Espo\Core\Utils\Util;
class Utils
{
public static function getIndexList(array $ormMeta)
{
$indexList = array();
foreach ($ormMeta as $entityName => $entityParams) {
foreach ($entityParams['fields'] as $fieldName => $fieldParams) {
if (isset($fieldParams['notStorable']) && $fieldParams['notStorable']) {
continue;
}
if (isset($fieldParams['index'])) {
$keyValue = $fieldParams['index'];
$columnName = Util::toUnderScore($fieldName);
if (!isset($indexList[$entityName])) {
$indexList[$entityName] = [];
}
if ($keyValue === true) {
$tableIndexName = static::generateIndexName($columnName);
$indexList[$entityName][$tableIndexName] = array($columnName);
} else if (is_string($keyValue)) {
$tableIndexName = static::generateIndexName($keyValue);
$indexList[$entityName][$tableIndexName][] = $columnName;
}
}
}
if (isset($entityParams['indexes']) && is_array($entityParams['indexes'])) {
foreach ($entityParams['indexes'] as $indexName => $indexParams) {
if (is_array($indexParams['columns'])) {
$tableIndexName = static::generateIndexName($indexName);
$indexList[$entityName][$tableIndexName] = Util::toUnderScore($indexParams['columns']);
}
}
}
}
return $indexList;
}
public static function generateIndexName($name, $prefix = 'IDX', $maxLength = 30)
{
$nameList = [];
$nameList[] = strtoupper($prefix);
$nameList[] = strtoupper( Util::toUnderScore($name) );
return substr(implode('_', $nameList), 0, $maxLength);
}
public static function getFieldListExceededIndexMaxLength(array $ormMeta, $indexMaxLength = 1000, $characterLength = 4)
{
$permittedFieldTypeList = [
'varchar',
];
$fields = array();
$indexList = static::getIndexList($ormMeta);
foreach ($indexList as $entityName => $indexes) {
foreach ($indexes as $indexName => $columnList) {
$indexLength = 0;
foreach ($columnList as $columnName) {
$fieldName = Util::toCamelCase($columnName);
if (!isset($ormMeta[$entityName]['fields'][$fieldName])) {
continue;
}
$indexLength += static::getFieldLength($ormMeta[$entityName]['fields'][$fieldName], $characterLength);
}
if ($indexLength > $indexMaxLength) {
foreach ($columnList as $columnName) {
$fieldName = Util::toCamelCase($columnName);
if (!isset($ormMeta[$entityName]['fields'][$fieldName])) {
continue;
}
$fieldType = static::getFieldType($ormMeta[$entityName]['fields'][$fieldName]);
if (in_array($fieldType, $permittedFieldTypeList)) {
if (!isset($fields[$entityName]) || !in_array($fieldName, $fields[$entityName])) {
$fields[$entityName][] = $fieldName;
}
}
}
}
}
}
return $fields;
}
protected static function getFieldLength(array $ormFieldDefs, $characterLength = 4)
{
$length = 0;
if (isset($ormFieldDefs['notStorable']) && $ormFieldDefs['notStorable']) {
return $length;
}
$defaultLength = array(
'datetime' => 8,
'time' => 4,
'int' => 4,
'bool' => 1,
'float' => 4,
'varchar' => 255,
);
$type = static::getFieldType($ormFieldDefs);
$length = isset($defaultLength[$type]) ? $defaultLength[$type] : $length;
$length = isset($ormFieldDefs['len']) ? $ormFieldDefs['len'] : $length;
switch ($type) {
case 'varchar':
$length = $length * $characterLength;
break;
}
return $length;
}
protected static function getFieldType(array $ormFieldDefs)
{
return isset($ormFieldDefs['dbType']) ? $ormFieldDefs['dbType'] : $ormFieldDefs['type'];
}
}
@@ -28,6 +28,7 @@
************************************************************************/
namespace Espo\Core\Utils\Database\Schema\rebuildActions;
class Currency extends \Espo\Core\Utils\Database\Schema\BaseRebuildActions
{
+179 -29
View File
@@ -30,6 +30,7 @@
namespace Espo\Core\Utils;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\BadRequest;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\Conflict;
use \Espo\Core\Utils\Json;
@@ -80,6 +81,11 @@ class EntityManager
return $this->language;
}
protected function getBaseLanguage()
{
return $this->container->get('baseLanguage');
}
protected function getFileManager()
{
return $this->fileManager;
@@ -122,17 +128,36 @@ class EntityManager
return false;
}
public function create($name, $type, $params = array())
public function create($name, $type, $params = [], $replaceData = [])
{
$name = ucfirst($name);
$name = trim($name);
if (empty($name) || empty($type)) {
throw new BadRequest();
}
if (strlen($name) > 100) {
throw new Error('Entity name should not be longer than 100.');
}
if (is_numeric($name[0])) {
throw new Error('Bad entity name.');
}
if (!in_array($type, $this->getMetadata()->get(['app', 'entityTemplateList'], []))) {
throw new Error('Type \''.$type.'\' does not exist.');
}
$templateDefs = $this->getMetadata()->get(['app', 'entityTemplates', $type], []);
if (!empty($templateDefs['isNotCreatable']) && empty($params['forceCreate'])) {
throw new Error('Type \''.$type.'\' is not creatable.');
}
if ($this->getMetadata()->get('scopes.' . $name)) {
throw new Conflict('Entity \''.$name.'\' already exists.');
}
if (empty($name) || empty($type)) {
throw new Error();
}
if ($this->checkControllerExists($name)) {
throw new Conflict('Entity name \''.$name.'\' is not allowed.');
@@ -149,9 +174,20 @@ class EntityManager
$normalizedName = Util::normilizeClassName($name);
$templateNamespace = "\Espo\Core\Templates";
$templatePath = "application/Espo/Core/Templates";
$templateModuleName = null;
if (!empty($templateDefs['module'])) {
$templateModuleName = $templateDefs['module'];
$normalizedTemplateModuleName = Util::normilizeClassName($templateModuleName);
$templateNamespace = "\Espo\Modules\\{$normalizedTemplateModuleName}\Core\Templates";
$templatePath = "application/Espo/Modules/".$normalizedTemplateModuleName."/Core/Templates";
}
$contents = "<" . "?" . "php\n\n".
"namespace Espo\Custom\Entities;\n\n".
"class {$normalizedName} extends \Espo\Core\Templates\Entities\\{$type}\n".
"class {$normalizedName} extends {$templateNamespace}\Entities\\{$type}\n".
"{\n".
" protected \$entityType = \"$name\";\n".
"}\n";
@@ -161,7 +197,7 @@ class EntityManager
$contents = "<" . "?" . "php\n\n".
"namespace Espo\Custom\Controllers;\n\n".
"class {$normalizedName} extends \Espo\Core\Templates\Controllers\\{$type}\n".
"class {$normalizedName} extends {$templateNamespace}\Controllers\\{$type}\n".
"{\n".
"}\n";
$filePath = "custom/Espo/Custom/Controllers/{$normalizedName}.php";
@@ -169,7 +205,7 @@ class EntityManager
$contents = "<" . "?" . "php\n\n".
"namespace Espo\Custom\Services;\n\n".
"class {$normalizedName} extends \Espo\Core\Templates\Services\\{$type}\n".
"class {$normalizedName} extends {$templateNamespace}\Services\\{$type}\n".
"{\n".
"}\n";
$filePath = "custom/Espo/Custom/Services/{$normalizedName}.php";
@@ -177,17 +213,17 @@ class EntityManager
$contents = "<" . "?" . "php\n\n".
"namespace Espo\Custom\Repositories;\n\n".
"class {$normalizedName} extends \Espo\Core\Templates\Repositories\\{$type}\n".
"class {$normalizedName} extends {$templateNamespace}\Repositories\\{$type}\n".
"{\n".
"}\n";
$filePath = "custom/Espo/Custom/Repositories/{$normalizedName}.php";
$this->getFileManager()->putContents($filePath, $contents);
if (file_exists('application/Espo/Core/Templates/SelectManagers/' . $type . '.php')) {
if (file_exists($templatePath . '/SelectManagers/' . $type . '.php')) {
$contents = "<" . "?" . "php\n\n".
"namespace Espo\Custom\SelectManagers;\n\n".
"class {$normalizedName} extends \Espo\Core\Templates\SelectManagers\\{$type}\n".
"class {$normalizedName} extends {$templateNamespace}\SelectManagers\\{$type}\n".
"{\n".
"}\n";
@@ -214,19 +250,26 @@ class EntityManager
$languageList = $this->getConfig()->get('languageList', []);
foreach ($languageList as $language) {
$filePath = 'application/Espo/Core/Templates/i18n/' . $language . '/' . $type . '.json';
$filePath = $templatePath . '/i18n/' . $language . '/' . $type . '.json';
if (!file_exists($filePath)) continue;
$languageContents = $this->getFileManager()->getContents($filePath);
$languageContents = str_replace('{entityType}', $name, $languageContents);
$languageContents = str_replace('{entityTypeTranslated}', $labelSingular, $languageContents);
foreach ($replaceData as $key => $value) {
$languageContents = str_replace('{'.$key.'}', $value, $languageContents);
}
$destinationFilePath = 'custom/Espo/Custom/Resources/i18n/' . $language . '/' . $name . '.json';
$this->getFileManager()->putContents($destinationFilePath, $languageContents);
}
$filePath = "application/Espo/Core/Templates/Metadata/{$type}/scopes.json";
$filePath = $templatePath . "/Metadata/{$type}/scopes.json";
$scopesDataContents = $this->getFileManager()->getContents($filePath);
$scopesDataContents = str_replace('{entityType}', $name, $scopesDataContents);
foreach ($replaceData as $key => $value) {
$scopesDataContents = str_replace('{'.$key.'}', $value, $scopesDataContents);
}
$scopesData = Json::decode($scopesDataContents, true);
$scopesData['stream'] = $stream;
@@ -236,28 +279,38 @@ class EntityManager
$scopesData['object'] = true;
$scopesData['isCustom'] = true;
if (!empty($templateDefs['isNotRemovable']) ||!empty($params['isNotRemovable'])) {
$scopesData['isNotRemovable'] = true;
}
$this->getMetadata()->set('scopes', $name, $scopesData);
$filePath = "application/Espo/Core/Templates/Metadata/{$type}/entityDefs.json";
$filePath = $templatePath . "/Metadata/{$type}/entityDefs.json";
$entityDefsDataContents = $this->getFileManager()->getContents($filePath);
$entityDefsDataContents = str_replace('{entityType}', $name, $entityDefsDataContents);
$entityDefsDataContents = str_replace('{tableName}', $this->getEntityManager()->getQuery()->toDb($name), $entityDefsDataContents);
foreach ($replaceData as $key => $value) {
$entityDefsDataContents = str_replace('{'.$key.'}', $value, $entityDefsDataContents);
}
$entityDefsData = Json::decode($entityDefsDataContents, true);
$this->getMetadata()->set('entityDefs', $name, $entityDefsData);
$filePath = "application/Espo/Core/Templates/Metadata/{$type}/clientDefs.json";
$filePath = $templatePath . "/Metadata/{$type}/clientDefs.json";
$clientDefsContents = $this->getFileManager()->getContents($filePath);
$clientDefsContents = str_replace('{entityType}', $name, $clientDefsContents);
foreach ($replaceData as $key => $value) {
$clientDefsContents = str_replace('{'.$key.'}', $value, $clientDefsContents);
}
$clientDefsData = Json::decode($clientDefsContents, true);
$this->getMetadata()->set('clientDefs', $name, $clientDefsData);
$this->getLanguage()->set('Global', 'scopeNames', $name, $labelSingular);
$this->getLanguage()->set('Global', 'scopeNamesPlural', $name, $labelPlural);
$this->getBaseLanguage()->set('Global', 'scopeNames', $name, $labelSingular);
$this->getBaseLanguage()->set('Global', 'scopeNamesPlural', $name, $labelPlural);
$this->getMetadata()->save();
$this->getLanguage()->save();
$this->getBaseLanguage()->save();
$layoutsPath = "application/Espo/Core/Templates/Layouts/{$type}";
$layoutsPath = $templatePath . "/Layouts/{$type}";
if ($this->getFileManager()->isDir($layoutsPath)) {
$this->getFileManager()->copy($layoutsPath, 'custom/Espo/Custom/Resources/layouts/' . $name);
}
@@ -284,6 +337,11 @@ class EntityManager
$this->getMetadata()->set('scopes', $name, $scopeData);
}
$isCustom = false;
if (!empty($scopeData['isCustom'])) {
$isCustom = true;
}
if (array_key_exists('statusField', $data)) {
$scopeData['statusField'] = $data['statusField'];
$this->getMetadata()->set('scopes', $name, $scopeData);
@@ -291,14 +349,23 @@ class EntityManager
if (!empty($data['labelSingular'])) {
$labelSingular = $data['labelSingular'];
$this->getLanguage()->set('Global', 'scopeNames', $name, $labelSingular);
$labelCreate = $this->getLanguage()->translate('Create') . ' ' . $labelSingular;
$this->getLanguage()->set('Global', 'scopeNames', $name, $labelSingular);
$this->getLanguage()->set($name, 'labels', 'Create ' . $name, $labelCreate);
if ($isCustom) {
$this->getBaseLanguage()->set('Global', 'scopeNames', $name, $labelSingular);
$this->getBaseLanguage()->set($name, 'labels', 'Create ' . $name, $labelCreate);
}
}
if (!empty($data['labelPlural'])) {
$labelPlural = $data['labelPlural'];
$this->getLanguage()->set('Global', 'scopeNamesPlural', $name, $labelPlural);
if ($isCustom) {
$this->getBaseLanguage()->set('Global', 'scopeNamesPlural', $name, $labelPlural);
}
}
if (isset($data['sortBy'])) {
@@ -322,11 +389,16 @@ class EntityManager
$this->getMetadata()->save();
$this->getLanguage()->save();
if ($isCustom) {
if ($this->getLanguage()->getLanguage() !== $this->getBaseLanguage()->getLanguage()) {
$this->getBaseLanguage()->save();
}
}
return true;
}
public function delete($name)
public function delete($name, $params = [])
{
if (!$this->isCustom($name)) {
throw new Forbidden;
@@ -336,6 +408,19 @@ class EntityManager
$type = $this->getMetadata()->get(['scopes', $name, 'type']);
$isNotRemovable = $this->getMetadata()->get(['scopes', $name, 'isNotRemovable']);
$templateDefs = $this->getMetadata()->get(['app', 'entityTemplates', $type], []);
$templateModuleName = null;
if (!empty($templateDefs['module'])) {
$templateModuleName = $templateDefs['module'];
}
if ((!empty($templateDefs['isNotRemovable']) || $isNotRemovable) && empty($params['forceRemove'])) {
throw new Error('Type \''.$type.'\' is not removable.');
}
$unsets = array(
'entityDefs',
'clientDefs',
@@ -377,11 +462,18 @@ class EntityManager
try {
$this->getLanguage()->delete('Global', 'scopeNames', $name);
$this->getLanguage()->delete('Global', 'scopeNamesPlural', $name);
$this->getBaseLanguage()->delete('Global', 'scopeNames', $name);
$this->getBaseLanguage()->delete('Global', 'scopeNamesPlural', $name);
} catch (\Exception $e) {}
$this->getMetadata()->save();
$this->getLanguage()->save();
if ($this->getLanguage()->getLanguage() !== $this->getBaseLanguage()->getLanguage()) {
$this->getBaseLanguage()->save();
}
if ($type) {
$this->processHook('afterRemove', $type, $name);
}
@@ -414,6 +506,18 @@ class EntityManager
}
}
if (empty($link) || empty($linkForeign)) {
throw new BadRequest();
}
if (strlen($link) > 255 || strlen($linkForeign) > 255) {
throw new Error('Link name should not be longer than 255.');
}
if (is_numeric($link[0]) || is_numeric($linkForeign[0])) {
throw new Error('Bad link name.');
}
$linkMultipleField = false;
if (!empty($params['linkMultipleField'])) {
$linkMultipleField = true;
@@ -619,9 +723,16 @@ class EntityManager
$this->getLanguage()->set($entity, 'links', $link, $label);
$this->getLanguage()->set($entityForeign, 'fields', $linkForeign, $labelForeign);
$this->getLanguage()->set($entityForeign, 'links', $linkForeign, $labelForeign);
$this->getLanguage()->save();
if ($this->getLanguage()->getLanguage() !== $this->getBaseLanguage()->getLanguage()) {
$this->getBaseLanguage()->set($entity, 'fields', $link, $label);
$this->getBaseLanguage()->set($entity, 'links', $link, $label);
$this->getBaseLanguage()->set($entityForeign, 'fields', $linkForeign, $labelForeign);
$this->getBaseLanguage()->set($entityForeign, 'links', $linkForeign, $labelForeign);
$this->getBaseLanguage()->save();
}
return true;
}
@@ -632,9 +743,6 @@ class EntityManager
$entityForeign = $params['entityForeign'];
$linkForeign = $params['linkForeign'];
$label = $params['label'];
$labelForeign = $params['labelForeign'];
if (empty($entity) || empty($entityForeign)) {
throw new Error();
}
@@ -642,6 +750,8 @@ class EntityManager
throw new Error();
}
$isCustom = $this->getMetadata()->get("entityDefs.{$entity}.links.{$link}.isCustom");
if (
$this->getMetadata()->get("entityDefs.{$entity}.links.{$link}.type") == 'hasMany'
&&
@@ -726,13 +836,41 @@ class EntityManager
}
}
$this->getLanguage()->set($entity, 'fields', $link, $label);
$this->getLanguage()->set($entity, 'links', $link, $label);
$this->getLanguage()->set($entityForeign, 'fields', $linkForeign, $labelForeign);
$this->getLanguage()->set($entityForeign, 'links', $linkForeign, $labelForeign);
$label = null;
if (isset($params['label'])) {
$label = $params['label'];
}
$labelForeign = null;
if (isset($params['labelForeign'])) {
$labelForeign = $params['labelForeign'];
}
if ($label) {
$this->getLanguage()->set($entity, 'fields', $link, $label);
$this->getLanguage()->set($entity, 'links', $link, $label);
}
if ($labelForeign) {
$this->getLanguage()->set($entityForeign, 'fields', $linkForeign, $labelForeign);
$this->getLanguage()->set($entityForeign, 'links', $linkForeign, $labelForeign);
}
$this->getLanguage()->save();
if ($isCustom) {
if ($this->getBaseLanguage()->getLanguage() !== $this->getBaseLanguage()->getLanguage()) {
if ($label) {
$this->getBaseLanguage()->set($entity, 'fields', $link, $label);
$this->getBaseLanguage()->set($entity, 'links', $link, $label);
}
if ($labelForeign) {
$this->getBaseLanguage()->set($entityForeign, 'fields', $linkForeign, $labelForeign);
$this->getBaseLanguage()->set($entityForeign, 'links', $linkForeign, $labelForeign);
}
$this->getBaseLanguage()->save();
}
}
return true;
}
@@ -786,8 +924,20 @@ class EntityManager
protected function getHook($type)
{
$templateDefs = $this->getMetadata()->get(['app', 'entityTemplates', $type], []);
$className = '\\Espo\\Core\\Utils\\EntityManager\\Hooks\\' . $type . 'Type';
$className = $this->getMetadata()->get(['entityTemplates', $type, 'hookClassName'], $className);
$templateModuleName = null;
if (!empty($templateDefs['module'])) {
$templateModuleName = $templateDefs['module'];
$normalizedTemplateModuleName = Util::normilizeClassName($templateModuleName);
$className = '\\Espo\\Modules\\'.$normalizedTemplateModuleName.'\\Core\\Utils\\EntityManager\\Hooks\\' . $type . 'Type';
}
$className = $this->getMetadata()->get(['app', 'entityTemplates', $type, 'hookClassName'], $className);
if (class_exists($className)) {
$hook = new $className();
+64 -68
View File
@@ -45,29 +45,28 @@ class FieldManager
private $container;
protected $metadataType = 'entityDefs';
protected $customOptionName = 'isCustom';
protected $forbiddenFieldNameList = ['id', 'deleted'];
public function __construct(Metadata $metadata, Language $language, Container $container = null)
public function __construct(Container $container = null)
{
$this->metadata = $metadata;
$this->language = $language;
$this->container = $container;
$this->metadataHelper = new \Espo\Core\Utils\Metadata\Helper($this->metadata);
$this->metadataHelper = new \Espo\Core\Utils\Metadata\Helper($this->getMetadata());
}
protected function getMetadata()
{
return $this->metadata;
return $this->container->get('metadata');
}
protected function getLanguage()
{
return $this->language;
return $this->container->get('language');
}
protected function getBaseLanguage()
{
return $this->container->get('baseLanguage');
}
protected function getMetadataHelper()
@@ -95,6 +94,18 @@ class FieldManager
public function create($scope, $name, $fieldDefs)
{
if (empty($name)) {
throw new BadRequest();
}
if (strlen($name) > 255) {
throw new Error('Field name should not be longer than 255.');
}
if (is_numeric($name[0])) {
throw new Error('Bad field name.');
}
$existingField = $this->getFieldDefs($scope, $name);
if (isset($existingField)) {
throw new Conflict('Field ['.$name.'] exists in '.$scope);
@@ -119,18 +130,25 @@ class FieldManager
$name = trim($name);
$this->isChanged = false;
/*Add option to metadata to identify the custom field*/
if (!$this->isCore($scope, $name)) {
$fieldDefs[$this->customOptionName] = true;
$fieldDefs['isCustom'] = true;
}
$isCustom = false;
if (!empty($fieldDefs['isCustom'])) {
$isCustom = true;
}
$result = true;
$isLabelChanged = false;
if (isset($fieldDefs['label'])) {
$isLabelChanged |= $this->setLabel($scope, $name, $fieldDefs['label']);
$this->setLabel($scope, $name, $fieldDefs['label'], $isNew, $isCustom);
$isLabelChanged = true;
}
if (isset($fieldDefs['tooltipText'])) {
$isLabelChanged |= $this->setTooltipText($scope, $name, $fieldDefs['tooltipText']);
$this->setTooltipText($scope, $name, $fieldDefs['tooltipText'], $isNew, $isCustom);
$isLabelChanged = true;
}
$type = isset($fieldDefs['type']) ? $fieldDefs['type'] : $type = $this->getMetadata()->get(['entityDefs', $scope, 'fields', $name, 'type']);
@@ -146,7 +164,8 @@ class FieldManager
unset($translatedOptions['_empty_']);
}
$isLabelChanged |= $this->setTranslatedOptions($scope, $name, $translatedOptions);
$this->setTranslatedOptions($scope, $name, $translatedOptions, $isNew, $isCustom);
$isLabelChanged = true;
}
}
@@ -163,17 +182,19 @@ class FieldManager
$subFieldName = $name . ucfirst($partField);
$subFieldLabel = $fieldDefs['label'] . ' ' . $partLabel;
}
$isLabelChanged |= $this->setLabel($scope, $subFieldName, $subFieldLabel);
$this->setLabel($scope, $subFieldName, $subFieldLabel, $isNew, $isCustom);
$isLabelChanged = true;
}
}
}
}
if ($isLabelChanged) {
$result &= $this->getLanguage()->save();
if (isset($fieldDefs['tooltipText'])) {
$this->getDefaultLanguage()->save();
$this->getLanguage()->save();
if ($isNew || $isCustom) {
if ($this->getBaseLanguage()->getLanguage() !== $this->getLanguage()->getLanguage()) {
$this->getBaseLanguage()->save();
}
}
}
@@ -185,9 +206,7 @@ class FieldManager
if (array_key_exists('dynamicLogicVisible', $fieldDefs)) {
if (!is_null($fieldDefs['dynamicLogicVisible'])) {
$this->prepareClientDefsFieldsDynamicLogic($clientDefs, $name);
$clientDefs['dynamicLogic']['fields'][$name]['visible'] = array(
'conditionGroup' => $fieldDefs['dynamicLogicVisible']
);
$clientDefs['dynamicLogic']['fields'][$name]['visible'] = $fieldDefs['dynamicLogicVisible'];
$metadataToBeSaved = true;
$clientDefsToBeSet = true;
} else {
@@ -198,15 +217,12 @@ class FieldManager
$clientDefsToBeSet = true;
}
}
}
if (array_key_exists('dynamicLogicReadOnly', $fieldDefs)) {
if (!is_null($fieldDefs['dynamicLogicReadOnly'])) {
$this->prepareClientDefsFieldsDynamicLogic($clientDefs, $name);
$clientDefs['dynamicLogic']['fields'][$name]['readOnly'] = array(
'conditionGroup' => $fieldDefs['dynamicLogicReadOnly']
);
$clientDefs['dynamicLogic']['fields'][$name]['readOnly'] = $fieldDefs['dynamicLogicReadOnly'];
$metadataToBeSaved = true;
$clientDefsToBeSet = true;
} else {
@@ -222,9 +238,7 @@ class FieldManager
if (array_key_exists('dynamicLogicRequired', $fieldDefs)) {
if (!is_null($fieldDefs['dynamicLogicRequired'])) {
$this->prepareClientDefsFieldsDynamicLogic($clientDefs, $name);
$clientDefs['dynamicLogic']['fields'][$name]['required'] = array(
'conditionGroup' => $fieldDefs['dynamicLogicRequired']
);
$clientDefs['dynamicLogic']['fields'][$name]['required'] = $fieldDefs['dynamicLogicRequired'];
$metadataToBeSaved = true;
$clientDefsToBeSet = true;
} else {
@@ -337,6 +351,11 @@ class FieldManager
}
}
$this->getLanguage()->save();
if ($this->getBaseLanguage()->getLanguage() !== $this->getLanguage()->getLanguage()) {
$this->getBaseLanguage()->save();
}
$this->processHook('afterRemove', $type, $scope, $name);
return (bool) $res;
@@ -362,47 +381,37 @@ class FieldManager
$this->getLanguage()->delete($scope, 'fields', $name);
$this->getLanguage()->delete($scope, 'options', $name);
$this->getLanguage()->delete($scope, 'tooltips', $name);
$this->getDefaultLanguage()->delete($scope, 'tooltips', $name);
$this->getLanguage()->save();
$this->getDefaultLanguage()->save();
}
protected function setTranslatedOptions($scope, $name, $value)
protected function setTranslatedOptions($scope, $name, $value, $isNew, $isCustom)
{
if (!$this->isLabelChanged($scope, 'options', $name, $value)) {
return false;
if ($isNew || $isCustom) {
$this->getBaseLanguage()->set($scope, 'options', $name, $value);
}
$this->getLanguage()->set($scope, 'options', $name, $value);
return true;
}
protected function setLabel($scope, $name, $value)
protected function setLabel($scope, $name, $value, $isNew, $isCustom)
{
if (!$this->isLabelChanged($scope, 'fields', $name, $value)) {
return false;
if ($isNew || $isCustom) {
$this->getBaseLanguage()->set($scope, 'fields', $name, $value);
}
$this->getLanguage()->set($scope, 'fields', $name, $value);
return true;
}
protected function setTooltipText($scope, $name, $value)
protected function setTooltipText($scope, $name, $value, $isNew, $isCustom)
{
if (!$this->isLabelChanged($scope, 'tooltips', $name, $value)) {
return false;
}
if ($value && $value !== '') {
$this->getLanguage()->set($scope, 'tooltips', $name, $value);
$this->getDefaultLanguage()->set($scope, 'tooltips', $name, $value);
$this->getBaseLanguage()->set($scope, 'tooltips', $name, $value);
} else {
$this->getLanguage()->delete($scope, 'tooltips', $name);
$this->getDefaultLanguage()->delete($scope, 'tooltips', $name);
$this->getBaseLanguage()->delete($scope, 'tooltips', $name);
}
return true;
}
protected function deleteLabel($scope, $name)
@@ -410,10 +419,10 @@ class FieldManager
$this->getLanguage()->delete($scope, 'fields', $name);
$this->getLanguage()->delete($scope, 'tooltips', $name);
$this->getLanguage()->delete($scope, 'options', $name);
$this->getDefaultLanguage()->delete($scope, 'tooltips', $name);
$this->getLanguage()->save();
$this->getDefaultLanguage()->save();
$this->getBaseLanguage()->delete($scope, 'fields', $name);
$this->getBaseLanguage()->delete($scope, 'tooltips', $name);
$this->getBaseLanguage()->delete($scope, 'options', $name);
}
protected function getFieldDefs($scope, $name)
@@ -557,16 +566,7 @@ class FieldManager
return $this->isChanged;
}
/**
* Check if label is changed
*
* @param string $scope
* @param string $category
* @param string $name
* @param mixed $newLabel
*
* @return boolean
*/
protected function isLabelChanged($scope, $category, $name, $newLabel)
{
$currentLabel = $this->getLanguage()->get([$scope, $category, $name]);
@@ -578,11 +578,7 @@ class FieldManager
return false;
}
/**
* Only for update method
*
* @return boolean
*/
public function isChanged()
{
return $this->isChanged;
@@ -598,7 +594,7 @@ class FieldManager
protected function isCore($scope, $name)
{
$existingField = $this->getFieldDefs($scope, $name);
if (isset($existingField) && (!isset($existingField[$this->customOptionName]) || !$existingField[$this->customOptionName])) {
if (isset($existingField) && (!isset($existingField['isCustom']) || !$existingField['isCustom'])) {
return true;
}
@@ -33,6 +33,8 @@ class FieldManagerUtil
{
private $metadata;
private $fieldByTypeListCache = [];
public function __construct(Metadata $metadata)
{
$this->metadata = $metadata;
@@ -95,4 +97,23 @@ class FieldManagerUtil
return array_merge($this->getActualAttributeList($scope, $name), $this->getNotActualAttributeList($scope, $name));
}
}
public function getFieldByTypeList($scope, $type)
{
if (!array_key_exists($scope, $this->fieldByTypeListCache)) {
$this->fieldByTypeListCache[$scope] = [];
}
if (!array_key_exists($type, $this->fieldByTypeListCache[$scope])) {
$fieldDefs = $this->getMetadata()->get(['entityDefs', $scope, 'fields'], []);
$list = [];
foreach ($fieldDefs as $field => $defs) {
if (isset($defs['type']) && $defs['type'] === $type) {
$list[] = $field;
}
}
$this->fieldByTypeListCache[$scope][$type] = $list;
}
return $this->fieldByTypeListCache[$scope][$type];
}
}
+24 -19
View File
@@ -28,8 +28,9 @@
************************************************************************/
namespace Espo\Core\Utils\File;
use Espo\Core\Utils,
Espo\Core\Exceptions\Error;
use Espo\Core\Utils;
use Espo\Core\Exceptions\Error;
class Manager
{
@@ -110,7 +111,7 @@ class Manager
}
if ($isReturnSingleArray) {
return $this->getSingeFileList($result, $onlyFileType);
return $this->getSingeFileList($result, $onlyFileType, $path);
}
return $result;
@@ -125,7 +126,7 @@ class Manager
*
* @return aray
*/
protected function getSingeFileList(array $fileList, $onlyFileType = null, $parentDirName = '')
protected function getSingeFileList(array $fileList, $onlyFileType = null, $basePath = null, $parentDirName = '')
{
$singleFileList = array();
foreach($fileList as $dirName => $fileName) {
@@ -133,16 +134,16 @@ class Manager
if (is_array($fileName)) {
$currentDir = Utils\Util::concatPath($parentDirName, $dirName);
if (!isset($onlyFileType) || $onlyFileType == $this->isFile($currentDir)) {
if (!isset($onlyFileType) || $onlyFileType == $this->isFile($currentDir, $basePath)) {
$singleFileList[] = $currentDir;
}
$singleFileList = array_merge($singleFileList, $this->getSingeFileList($fileName, $onlyFileType, $currentDir));
$singleFileList = array_merge($singleFileList, $this->getSingeFileList($fileName, $onlyFileType, $basePath, $currentDir));
} else {
$currentFileName = Utils\Util::concatPath($parentDirName, $fileName);
if (!isset($onlyFileType) || $onlyFileType == $this->isFile($currentFileName)) {
if (!isset($onlyFileType) || $onlyFileType == $this->isFile($currentFileName, $basePath)) {
$singleFileList[] = $currentFileName;
}
}
@@ -221,7 +222,6 @@ class Manager
*/
public function putPhpContents($path, $data, $withObjects = false)
{
return $this->putContents($path, $this->wrapForDataExport($data, $withObjects), LOCK_EX);
}
@@ -425,13 +425,7 @@ class Manager
$sourcePath = $this->concatPaths($sourcePath);
$destPath = $this->concatPaths($destPath);
if (isset($fileList)) {
if (!empty($sourcePath)) {
foreach ($fileList as &$fileName) {
$fileName = $this->concatPaths(array($sourcePath, $fileName));
}
}
} else {
if (!isset($fileList)) {
$fileList = is_file($sourcePath) ? (array) $sourcePath : $this->getFileList($sourcePath, $recursively, '', true, true);
}
@@ -489,8 +483,7 @@ class Manager
$defaultPermissions = $this->getPermissionUtils()->getDefaultPermissions();
if (file_exists($filePath)) {
if (!in_array($this->getPermissionUtils()->getCurrentPermission($filePath), array($defaultPermissions['file'], $defaultPermissions['dir']))) {
if (!is_writable($filePath) && !in_array($this->getPermissionUtils()->getCurrentPermission($filePath), array($defaultPermissions['file'], $defaultPermissions['dir']))) {
return $this->getPermissionUtils()->setDefaultPermissions($filePath, true);
}
return true;
@@ -679,10 +672,16 @@ class Manager
* Check if $dirname is directory.
*
* @param string $dirname
* @param string $basePath
*
* @return boolean
*/
public function isDir($dirname)
public function isDir($dirname, $basePath = null)
{
if (!empty($basePath)) {
$dirname = $this->concatPaths([$basePath, $dirname]);
}
return is_dir($dirname);
}
@@ -690,10 +689,16 @@ class Manager
* Check if $filename is file. If $filename doesn'ot exist, check by pathinfo
*
* @param string $filename
* @param string $basePath
*
* @return boolean
*/
public function isFile($filename)
public function isFile($filename, $basePath = null)
{
if (!empty($basePath)) {
$filename = $this->concatPaths([$basePath, $filename]);
}
if (file_exists($filename)) {
return is_file($filename);
}
+66 -3
View File
@@ -236,17 +236,39 @@ class Metadata
}
}
public function getAllObjects($isJSON = false, $reload = false)
protected function getObjData($reload = false)
{
if (!isset($this->objData) || $reload) {
$this->objInit($reload);
}
return $this->objData;
}
/**
* Get Object Metadata
*
* @param mixed string|array $key
* @param mixed $default
*
* @return object
*/
public function getObjects($key = null, $default = null)
{
$objData = $this->getObjData();
return Util::getValueByKey($objData, $key, $default);
}
public function getAllObjects($isJSON = false, $reload = false)
{
$objData = $this->getObjData($reload);
if ($isJSON) {
return Json::encode($this->objData);
return Json::encode($objData);
}
return $this->objData;
return $objData;
}
public function getAllForFrontend()
@@ -305,6 +327,47 @@ class Metadata
return $data;
}
/**
* Get metadata definition in custom directory
*
* @param string|array $key
* @param mixed $default
*
* @return object|mixed
*/
public function getCustom($key1, $key2, $default = null)
{
$filePath = array($this->paths['customPath'], $key1, $key2.'.json');
$fileContent = $this->getFileManager()->getContents($filePath);
if ($fileContent) {
return Json::decode($fileContent);
}
return $default;
}
/**
* Set and save metadata in custom directory. The data is not merging with existing data. Use getCustom() to get existing data.
*
* @param string $key1
* @param string $key2
* @param array $data
*
* @return boolean
*/
public function saveCustom($key1, $key2, $data)
{
$filePath = array($this->paths['customPath'], $key1, $key2.'.json');
$changedData = Json::encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$result = $this->getFileManager()->putContents($filePath, $changedData);
$this->init(true);
return true;
}
/**
* Set Metadata data
* Ex. $key1 = menu, $key2 = Account then will be created a file metadataFolder/menu/Account.json
+3 -10
View File
@@ -32,7 +32,7 @@ return array (
'driver' => 'pdo_mysql',
'host' => 'localhost',
'port' => '',
'charset' => 'utf8',
'charset' => 'utf8mb4',
'dbname' => '',
'user' => '',
'password' => '',
@@ -139,7 +139,7 @@ return array (
'x' => 2,
'y' => 2,
'width' => 2,
'height' => 2
'height' => 4
],
(object) [
'id' => 'default-stream',
@@ -148,14 +148,6 @@ return array (
'y' => 0,
'width' => 2,
'height' => 4
],
(object) [
'id' => 'default-tasks',
'name' => 'Tasks',
'x' => 2,
'y' => 0,
'width' => 2,
'height' => 2
]
]
]
@@ -172,6 +164,7 @@ return array (
'aclStrictMode' => false,
'aclAllowDeleteCreated' => false,
'inlineAttachmentUploadMaxSize' => 20,
'textFilterUseContainsForVarchar' => false,
'isInstalled' => false
);
@@ -47,7 +47,7 @@ class AssignmentEmailNotification extends \Espo\Core\Hooks\Base
) {
$userId = $entity->get('assignedUserId');
if (!empty($userId) && $userId != $this->getUser()->id && $entity->isFieldChanged('assignedUserId')) {
if (!empty($userId) && $userId != $this->getUser()->id && $entity->isAttributeChanged('assignedUserId')) {
$job = $this->getEntityManager()->getEntity('Job');
$job->set(array(
'serviceName' => 'EmailNotification',
+1 -1
View File
@@ -249,7 +249,7 @@ class Stream extends \Espo\Core\Hooks\Base
}
} else {
if (empty($options['noStream']) && empty($options['silent'])) {
if ($entity->isFieldChanged('assignedUserId')) {
if ($entity->isAttributeChanged('assignedUserId')) {
$assignedUserId = $entity->get('assignedUserId');
if (!empty($assignedUserId)) {
$this->getStreamService()->followEntity($entity, $assignedUserId);
+33 -15
View File
@@ -62,13 +62,25 @@ class Cleanup extends \Espo\Core\Jobs\Base
$this->cleanupActionHistory();
$this->cleanupAuthToken();
$this->cleanupUpgradeBackups();
$this->cleanupUniqueIds();
}
protected function cleanupJobs()
{
$query = "DELETE FROM `job` WHERE DATE(modified_at) < '".$this->getCleanupJobFromDate()."' AND status <> 'Pending'";
$pdo = $this->getEntityManager()->getPDO();
$query = "DELETE FROM `job` WHERE DATE(modified_at) < ".$pdo->quote($this->getCleanupJobFromDate())." AND status <> 'Pending'";
$sth = $pdo->prepare($query);
$sth->execute();
}
protected function cleanupUniqueIds()
{
$pdo = $this->getEntityManager()->getPDO();
$query = "DELETE FROM `unique_id` WHERE terminate_at IS NOT NULL AND terminate_at < ".$pdo->quote(date('Y-m-d H:i:s'))."";
$sth = $pdo->prepare($query);
$sth->execute();
}
@@ -83,29 +95,35 @@ class Cleanup extends \Espo\Core\Jobs\Base
while ($row = $sth->fetch(\PDO::FETCH_ASSOC)) {
$id = $row['id'];
$lastRowsSql = "SELECT id FROM scheduled_job_log_record WHERE scheduled_job_id = '".$id."' ORDER BY created_at DESC LIMIT 0,10";
$lastRowsSql = "SELECT id FROM scheduled_job_log_record WHERE scheduled_job_id = ".$pdo->quote($id)." ORDER BY created_at DESC LIMIT 0,10";
$lastRowsSth = $pdo->prepare($lastRowsSql);
$lastRowsSth->execute();
$lastRowIds = $lastRowsSth->fetchAll(\PDO::FETCH_COLUMN, 0);
$delSql = "DELETE FROM `scheduled_job_log_record`
WHERE scheduled_job_id = '".$id."'
AND DATE(created_at) < '".$this->getCleanupJobFromDate()."'
AND id NOT IN ('".implode("', '", $lastRowIds)."')
";
$pdo->query($delSql);
if (count($lastRowIds)) {
foreach ($lastRowIds as $i => $v) {
$lastRowIds[$i] = $pdo->quote($v);
}
$delSql = "DELETE FROM `scheduled_job_log_record`
WHERE scheduled_job_id = ".$pdo->quote($id)."
AND DATE(created_at) < ".$pdo->quote($this->getCleanupJobFromDate())."
AND id NOT IN (".implode(',', $lastRowIds).")
";
$pdo->query($delSql);
}
}
}
protected function cleanupActionHistory()
{
$pdo = $this->getEntityManager()->getPDO();
$period = '-' . $this->getConfig()->get('cleanupActionHistoryPeriod', $this->cleanupActionHistoryPeriod);
$datetime = new \DateTime();
$datetime->modify($period);
$query = "DELETE FROM `action_history_record` WHERE DATE(created_at) < '" . $datetime->format('Y-m-d') . "'";
$query = "DELETE FROM `action_history_record` WHERE DATE(created_at) < " . $pdo->quote($datetime->format('Y-m-d')) . "";
$pdo = $this->getEntityManager()->getPDO();
$sth = $pdo->prepare($query);
$sth->execute();
}
@@ -116,7 +134,7 @@ class Cleanup extends \Espo\Core\Jobs\Base
$datetime = new \DateTime();
$datetime->modify($period);
$query = "DELETE FROM `reminder` WHERE DATE(remind_at) < '" . $datetime->format('Y-m-d') . "'";
$query = "DELETE FROM `reminder` WHERE DATE(remind_at) < " . $pdo->quote($datetime->format('Y-m-d')) . "";
$pdo = $this->getEntityManager()->getPDO();
$sth = $pdo->prepare($query);
@@ -125,13 +143,14 @@ class Cleanup extends \Espo\Core\Jobs\Base
protected function cleanupAuthToken()
{
$pdo = $this->getEntityManager()->getPDO();
$period = '-' . $this->getConfig()->get('cleanupAuthTokenPeriod', $this->cleanupAuthTokenPeriod);
$datetime = new \DateTime();
$datetime->modify($period);
$query = "DELETE FROM `auth_token` WHERE DATE(modified_at) < '" . $datetime->format('Y-m-d') . "' AND is_active = 0";
$query = "DELETE FROM `auth_token` WHERE DATE(modified_at) < " . $pdo->quote($datetime->format('Y-m-d')) . " AND is_active = 0";
$pdo = $this->getEntityManager()->getPDO();
$sth = $pdo->prepare($query);
$sth->execute();
}
@@ -334,4 +353,3 @@ class Cleanup extends \Espo\Core\Jobs\Base
}
}
}
@@ -97,6 +97,21 @@ class Invitations
'inviteeType' => $invitee->getEntityType(),
'link' => $link
));
if ($entity->get('dateEnd')) {
$terminateAt = $entity->get('dateEnd');
} else {
$dt = new \DateTime();
$dt->modify('+1 month');
$terminateAt = $dt->format('Y-m-d H:i:s');
}
$uid->set([
'targetId' => $entity->id,
'targetType' => $entity->getEntityType(),
'terminateAt' => $terminateAt
]);
$this->getEntityManager()->saveEntity($uid);
$emailAddress = $invitee->get('emailAddress');
@@ -37,7 +37,7 @@ class Opportunity extends \Espo\Core\Controllers\Record
public function actionReportByLeadSource($params, $data, $request)
{
$level = $this->getAcl()->getLevel('Opportunity', 'read');
if (!$level || $level == 'own' || $level == 'no') {
if (!$level || $level == 'no') {
throw new Forbidden();
}
@@ -51,7 +51,7 @@ class Opportunity extends \Espo\Core\Controllers\Record
public function actionReportByStage($params, $data, $request)
{
$level = $this->getAcl()->getLevel('Opportunity', 'read');
if (!$level || $level == 'own' || $level == 'no') {
if (!$level || $level == 'no') {
throw new Forbidden();
}
@@ -65,7 +65,7 @@ class Opportunity extends \Espo\Core\Controllers\Record
public function actionReportSalesByMonth($params, $data, $request)
{
$level = $this->getAcl()->getLevel('Opportunity', 'read');
if (!$level || $level == 'own' || $level == 'no') {
if (!$level || $level == 'no') {
throw new Forbidden();
}
@@ -79,7 +79,7 @@ class Opportunity extends \Espo\Core\Controllers\Record
public function actionReportSalesPipeline($params, $data, $request)
{
$level = $this->getAcl()->getLevel('Opportunity', 'read');
if (!$level || $level == 'own' || $level == 'no') {
if (!$level || $level == 'no') {
throw new Forbidden();
}
@@ -90,4 +90,3 @@ class Opportunity extends \Espo\Core\Controllers\Record
return $this->getService('Opportunity')->reportSalesPipeline($dateFilter, $dateFrom, $dateTo);
}
}
@@ -56,7 +56,6 @@ class EventConfirmation extends \Espo\Core\EntryPoints\Base
if (!$uniqueId) {
throw new NotFound();
return;
}
$data = $uniqueId->get('data');
@@ -70,36 +69,55 @@ class EventConfirmation extends \Espo\Core\EntryPoints\Base
if (!empty($eventType) && !empty($eventId) && !empty($inviteeType) && !empty($inviteeId) && !empty($link)) {
$event = $this->getEntityManager()->getEntity($eventType, $eventId);
$invitee = $this->getEntityManager()->getEntity($inviteeType, $inviteeId);
if ($event && $invitee) {
$relDefs = $event->getRelations();
$tableName = Util::toUnderscore($relDefs[$link]['relationName']);
$status = 'None';
if ($action == 'accept') {
$status = 'Accepted';
} else if ($action == 'decline') {
$status = 'Declined';
} else if ($action == 'tentative') {
$status = 'Tentative';
}
$pdo = $this->getEntityManager()->getPDO();
$sql = "
UPDATE `{$tableName}` SET status = '{$status}'
WHERE ".strtolower($eventType)."_id = '{$eventId}' AND ".strtolower($inviteeType)."_id = '{$inviteeId}'
";
$sth = $pdo->prepare($sql);
$sth->execute();
$this->getEntityManager()->getRepository('UniqueId')->remove($uniqueId);
echo $status;
return;
if (!$event || !$invitee) {
throw new NotFound();
}
if ($event->get('status') === 'Held' || $event->get('status') === 'Not Held') {
throw new NotFound();
}
$status = 'None';
$hookMethodName = 'afterConfirmation';
if ($action == 'accept') {
$status = 'Accepted';
} else if ($action == 'decline') {
$status = 'Declined';
} else if ($action == 'tentative') {
$status = 'Tentative';
}
$data = (object) [
'status' => $status
];
$this->getEntityManager()->getRepository($eventType)->updateRelation($event, $link, $invitee->id, $data);
$actionData = [
'eventName' => $event->get('name'),
'eventType' => $event->getEntityType(),
'eventId' => $event->id,
'dateStart' => $event->get('dateStart'),
'action' => $action,
'status' => $status,
'link' => $link,
'inviteeType' => $invitee->getEntityType(),
'inviteeId' => $invitee->id
];
$this->getEntityManager()->getHookManager()->process($event->getEntityType(), $hookMethodName, $event, [], $actionData);
$runScript = "
Espo.require('crm:controllers/event-confirmation', function (Controller) {
var controller = new Controller(app.baseController.params, app.getControllerInjection());
controller.masterView = app.masterView;
controller.doAction('confirmEvent', ".json_encode($actionData).");
});
";
$this->getClientManager()->display($runScript);
}
throw new Error();
}
}
@@ -113,9 +113,19 @@ class SubscribeAgain extends \Espo\Core\EntryPoints\Base
$this->getHookManager()->process('TargetList', 'afterCancelOptOut', $targetList, [], $hookData);
}
}
echo $this->getLanguage()->translate('subscribedAgain', 'messages', 'Campaign');
echo '<br><br>';
echo '<a href="?entryPoint=unsubscribe&id='.$queueItemId.'">' . $this->getLanguage()->translate('Unsubscribe again', 'labels', 'Campaign') . '</a>';
$data = [
'queueItemId' => $queueItemId
];
$runScript = "
Espo.require('crm:controllers/unsubscribe', function (Controller) {
var controller = new Controller(app.baseController.params, app.getControllerInjection());
controller.masterView = app.masterView;
controller.doAction('subscribeAgain', ".json_encode($data).");
});
";
$this->getClientManager()->display($runScript);
}
}
}
@@ -113,9 +113,19 @@ class Unsubscribe extends \Espo\Core\EntryPoints\Base
$this->getHookManager()->process('TargetList', 'afterOptOut', $targetList, [], $hookData);
}
}
echo $this->getLanguage()->translate('unsubscribed', 'messages', 'Campaign');
echo '<br><br>';
echo '<a href="?entryPoint=subscribeAgain&id='.$queueItemId.'">' . $this->getLanguage()->translate('Subscribe again', 'labels', 'Campaign') . '</a>';
$data = [
'queueItemId' => $queueItemId
];
$runScript = "
Espo.require('crm:controllers/unsubscribe', function (Controller) {
var controller = new Controller(app.baseController.params, app.getControllerInjection());
controller.masterView = app.masterView;
controller.doAction('unsubscribe', ".json_encode($data).");
});
";
$this->getClientManager()->display($runScript);
}
}
}
@@ -36,16 +36,18 @@ class Meeting extends \Espo\Core\Repositories\Event
{
protected function beforeSave(Entity $entity, array $options = array())
{
if (!$entity->isNew() && $entity->isAttributeChanged('parentId')) {
$entity->set('accountId', null);
}
$parentId = $entity->get('parentId');
$parentType = $entity->get('parentType');
if (!empty($parentId) || !empty($parentType)) {
if ($parentId && $parentType) {
$parent = $this->getEntityManager()->getEntity($parentType, $parentId);
if (!empty($parent)) {
if ($parent) {
$accountId = null;
if ($parent->getEntityType() == 'Account') {
$accountId = $parent->id;
} else if ($parent->get('accountId')) {
$accountId = $parent->get('accountId');
} else if ($parent->getEntityType() == 'Lead') {
if ($parent->get('status') == 'Converted') {
if ($parent->get('createdAccountId')) {
@@ -53,14 +55,17 @@ class Meeting extends \Espo\Core\Repositories\Event
}
}
}
if (!empty($accountId)) {
if (!$accountId && $parent->get('accountId') && $parent->getRelationParam('account', 'entity') == 'Account') {
$accountId = $parent->get('accountId');
}
if ($accountId) {
$entity->set('accountId', $accountId);
}
}
}
if (!$entity->isNew()) {
if ($entity->isFieldChanged('dateStart') && $entity->isFieldChanged('dateStart') && !$entity->isFieldChanged('dateEnd')) {
if ($entity->isAttributeChanged('dateStart') && $entity->isAttributeChanged('dateStart') && !$entity->isAttributeChanged('dateEnd')) {
$dateEndPrevious = $entity->getFetched('dateEnd');
$dateStartPrevious = $entity->getFetched('dateStart');
if ($dateStartPrevious && $dateEndPrevious) {
@@ -71,7 +71,7 @@ class Task extends \Espo\Core\Repositories\Event
protected function beforeSave(Entity $entity, array $options = array())
{
if ($entity->isFieldChanged('status')) {
if ($entity->isAttributeChanged('status')) {
if ($entity->get('status') == 'Completed') {
$entity->set('dateCompleted', date('Y-m-d H:i:s'));
} else {
@@ -103,26 +103,46 @@ class Task extends \Espo\Core\Repositories\Event
}
}
if (!$entity->isNew() && $entity->isAttributeChanged('parentId')) {
$entity->set('accountId', null);
$entity->set('contactId', null);
}
$parentId = $entity->get('parentId');
$parentType = $entity->get('parentType');
if (!empty($parentId) || !empty($parentType)) {
if ($parentId && $parentType) {
$parent = $this->getEntityManager()->getEntity($parentType, $parentId);
if (!empty($parent)) {
if ($parent) {
$accountId = null;
$contactId = null;
if ($parent->getEntityType() == 'Account') {
$accountId = $parent->id;
} else if ($parent->get('accountId')) {
$accountId = $parent->get('accountId');
} else if ($parent->getEntityType() == 'Lead') {
if ($parent->get('status') == 'Converted') {
if ($parent->get('createdAccountId')) {
$accountId = $parent->get('createdAccountId');
}
if ($parent->get('createdContactId')) {
$contactId = $parent->get('createdContactId');
}
}
} else if ($parent->getEntityType() == 'Contact') {
$contactId = $parent->id;
}
if (!empty($accountId)) {
if (!$accountId && $parent->get('accountId') && $parent->getRelationParam('account', 'entity') == 'Account') {
$accountId = $parent->get('accountId');
}
if (!$contactId && $parent->get('contactId') && $parent->getRelationParam('contact', 'entity') == 'Contact') {
$contactId = $parent->get('contactId');
}
if ($accountId) {
$entity->set('accountId', $accountId);
}
if ($contactId) {
$entity->set('contactId', $contactId);
}
}
}
@@ -19,19 +19,6 @@
"documents": "Dokumenty",
"campaign": "Kampaň"
},
"options": {
"stage": {
"Request": "Poptávka",
"Quote": "Nabídka",
"Order": "Objednávka",
"ToProduction": "Čeká na výrobu",
"InProduction": "Vyrábí se...",
"Dispatch": "Expedice",
"Delivered": "Dodáno",
"Closed Won": "VYFAKTUROVÁNO",
"Closed Lost": "ODMÍTNUTO"
}
},
"labels": {
"Create Opportunity": "Vytvořit případ"
},
@@ -26,6 +26,8 @@
"stage": {
"Prospecting": "Undersøges",
"Qualification": "Kvalifikation",
"Proposal": "Tilbud",
"Negotiation": "Forhandler",
"Needs Analysis": "Behovsanalyse",
"Value Proposition": "Vejledende Tilbud",
"Id. Decision Makers": "Identifikation af Beslutningstagere",
@@ -25,6 +25,8 @@
"stage": {
"Prospecting": "Identifikation",
"Qualification": "Qualifikation",
"Proposal": "Angebot",
"Negotiation": "Verhandlung",
"Needs Analysis": "Bedarfserhebung",
"Value Proposition": "Richtangebot",
"Id. Decision Makers": "Identifikation der Entscheider",
@@ -9,12 +9,12 @@
"priority": "Priority",
"type": "Type",
"description": "Description",
"inboundEmail": "Inbound Email",
"inboundEmail": "Group Email Account",
"lead": "Lead",
"attachments": "Attachments"
},
"links": {
"inboundEmail": "Inbound Email",
"inboundEmail": "Group Email Account",
"account": "Account",
"contact": "Contact (Primary)",
"Contacts": "Contacts",
@@ -31,6 +31,7 @@
"account": "Account (Primary)",
"accounts": "Accounts",
"casesPrimary": "Cases (Primary)",
"tasksPrimary": "Tasks (expanded)",
"portalUser": "Portal User",
"originalLead": "Original Lead",
"documents": "Documents"
@@ -27,6 +27,8 @@
"stage": {
"Prospecting": "Prospecting",
"Qualification": "Qualification",
"Proposal": "Proposal",
"Negotiation": "Negotiation",
"Needs Analysis": "Needs Analysis",
"Value Proposition": "Value Proposition",
"Id. Decision Makers": "Id. Decision Makers",
@@ -13,10 +13,13 @@
"account": "Account",
"dateCompleted": "Date Completed",
"attachments": "Attachments",
"reminders": "Reminders"
"reminders": "Reminders",
"contact": "Contact"
},
"links": {
"attachments": "Attachments"
"attachments": "Attachments",
"account": "Account",
"contact": "Contact"
},
"options": {
"status": {
@@ -27,6 +27,8 @@
"stage": {
"Prospecting": "Prospección",
"Qualification": "Calificación",
"Proposal": "Propuesta",
"Negotiation": "Negociación",
"Needs Analysis": "Análisis de necesidades",
"Value Proposition": "Propuesta de valor",
"Id. Decision Makers": "Identificar tomador de decisiones",
@@ -27,6 +27,8 @@
"stage": {
"Prospecting": "Prospección",
"Qualification": "Calificación",
"Proposal": "Cotización con Propuesta",
"Negotiation": "Negociación",
"Needs Analysis": "Análisis de Necesidades",
"Value Proposition": "Propuesta de Valor",
"Id. Decision Makers": "Id. Tomadores de Decisiones",
@@ -27,6 +27,8 @@
"stage": {
"Prospecting": "Prospection",
"Qualification": "Qualification",
"Proposal": "Proposition",
"Negotiation": "Négociation",
"Needs Analysis": "Analyse des besoins",
"Value Proposition": "Valeur de la proposition",
"Id. Decision Makers": "Id. Decisionnaire",
@@ -24,6 +24,8 @@
"Prospecting": "Pencarian",
"Qualification": "Kualifikasi",
"Needs Analysis": "Butuh analisa",
"Proposal": "Proposal",
"Negotiation": "Negosiasi",
"Value Proposition": "Proposisi nilai",
"Id. Decision Makers": "Id. Pembuat keputusan",
"Perception Analysis": "Analisis persepsi",
@@ -27,6 +27,8 @@
"stage": {
"Prospecting": "Prospecting",
"Qualification": "Qualifica",
"Proposal": "Proposta",
"Negotiation": "Negoziazione",
"Needs Analysis": "Necessita di analisi",
"Value Proposition": "Proposta di Valore",
"Id. Decision Makers": "Id. Responsabile",
@@ -24,6 +24,8 @@
"stage": {
"Prospecting": "Potencialių klientų paieška",
"Qualification": "Kvalifikacija",
"Proposal": "Pasiūlymas",
"Negotiation": "Derybos",
"Needs Analysis": "Reikalinga analizė",
"Value Proposition": "Pasiūlymo vertė",
"Id. Decision Makers": "Id. Sprendimus priimantys žmonės",
@@ -25,6 +25,8 @@
"stage": {
"Prospecting": "Undersøkes",
"Qualification": "Kvalitetskontrolleres",
"Proposal": "Prisforslag",
"Negotiation": "Forhandler",
"Needs Analysis": "Trenger analyse",
"Value Proposition": "Verdivurdering",
"Id. Decision Makers": "Ser etter beslutningstagere",
@@ -23,6 +23,8 @@
"stage": {
"Prospecting": "Prospectie",
"Qualification": "Kwalificatie",
"Proposal": "Voorstel",
"Negotiation": "Onderhandeling",
"Needs Analysis": "Analyse behoefte",
"Value Proposition": "Waarde voorstel",
"Id. Decision Makers": "Id. Beslissers",
@@ -23,12 +23,14 @@
"stage": {
"Prospecting": "I Spotkanie",
"Qualification": "Kwalifikacja",
"Proposal": "Oferta Cenowa",
"Negotiation": "Negocjacje",
"Needs Analysis": "Dodatkowa Analiza",
"Value Proposition": "Oferta Produktowa",
"Id. Decision Makers": "Oferta przekazna do osoby decyzyjnej",
"Perception Analysis": "Perception Analysis",
"Proposal/Price Quote": "Oferta Cenowa",
"Negotiation/Review": "Negocjacje ",
"Negotiation/Review": "Negocjacje",
"Closed Won": "Zakończone Wygrane",
"Closed Lost": "Zakończone Przegrane"
}
@@ -22,6 +22,8 @@
"stage": {
"Prospecting": "Prospectando",
"Qualification": "Qualificação",
"Proposal": "Proposta",
"Negotiation": "Negociação",
"Needs Analysis": "Necessita de Análise",
"Value Proposition": "Proposta de Valor",
"Id. Decision Makers": "Identificação dos Decisores",
@@ -1,40 +1,51 @@
{
"fields": {
"name": "Nume",
"emailAddress": "Email",
"website": "Site Web",
"phoneNumber": "Telefon",
"billingAddress": "Adresa de Facturare",
"shippingAddress": "Adresa de Transport",
"description": "Descriere",
"sicCode": "Cod Sic",
"industry": "Industrie",
"type": "Tip",
"contactRole": "Rol"
"fields": {
"name": "Nume",
"website": "Site Web",
"phoneNumber": "Telefon",
"billingAddress": "Adresa de Facturare",
"shippingAddress": "Adresa de Transport",
"description": "Descriere",
"sicCode": "Cod Sic",
"industry": "Industrie",
"type": "Tip",
"contactRole": "Rol"
},
"links": {
"contacts": "Contacte",
"opportunities": "Oportunitati",
"cases": "Cazuri"
},
"options": {
"type": {
"Customer": "Client",
"Investor": "Investitor",
"Partner": "Partener"
},
"links": {
"contacts": "Contacte",
"opportunities": "Oportunitati",
"cases": "Cazuri"
},
"options": {
"type": {
"Customer": "Client",
"Investor": "Investitor",
"Partner": "Partener",
"Reseller": "Reseller"
},
"industry": {
"Apparel": "Haine",
"Banking": "Banking",
"Computer Software": "Software Computer",
"Education": "Educatie",
"Electronics": "Electronice",
"Finance": "Finante",
"Insurance": "Asigurari"
}
},
"labels": {
"Create Account": "Creare cont"
"industry": {
"Education": "Educatie",
"Electronics": "Electronice",
"Finance": "Finante",
"Insurance": "Asigurari",
"Technology": "Tehnologie",
"Telecommunications": "Telecomunicații",
"Television": "Televiziune",
"Testing, Inspection & Certification": "Testare, Inspecție & Certificări",
"Transportation": "Transport",
"Travel": "Călătorit",
"Venture Capital": "Capital de risc",
"Wholesale": "Angro",
"Water": "Apă"
}
}
},
"labels": {
"Create Account": "Creare cont",
"Copy Billing": "Copiați facturarea",
"Set Primary": "Setați primar"
},
"presetFilters": {
"customers": "Clienți",
"partners": "Parteneri",
"recentlyCreated": "Create recent"
}
}
@@ -0,0 +1,5 @@
{
"layouts": {
"listForAccount": "Listă (pentru Cont)"
}
}
@@ -1,13 +1,21 @@
{
"modes": {
"month": "Luna",
"week": "Saptamana",
"day": "Zi",
"agendaWeek": "Saptamana",
"agendaDay": "Zi"
},
"labels": {
"Today": "Astazi",
"Create": "Creaza"
}
}
"modes": {
"month": "Lună",
"week": "Săptămână",
"agendaWeek": "Săptămână",
"day": "Zi",
"agendaDay": "Zi",
"timeline": "Cronologie"
},
"labels": {
"Today": "Astăzi",
"Create": "Crează",
"Shared": "Partajat",
"Add User": "Adăugare utilizator",
"current": "actual",
"time": "timp",
"User List": "Listă Utiliziator",
"Manage Users": "Gestionare Utilizatori",
"View Calendar": "Vizualizare Calendar"
}
}
@@ -1,39 +1,50 @@
{
"fields": {
"name": "Nume",
"parent": "Parinte",
"status": "Status",
"dateStart": "Data inceput",
"dateEnd": "Data terminare",
"direction": "Directie",
"duration": "Durata",
"description": "Descriere",
"users": "Utilizatori",
"contacts": "Contacte",
"leads": "Lead-uri"
"fields": {
"name": "Nume",
"parent": "Părinte",
"status": "Stare",
"dateStart": "Dată început",
"dateEnd": "Dată terminare",
"direction": "Direcție",
"duration": "Durată",
"description": "Descriere",
"users": "Utilizatori",
"contacts": "Contacte",
"leads": "Lead-uri",
"reminders": "Mementouri",
"account": "Cont",
"acceptanceStatus": "Starea de acceptare"
},
"options": {
"status": {
"Planned": "Planificat",
"Held": "Reținut",
"Not Held": "Nereșinut"
},
"links": {
"direction": {
"Outbound": "Ieșire",
"Inbound": "Intrare"
},
"options": {
"status": {
"Planned": "Planificat",
"Held": "Detinut",
"Not Held": "Nedetinut"
},
"direction": {
"Outbound": "Iesire",
"Inbound": "Intrare"
}
},
"labels": {
"Create Call": "Creare Apelare",
"Set Held": "Setare detinere",
"Set Not Held": "Setare nedetinere",
"Send Invitations": "Trimite invitatii"
},
"presetFilters": {
"planned": "Planificat",
"held": "Detinut",
"todays": "Today's"
"acceptanceStatus": {
"None": "Nici unul",
"Accepted": "Acceptat",
"Declined": "Respins",
"Tentative": "Tentativă"
}
}
},
"massActions": {
"setHeld": "Setează ca reținut",
"setNotHeld": "Setează ca nereținut"
},
"labels": {
"Create Call": "Creare Apelare",
"Set Held": "Setează ca reținut",
"Set Not Held": "Setează ca nereținut",
"Send Invitations": "Trimite invitații"
},
"presetFilters": {
"planned": "Planificat",
"held": "Reținut",
"todays": "Azi"
}
}
@@ -0,0 +1,66 @@
{
"fields": {
"name": "Nume",
"description": "Descriere",
"status": "Stare",
"type": "Tip",
"startDate": "Dată începere",
"endDate": "Dată terminare",
"targetLists": "Liste țintă",
"excludingTargetLists": "Excluderea listelor țintă",
"sentCount": "Trimis",
"openedCount": "Deschis",
"clickedCount": "Apăsat",
"optedOutCount": "Ales să nu",
"revenue": "Venituri",
"revenueConverted": "Venituri (convertit)",
"budget": "Buget",
"budgetConverted": "Buget (convertit)"
},
"links": {
"targetLists": "Liste Țintă",
"excludingTargetLists": "Excluderea listelor țintă",
"accounts": "Conturi",
"contacts": "Contacte",
"leads": "Lead-uri",
"opportunities": "Oportunități",
"campaignLogRecords": "Jurnal",
"massEmails": "Email-uri în Masă",
"trackingUrls": "Urmărire URL-uri"
},
"options": {
"type": {
"Television": "Televiziune"
},
"status": {
"Planning": "Planificare",
"Active": "Activ",
"Inactive": "Inactiv",
"Complete": "Complet"
}
},
"labels": {
"Create Campaign": "Crează Campanie",
"Target Lists": "Liste Țintă",
"Statistics": "Statistici",
"hard": "tare",
"soft": "ușor",
"Unsubscribe": "Dezabonare",
"Mass Emails": "Email-uri în Masă",
"Email Templates": "Șabloane Email",
"Unsubscribe again": "Dezabonare încă o dată",
"Subscribe again": "Abonare încă o dată",
"Create Target List": "Creați Listă Țintă"
},
"presetFilters": {
"active": "Activ"
},
"messages": {
"unsubscribed": "V-ați dezabonat din lista noastră de mail-uri",
"subscribedAgain": "V-ați abonat încă o dată."
},
"tooltips": {
"targetLists": "Țintele care ar trebui să primească mesaje.",
"excludingTargetLists": "Țintele care nu ar trebui să primească mesaje."
}
}
@@ -0,0 +1,40 @@
{
"fields": {
"action": "Acțiune",
"actionDate": "Dată",
"data": "Date",
"campaign": "Campanie",
"parent": "Țintă",
"object": "Obiect",
"application": "Aplicație",
"queueItem": "Elementul de așteptare",
"stringData": "Șir Date",
"stringAdditionalData": "Șir de Date Adiționale",
"isTest": "Este Test"
},
"links": {
"queueItem": "Elementul de așteptare",
"parent": "Părinte",
"object": "Obiect",
"campaign": "Campanie"
},
"options": {
"action": {
"Sent": "Trimis",
"Opened": "Deschis",
"Opted Out": "Ales să nu",
"Clicked": "Apăsat",
"Lead Created": "Lead-ul a fost creat"
}
},
"labels": {
"All": "Tot"
},
"presetFilters": {
"sent": "Trimis",
"opened": "Deschis",
"optedOut": "Ales să nu",
"clicked": "Apăsat",
"leadCreated": "Lead-ul a fost creat"
}
}
@@ -0,0 +1,12 @@
{
"fields": {
"urlToUse": "Cod pentru inserare, în locul URL",
"campaign": "Campanie"
},
"links": {
"campaign": "Campanie"
},
"labels": {
"Create CampaignTrackingUrl": "Creați Urmărire URL"
}
}
@@ -1,42 +1,55 @@
{
"fields": {
"name": "Nume",
"number": "Numar",
"status": "Status",
"account": "Cont",
"contact": "Contact",
"priority": "Prioritate",
"type": "Tip",
"description": "Descriere"
"fields": {
"name": "Nume",
"number": "Număr",
"status": "Stare",
"account": "Cont",
"contacts": "Contacte",
"priority": "Prioritate",
"type": "Tip",
"description": "Descriere",
"inboundEmail": "Grup Cont Email",
"attachments": "Atașamente"
},
"links": {
"inboundEmail": "Grup Cont Email",
"account": "Cont",
"contact": "Contact (Principal)",
"Contacts": "Contacte",
"meetings": "Întâlniri",
"calls": "Apeluri",
"tasks": "Sarcini",
"emails": "Email-uri",
"articles": "Articole Bază de Cunoștință",
"attachments": "Atașamente"
},
"options": {
"status": {
"New": "Nou",
"Assigned": "Alocat",
"Pending": "În așteptare",
"Closed": "Închis",
"Rejected": "Respins",
"Duplicate": "Duplicat"
},
"links": {
"priority": {
"Low": "Scăzut",
"High": "Înalt"
},
"options": {
"status": {
"New": "Nou",
"Assigned": "Alocat",
"Pending": "In asteptare",
"Closed": "Inchis",
"Rejected": "Respins",
"Duplicate": "Duplicat"
},
"priority" : {
"Low": "Scazut",
"Normal": "Normal",
"High": "Inalt",
"Urgent": "Urgent"
},
"type": {
"Question": "Intrebare",
"Incident": "Incident",
"Problem": "Problema"
}
},
"labels": {
"Create Case": "Creare Caz"
},
"presetFilters": {
"open": "Deschide",
"closed": "Inchis"
"type": {
"Question": "Întrebare",
"Problem": "Problemă"
}
}
},
"labels": {
"Create Case": "Creare Caz",
"Close": "Închis",
"Reject": "Respins",
"Closed": "Închis",
"Rejected": "Respins"
},
"presetFilters": {
"open": "Deschis",
"closed": "Închis"
}
}
@@ -1,31 +1,52 @@
{
"fields": {
"name": "Nume",
"emailAddress": "Email",
"title": "Titlu",
"account": "Cont",
"accounts": "Conturi",
"phoneNumber": "Telefon",
"accountType": "Tip cont",
"doNotCall": "Nu sunati",
"address": "Adresa",
"opportunityRole": "Opportunity Role",
"accountRole": "Rol",
"description": "Descriere"
},
"links": {
"opportunities": "Oportunitati",
"cases": "Cazuri"
},
"labels": {
"Create Contact": "Creare Contact"
},
"options": {
"opportunityRole": {
"": "--None--",
"Decision Maker": "Decision Maker",
"Evaluator": "Evaluator",
"Influencer": "Influencer"
}
"fields": {
"name": "Nume",
"title": "Rol",
"accountRole": "Rol",
"account": "Cont",
"accounts": "Conturi",
"phoneNumber": "Telefon",
"accountType": "Tip cont",
"doNotCall": "Nu sunați",
"address": "Adresă",
"opportunityRole": "Rol Oportunitate",
"description": "Descriere",
"campaign": "Campanie",
"targetLists": "Liste Țintă",
"targetList": "Listă țintă",
"portalUser": "Utilizator Portal",
"originalLead": "Lead Original",
"acceptanceStatus": "Stare Acceptare",
"accountIsInactive": "Cont Inactiv",
"acceptanceStatusMeetings": "Stare Acceptare (Întâlniri)",
"acceptanceStatusCalls": "Stare Acceptare (Apeluri)"
},
"links": {
"opportunities": "Oportunități",
"cases": "Cazuri",
"targetLists": "Liste Țintă",
"campaignLogRecords": "Jurnal Campanie",
"campaign": "Campanie",
"account": "Cont (Principal)",
"accounts": "Conturi",
"casesPrimary": "Cazuri (Principal)",
"tasksPrimary": "Sarcini (extins)",
"portalUser": "Utilizator Portal",
"originalLead": "Lead Original",
"documents": "Documente"
},
"labels": {
"Create Contact": "Creare Contact"
},
"options": {
"opportunityRole": {
"": "--Nici unul--",
"Decision Maker": "Factor de decizie"
}
}
},
"presetFilters": {
"portalUsers": "Utilizatori Portal",
"notPortalUsers": "Nu sunt Utilizatori ai Portalului",
"accountActive": "Activ"
}
}
@@ -0,0 +1,40 @@
{
"labels": {
"Create Document": "Creați Document",
"Details": "Detalii"
},
"fields": {
"name": "Nume",
"status": "Stare",
"file": "Fișier",
"type": "Tip",
"publishDate": "Publică Dată",
"expirationDate": "Data expirării",
"description": "Descriere",
"accounts": "Conturi",
"folder": "Dosar"
},
"links": {
"accounts": "Conturi",
"opportunities": "Oportunități",
"folder": "Dosar",
"leads": "Lead-uri",
"contacts": "Contacte"
},
"options": {
"status": {
"Active": "Activ",
"Draft": "Schiță",
"Expired": "Expirat",
"Canceled": "Anulat"
},
"type": {
"": "Nici unul",
"License Agreement": "Acord de licențiere"
}
},
"presetFilters": {
"active": "Activ",
"draft": "Schiță"
}
}
@@ -0,0 +1,10 @@
{
"labels": {
"Create DocumentFolder": "Creați Dosarul Documentului",
"Manage Categories": "Gestionare Dosare",
"Documents": "Documente"
},
"links": {
"documents": "Documente"
}
}
@@ -0,0 +1,10 @@
{
"labels": {
"Create Lead": "Creați Lead",
"Create Contact": "Creați Contact",
"Add to Contact": "Adăugați la Contacte",
"Add to Lead": "Adăugați la Lead",
"Create Task": "Creați Sarcină",
"Create Case": "Creați Caz"
}
}
@@ -0,0 +1,29 @@
{
"fields": {
"name": "Nume",
"status": "Stare",
"target": "Țintă",
"sentAt": "Dată Trimitere",
"attemptCount": "Încercări",
"emailAddress": "Adresă Email",
"massEmail": "Email în Masă",
"isTest": "Este test"
},
"links": {
"target": "Țintă",
"massEmail": "Email în Masă"
},
"options": {
"status": {
"Pending": "În așteptare",
"Sent": "Trimis",
"Failed": "Eșuat",
"Sending": "Se trimite"
}
},
"presetFilters": {
"pending": "În așteptare",
"sent": "Trimis",
"failed": "Eșuat"
}
}
@@ -1,79 +1,105 @@
{
"scopeNames": {
"Account": "Cont",
"Contact": "Contact",
"Lead": "Lead",
"Target": "Tinta",
"Opportunity": "Oportunitate",
"Meeting": "Intalnire",
"Calendar": "Calendar",
"Call": "Apel",
"Task": "Activitate",
"Case": "Caz",
"InboundEmail": "Intrari Email"
},
"scopeNamesPlural": {
"Account": "Conturi",
"Contact": "Contacte",
"Lead": "Lead-uri",
"Target": "Tinte",
"Opportunity": "Oportunitati",
"Meeting": "Intalniri",
"Calendar": "Calendar",
"Call": "Apeluri",
"Task": "Activitati",
"Case": "Cazuri",
"InboundEmail": "Email-uri intrate"
},
"dashlets": {
"Leads": "Lead-urile mele",
"Opportunities": "Oportunitatile mele",
"Tasks": "Activitatile mele",
"Cases": "Cazurile mele",
"Calendar": "Calendar",
"OpportunitiesByStage": "Oportunitati in functie de stadiu",
"OpportunitiesByLeadSource": "Oportunitati in functie de Sursal Lead-ului",
"SalesByMonth": "Vanzari pe luna",
"SalesPipeline": "Flux Vanzari"
},
"labels": {
"Create InboundEmail": "Creare Email intrare",
"Activities": "Activitati",
"History": "Istoric",
"Attendees": "Participanti",
"Schedule Meeting": "Programeaza Intalnire",
"Schedule Call": "Programeaza Apel",
"Compose Email": "Compune Email",
"Log Meeting": "Log Intalnire",
"Log Call": "Log Apel",
"Archive Email": "Arhiveaza Email",
"Create Task": "Creaza Activitate",
"Tasks": "Activitati"
},
"fields": {
"billingAddressCity": "Oras",
"billingAddressCountry": "Tara",
"billingAddressPostalCode": "Code Postal ",
"billingAddressState": "Stat",
"billingAddressStreet": "Strada",
"addressCity": "Oras",
"addressStreet": "Strada",
"addressCountry": "Tara",
"addressState": "Stat",
"addressPostalCode": "Code Postal ",
"shippingAddressCity": "City (Shipping)",
"shippingAddressStreet": "Street (Shipping)",
"shippingAddressCountry": "Country (Shipping)",
"shippingAddressState": "State (Shipping)",
"shippingAddressPostalCode": "Postal Code (Shipping)"
},
"links": {
"contacts": "Contacte",
"opportunities": "Oportunitati",
"leads": "Lead-uri",
"meetings": "Intalniri",
"calls": "Apeluri",
"tasks": "Activitati",
"emails": "Email-uri"
}
}
"links": {
"parent": "Părinte",
"contacts": "Contacte",
"opportunities": "Oportunități",
"leads": "Lead-uri",
"meetings": "Întâlniri",
"calls": "Apeluri",
"tasks": "Sarcini",
"emails": "Email-uri",
"accounts": "Conturi",
"cases": "Cazuri",
"documents": "Documente",
"account": "Cont",
"opportunity": "Oportunitate"
},
"scopeNames": {
"Account": "Cont",
"Target": "Țintă",
"Opportunity": "Oportunitate",
"Meeting": "Întâlnire",
"Call": "Apel",
"Task": "Sarcină",
"Case": "Caz",
"DocumentFolder": "Dosar Document",
"Campaign": "Campanie",
"TargetList": "Listă Țintă",
"MassEmail": "Email în Masă",
"EmailQueueItem": "Email Element de așteptare",
"CampaignTrackingUrl": "Urmărire URL",
"Activities": "Activități",
"KnowledgeBaseArticle": "Atricol Bază de Cunoștințe",
"KnowledgeBaseCategory": "Categorie Bază de Cunoștințe",
"CampaignLogRecord": "Înregistrare Jurnal Campanie"
},
"scopeNamesPlural": {
"Account": "Conturi",
"Contact": "Contacte",
"Lead": "Lead-uri",
"Target": "Ținte",
"Opportunity": "Oportunități",
"Meeting": "Întâlniri",
"Call": "Apeluri",
"Task": "Sarcini",
"Case": "Cazuri",
"Document": "Documente",
"DocumentFolder": "Dosare Document",
"Campaign": "Campanii",
"TargetList": "Liste Țintă",
"MassEmail": "Email-uri în Masă",
"EmailQueueItem": "Email Element de Așteptare",
"CampaignTrackingUrl": "Urmărire URL-uri",
"Activities": "Activității",
"KnowledgeBaseArticle": "Bază de Cunoștințe",
"KnowledgeBaseCategory": "Categorii Bază de Cunoștințe",
"CampaignLogRecord": "Înregistrări Jurnal Campanie"
},
"dashlets": {
"Leads": "Lead-urile mele",
"Opportunities": "Oportunitățile mele",
"Tasks": "Sarcinile mele",
"Cases": "Cazurile mele",
"Calls": "Apelurile mele",
"Meetings": "Întâlnirile mele",
"OpportunitiesByStage": "Oportunități în funcție de stadiu",
"OpportunitiesByLeadSource": "Oportunități în funcție de Sursa Lead-ului",
"SalesByMonth": "Vânzări pe luna",
"SalesPipeline": "Linie de Vanzari",
"Activities": "Activitățiile mele"
},
"labels": {
"Create InboundEmail": "Creare Email Intrare",
"Activities": "Activități",
"History": "Istoric",
"Attendees": "Participanți",
"Schedule Meeting": "Planifică Întâlnire",
"Schedule Call": "Planifică Apel",
"Compose Email": "Compune Email",
"Log Meeting": "Jurnal Intalnire",
"Log Call": "Jurnal Apel",
"Archive Email": "Arhivează Email",
"Create Task": "Crează Sarcină",
"Tasks": "Activități"
},
"fields": {
"billingAddressCity": "Oraș",
"addressCity": "Oraș",
"billingAddressCountry": "Țara",
"addressCountry": "Țara",
"billingAddressPostalCode": "Cod Poștal",
"addressPostalCode": "Cod Poștal",
"billingAddressState": "Stat",
"addressState": "Stat",
"billingAddressStreet": "Strada",
"addressStreet": "Strada",
"billingAddressMap": "Hartă",
"addressMap": "Hartă",
"shippingAddressCity": "Oraș (Livrare)",
"shippingAddressStreet": "Stradă (Livrare)",
"shippingAddressCountry": "Țară (Livrare)",
"shippingAddressState": "Stat (Livrare)",
"shippingAddressPostalCode": "Cod Poștal (Livrare)",
"shippingAddressMap": "Hartă (Livrare)"
}
}
@@ -0,0 +1,47 @@
{
"labels": {
"Create KnowledgeBaseArticle": "Creați Articol",
"Any": "Oricare",
"Send in Email": "Trimite în Email",
"Move Up": "Mută Sus",
"Move Down": "Mută Jos",
"Move to Top": "Mută la Început",
"Move to Bottom": "Mută la Sfârșit"
},
"fields": {
"name": "Nume",
"status": "Stare",
"type": "Tip",
"attachments": "Atașamente",
"publishDate": "Publică Dată",
"expirationDate": "Data Expirării",
"description": "Descriere",
"body": "Conținut",
"categories": "Categorii",
"language": "Limbă",
"portals": "Portaluri"
},
"links": {
"cases": "Cazuri",
"opportunities": "Oportunități",
"categories": "Ctegorii",
"portals": "Portaluri"
},
"options": {
"status": {
"In Review": "În revizuire",
"Draft": "Schiță",
"Archived": "Arhivat",
"Published": "Publicat"
},
"type": {
"Article": "Articol"
}
},
"tooltips": {
"portals": "Articolul o să fie disponibil doar în portalurile specificate."
},
"presetFilters": {
"published": "Publicat"
}
}
@@ -0,0 +1,10 @@
{
"labels": {
"Create KnowledgeBaseCategory": "Creați Categorie",
"Manage Categories": "Gestionare Categorii",
"Articles": "Articole"
},
"links": {
"articles": "Articole"
}
}
@@ -1,50 +1,65 @@
{
"labels": {
"Converted To": "Convertit la",
"Create Lead": "Creare Lead",
"Convert": "Converteste",
"convert": "converteste"
"labels": {
"Converted To": "Convertit la",
"Create Lead": "Creare Lead",
"Convert": "Convertește",
"convert": "convertește"
},
"fields": {
"name": "Nume",
"title": "Titlu",
"website": "Site Web",
"phoneNumber": "Telefon",
"accountName": "Nume Cont",
"doNotCall": "Nu sunați",
"address": "Adresa",
"status": "Stare",
"source": "Sursa",
"opportunityAmount": "Sumă Oportunitate",
"opportunityAmountConverted": "Sumă Oportunitate (convertită)",
"description": "Descriere",
"createdAccount": "Cont",
"createdOpportunity": "Oportunitate",
"campaign": "Campanie",
"targetLists": "Liste Țintă",
"targetList": "Listă Țintă",
"industry": "Industrie",
"acceptanceStatus": "Stare Acceptare",
"opportunityAmountCurrency": "Valută Sumă Oportunitate",
"acceptanceStatusMeetings": "Stare Acceptare (Întâlniri)",
"acceptanceStatusCalls": "Stare Acceptare (Apeluri)"
},
"links": {
"targetLists": "Liste Țintă",
"campaignLogRecords": "Jurnal Campanie",
"campaign": "Campanie",
"createdAccount": "Cont",
"createdOpportunity": "Oportunitate",
"cases": "Cazuri",
"documents": "Documente"
},
"options": {
"status": {
"New": "Nou",
"Assigned": "Atribuit",
"In Process": "În Proces",
"Converted": "Convertit",
"Recycled": "Reciclat",
"Dead": "Inactiv"
},
"fields": {
"name": "Nume",
"emailAddress": "Email",
"title": "Titlu",
"website": "Site Web",
"phoneNumber": "Telefon",
"accountName": "Nume Cont",
"doNotCall": "Nu sunati",
"address": "Adresa",
"status": "Status",
"source": "Sursa",
"opportunityAmount": "Suma Oportunitate",
"description": "Descriere",
"createdAccount": "Cont",
"createdContact": "Contact",
"createdOpportunity": "Oportunitate"
},
"links": {
},
"options": {
"status": {
"New": "Nou",
"Assigned": "Alocat",
"In Process": "In Proces",
"Converted": "Convertit",
"Recycled": "Reciclat",
"Dead": "Inactiv"
},
"source": {
"Call": "Apel",
"Email": "Email",
"Existing Customer": "Client Existent",
"Partner": "Partener",
"Public Relations": "Relatii Publice",
"Web Site": "Site Web",
"Campaign": "Campanie",
"Other": "Altele"
}
},
"presetFilters": {
"active": "Activ"
"source": {
"": "Nici unul",
"Call": "Apel",
"Existing Customer": "Client Existent",
"Partner": "Partener",
"Public Relations": "Relații Publice",
"Web Site": "Site Web",
"Campaign": "Campanie",
"Other": "Altele"
}
}
},
"presetFilters": {
"active": "Activ",
"converted": "Convertit"
}
}
@@ -0,0 +1,56 @@
{
"fields": {
"name": "Nume",
"status": "Stare",
"storeSentEmails": "Stocați email-urile trimise",
"startAt": "Data Începerii",
"fromAddress": "Din Adresă",
"fromName": "Din Nume",
"replyToAddress": "Răspundeți la Adresă",
"replyToName": "Răspundeți la Nume",
"campaign": "Campanie",
"emailTemplate": "Șablon Email",
"inboundEmail": "Cont Email",
"targetLists": "Liste Țintă",
"excludingTargetLists": "Excluderea listelor țintă",
"smtpAccount": "Cont SMTP"
},
"links": {
"targetLists": "Liste Țintă",
"excludingTargetLists": "Excluderea listelor țintă",
"queueItems": "Elemente de Așteptare",
"campaign": "Campanie",
"emailTemplate": "Șablon Email",
"inboundEmail": "Cont Email"
},
"options": {
"status": {
"Draft": "Schiță",
"Pending": "În așteptare",
"In Process": "În proces",
"Complete": "Terminat",
"Canceled": "Anulat",
"Failed": "Eșuat"
}
},
"labels": {
"Create MassEmail": "Creați Email în Masă",
"Send Test": "Trimite Test",
"System SMTP": "Sistem SMTP",
"system": "sistem",
"group": "grup"
},
"messages": {
"selectAtLeastOneTarget": "Selectați cel puțin o țintă.",
"testSent": "Email(uri) test care ar trebui să fie trimise"
},
"tooltips": {
"optOutEntirely": "Adresele de email ale destinatarilor care s-au dezabonat, vor fi marcate ca fiind oprite și nu vor mai primi email-uri în masă ",
"targetLists": "Țintele care ar trebui să primească mesaje.",
"excludingTargetLists": "Țintele care nu ar trebui să primească mesaje.",
"storeSentEmails": "Email-urile o să fie stocate în CRM."
},
"presetFilters": {
"complete": "Terminat"
}
}
@@ -1,36 +1,50 @@
{
"fields": {
"name": "Nume",
"parent": "Parinte",
"status": "Status",
"dateStart": "Data inceput",
"dateEnd": "Data terminare",
"duration": "Durata",
"description": "Descriere",
"users": "Utilizatori",
"contacts": "Contacte",
"leads": "Lead-uri"
"fields": {
"name": "Nume",
"parent": "Părinte",
"status": "Stare",
"dateStart": "Dată Început",
"dateEnd": "Dată Terminare",
"duration": "Durată",
"description": "Descriere",
"users": "Utilizatori",
"contacts": "Contacte",
"leads": "Lead-uri",
"reminders": "Mementouri",
"account": "Cont",
"acceptanceStatus": "Stare Acceptare"
},
"options": {
"status": {
"Planned": "Planificat",
"Held": "Reținut",
"Not Held": "Nereținut"
},
"links": {
},
"options": {
"status": {
"Planned": "Planificat",
"Held": "Detinut",
"Not Held": "Nedetinut"
}
},
"labels": {
"Create Meeting": "Creare Intalnire",
"Set Held": "Setare detinere",
"Set Not Held": "Setare nedetinere",
"Send Invitations": "Trimite invitatii",
"Saved as Held": "Saved as Held",
"Saved as Not Held": "Saved as Not Held"
},
"presetFilters": {
"planned": "Planificat",
"held": "Detinut",
"todays": "Today's"
"acceptanceStatus": {
"None": "Nici unul",
"Accepted": "Acceptat",
"Declined": "Respins",
"Tentative": "Tentativă"
}
}
},
"massActions": {
"setHeld": "Setat ca reținut",
"setNotHeld": "Setat ca nereținut"
},
"labels": {
"Create Meeting": "Creare Întâlnire",
"Set Held": "Setat ca reținut",
"Set Not Held": "Setat ca nereținut",
"Send Invitations": "Trimite Invitații",
"on time": "la timp",
"before": "înainte"
},
"presetFilters": {
"planned": "Planificat",
"held": "Reținut",
"todays": "Astăzi"
},
"messages": {
"nothingHasBeenSent": "Nu a fost trimis nimic"
}
}
@@ -1,38 +1,50 @@
{
"fields": {
"name": "Nume",
"account": "Cont",
"stage": "Stadiu",
"amount": "Suma",
"probability": "Probabilitate, %",
"leadSource": "Sursa Lead",
"doNotCall": "Nu sunati",
"closeDate": "Data Inchiderii",
"contacts": "Contacte",
"description": "Descriere"
},
"links": {
"contacts": "Contacte"
},
"options": {
"stage": {
"Prospecting": "Prospectare",
"Qualification": "Calificari",
"Needs Analysis": "Necesita Analiza",
"Value Proposition": "Valoare Propunere",
"Id. Decision Makers": "Id. Factori de Decizie",
"Perception Analysis": "Analiza Perceptiei",
"Proposal/Price Quote": "Propunere/Oferta Pret",
"Negotiation/Review": "Negociere/Revizuire",
"Closed Won": "Inchide ca Castigat",
"Closed Lost": "Inchide ca Pierdut"
}
},
"labels": {
"Create Opportunity": "Creare Oportunitate"
},
"presetFilters": {
"open": "Deschide",
"won": "Won"
"fields": {
"name": "Nume",
"account": "Cont",
"stage": "Stadiu",
"amount": "Sumă",
"probability": "Probabilitate, %",
"leadSource": "Sursă Lead",
"doNotCall": "Nu sunați",
"closeDate": "Data Închiderii",
"contacts": "Contacte",
"description": "Descriere",
"amountConverted": "Sumă (convertit)",
"amountWeightedConverted": "Suma ponderată",
"campaign": "Campanie",
"originalLead": "Lead Original",
"amountCurrency": "Sumă Valută",
"contactRole": "Rol Contact"
},
"links": {
"contacts": "Contacte",
"documents": "Documente",
"campaign": "Campanie",
"originalLead": "Lead Original"
},
"options": {
"stage": {
"Prospecting": "Prospectare",
"Qualification": "Calificări",
"Proposal": "Propunere",
"Negotiation": "Negociere",
"Needs Analysis": "Necesită Analiză",
"Value Proposition": "Propunere Valoare",
"Id. Decision Makers": "Id. Factori de Decizie",
"Perception Analysis": "Analiză Percepției",
"Proposal/Price Quote": "Propunere/Preț Ofertă",
"Negotiation/Review": "Negociere/Revizuire",
"Closed Won": "Închide Câștigat",
"Closed Lost": "Închide Pierdut"
}
}
},
"labels": {
"Create Opportunity": "Creare Oportunitate"
},
"presetFilters": {
"open": "Deschide",
"won": "Câștigat",
"lost": "Pierdut"
}
}
@@ -0,0 +1,5 @@
{
"links": {
"articles": "Articole Bază de Cunoștință"
}
}
@@ -0,0 +1,8 @@
{
"options": {
"job": {
"ProcessMassEmail": "Trimite Email-uri în Masă",
"ControlKnowledgeBaseArticleStatus": "Controlează Starea Articolelor Bază de Cunoștințe"
}
}
}
@@ -0,0 +1,30 @@
{
"fields": {
"name": "Nume",
"description": "Descriere",
"entryCount": "Numără Intrări",
"campaigns": "Campanii",
"endDate": "Dată Terminare",
"targetLists": "Liste Țintă",
"includingActionList": "Include",
"excludingActionList": "Exclude"
},
"links": {
"accounts": "Conturi",
"contacts": "Contacte",
"leads": "Lead-uri",
"campaigns": "Campanii",
"massEmails": "Email-uri în Masă"
},
"options": {
"type": {
"Television": "Televiziune"
}
},
"labels": {
"Create TargetList": "Creați Listă Țintă",
"Opted Out": "Ales să nu",
"Cancel Opt-Out": "Anulează alegerea de ieșire",
"Opt-Out": "Aleg să nu"
}
}
@@ -1,37 +1,45 @@
{
"fields": {
"name": "Nume",
"parent": "Parinte",
"status": "Status",
"dateStart": "Data inceput",
"dateEnd": "Data scadenta",
"priority": "Prioritate",
"description": "Descriere",
"isOverdue": "Este Restant"
"fields": {
"name": "Nume",
"parent": "Părinte",
"status": "Stare",
"dateStart": "Dată Începere",
"dateEnd": "Dată Scadență",
"dateStartDate": "Dată Începere (toată ziua)",
"dateEndDate": "Dată Terminare (toată ziua)",
"priority": "Prioritate",
"description": "Descriere",
"isOverdue": "Este Restant",
"account": "Cont",
"dateCompleted": "Dată Terminare",
"attachments": "Atașamente",
"reminders": "Mementouri"
},
"links": {
"attachments": "Atașamente",
"account": "Cont"
},
"options": {
"status": {
"Not Started": "Nu a început",
"Started": "Început",
"Completed": "Terminat",
"Canceled": "Anulat",
"Deferred": "Amânat"
},
"links": {
},
"options": {
"status": {
"Not Started": "Ne inceput",
"Started": "Inceput",
"Completed": "Completat",
"Canceled": "Anulat"
},
"priority" : {
"Low": "Scazut",
"Normal": "Normal",
"High": "Inalt",
"Urgent": "Urgent"
}
},
"labels": {
"Create Task": "Creaza Activitate"
},
"presetFilters": {
"active": "Activ",
"completed": "Completat",
"todays": "Today's",
"overdue": "Overdue"
"priority": {
"Low": "Scăzut",
"High": "Înalt"
}
}
},
"labels": {
"Create Task": "Crează Sarcine",
"Complete": "Terminat"
},
"presetFilters": {
"completed": "Terminat",
"deferred": "Amânat",
"todays": "Astăzi",
"overdue": "Restant"
}
}
@@ -0,0 +1,10 @@
{
"fields": {
"acceptanceStatus": "Stare Acceptare",
"acceptanceStatusMeetings": "Stare Acceptare (Întâlniri)",
"acceptanceStatusCalls": "Stare Acceptare (Apeluri)"
},
"links": {
"targetLists": "Liste Țintă"
}
}
@@ -26,6 +26,8 @@
"stage": {
"Prospecting": "Привлечение клиента",
"Qualification": "Оценка возможности",
"Proposal": "Предложение",
"Negotiation": "Согласование",
"Needs Analysis": "Требует анализа",
"Value Proposition": "Выбор предложения/оферты",
"Id. Decision Makers": "Определение ответственного лица",
@@ -27,6 +27,8 @@
"stage": {
"Prospecting": "Izviđanje",
"Qualification": "Kvalifikacija",
"Proposal": "Predlog",
"Negotiation": "Pregovori",
"Needs Analysis": "Analiza potrebna",
"Value Proposition": "Dobitak",
"Id. Decision Makers": "Id. Donosioci odluka",
@@ -26,6 +26,8 @@
"stage": {
"Prospecting": "Araştırma",
"Qualification": "Yeterlilik",
"Proposal": "Proposal",
"Negotiation": "Görüşme",
"Needs Analysis": "Analiz Yapılmalı",
"Value Proposition": "Değer Önerisi",
"Id. Decision Makers": "Id. Karar Vericiler",

Some files were not shown because too many files have changed in this diff Show More