Compare commits

...

691 Commits

Author SHA1 Message Date
frappe-pr-bot
fa14d4ad15 chore: update POT file 2025-08-03 09:38:21 +00:00
Shariq Ansari
44deb5878c
Merge pull request #1103 from frappe/l10n_develop3
chore: sync translations from crowdin
2025-08-03 13:59:40 +05:30
Shariq Ansari
2601c1d059 chore: Persian translations 2025-08-03 13:43:25 +05:30
Shariq Ansari
7e51d96379 chore: Indonesian translations 2025-08-03 13:43:12 +05:30
Shariq Ansari
4c53dd6ea3 chore: Persian translations 2025-08-01 13:09:10 +05:30
Shariq Ansari
c64dcb43b4
Merge pull request #1095 from frappe/l10n_develop3 2025-07-31 16:38:53 +05:30
Shariq Ansari
7dafba9fc3
Merge pull request #1100 from shariquerik/bug-fix 2025-07-31 16:32:03 +05:30
Shariq Ansari
abc501825a fix: update delete button click handler in Deal component 2025-07-31 16:30:25 +05:30
Shariq Ansari
3ad2a56efb
Merge pull request #1094 from C-L-STARK/patch-1 2025-07-31 16:28:38 +05:30
Shariq Ansari
c53e486bf0 fix: update init scripts for consistent Redis configuration and branch usage 2025-07-31 16:02:00 +05:30
Shariq Ansari
d852fe1e9f
Merge pull request #1096 from shariquerik/refactor-2 2025-07-30 18:13:41 +05:30
Shariq Ansari
d431d5b4b1 refactor: remove unused get_lead function and related imports from api.py 2025-07-30 17:55:47 +05:30
Shariq Ansari
ba99d14f68 refactor: clean up unused variables and streamline tab management in Deal, Lead, and MobileLead components 2025-07-30 17:50:16 +05:30
Shariq Ansari
7e42599b49 refactor: update mobile lead/deal components 2025-07-30 17:31:22 +05:30
Shariq Ansari
c38c190d42 fix: update docname references to use dealId and leadId in Deal and Lead components 2025-07-30 16:43:40 +05:30
Shariq Ansari
6e8d869afb fix: update whatsappMessages auto property based on whatsappEnabled state 2025-07-30 16:24:19 +05:30
Shariq Ansari
63d6062673 refactor: update button icon rendering in AttachmentArea component 2025-07-30 16:12:21 +05:30
Shariq Ansari
af830b8782 refactor: use doc in activity & child components 2025-07-30 16:11:49 +05:30
Shariq Ansari
0605cf5fd0 refactor: updateField & deleteDeal/Lead 2025-07-30 15:16:13 +05:30
Shariq Ansari
4acb4dd3a7 fix: remount on route change with param and hash change 2025-07-30 15:13:38 +05:30
Shariq Ansari
72c31b9f21 chore: Croatian translations 2025-07-30 13:11:43 +05:30
Shariq Ansari
f2b1c24d19 chore: Serbian (Latin) translations 2025-07-30 13:11:28 +05:30
Shariq Ansari
f86bb0ada2 chore: Bosnian translations 2025-07-30 13:11:27 +05:30
Shariq Ansari
ec4866f39a chore: Indonesian translations 2025-07-30 13:11:26 +05:30
Shariq Ansari
7961b614f1 chore: Swedish translations 2025-07-30 13:11:24 +05:30
Shariq Ansari
901d84d070 chore: Serbian (Cyrillic) translations 2025-07-30 13:11:22 +05:30
Shariq Ansari
0144bc109a fix: use document.doc instead of lead.data/deal.data 2025-07-30 12:56:27 +05:30
Shariq Ansari
ca5d82f5be fix: handle errors when creating document resources 2025-07-30 12:50:35 +05:30
C.L.STARK
b6a6152a49
Update init.sh
Fixed: docker setup error in crm-frappe container.
2025-07-30 05:11:40 +08:00
Shariq Ansari
5e19a83f8a
Merge pull request #1090 from frappe/l10n_develop3 2025-07-29 16:10:10 +05:30
Shariq Ansari
cd30e9d533
Merge pull request #1091 from shariquerik/modified-error 2025-07-29 14:13:59 +05:30
Shariq Ansari
ea0011771b fix: reload doc after sending email 2025-07-29 14:08:11 +05:30
Shariq Ansari
94b0077b2a chore: Esperanto translations 2025-07-29 12:32:06 +05:30
Shariq Ansari
ea815d0147 chore: Croatian translations 2025-07-29 12:32:04 +05:30
Shariq Ansari
0ae048d396 chore: Thai translations 2025-07-29 12:32:03 +05:30
Shariq Ansari
972558396e chore: Persian translations 2025-07-29 12:32:02 +05:30
Shariq Ansari
c218241e80 chore: Vietnamese translations 2025-07-29 12:32:00 +05:30
Shariq Ansari
e616e69aa4 chore: Chinese Simplified translations 2025-07-29 12:31:59 +05:30
Shariq Ansari
2627471b23 chore: Turkish translations 2025-07-29 12:31:58 +05:30
Shariq Ansari
a2c42ee5a7 chore: Russian translations 2025-07-29 12:31:56 +05:30
Shariq Ansari
8d89caeba7 chore: Portuguese translations 2025-07-29 12:31:55 +05:30
Shariq Ansari
b0393b532c chore: Dutch translations 2025-07-29 12:31:54 +05:30
Shariq Ansari
2ea875f2cb chore: Hungarian translations 2025-07-29 12:31:53 +05:30
Shariq Ansari
d1d2900847 chore: Czech translations 2025-07-29 12:31:51 +05:30
Shariq Ansari
98d851d76d chore: Arabic translations 2025-07-29 12:31:50 +05:30
Shariq Ansari
30fdc1db4c chore: Spanish translations 2025-07-29 12:31:49 +05:30
Shariq Ansari
12df15154d chore: French translations 2025-07-29 12:31:47 +05:30
Shariq Ansari
0bf6c01f91 chore: German translations 2025-07-29 12:31:46 +05:30
Shariq Ansari
497263f367 chore: Serbian (Latin) translations 2025-07-29 12:31:44 +05:30
Shariq Ansari
cd458d6d22 chore: Bosnian translations 2025-07-29 12:31:43 +05:30
Shariq Ansari
a2b55166ed chore: Indonesian translations 2025-07-29 12:31:41 +05:30
Shariq Ansari
570d31b6d1 chore: Portuguese, Brazilian translations 2025-07-29 12:31:40 +05:30
Shariq Ansari
2483dd6828 chore: Swedish translations 2025-07-29 12:31:38 +05:30
Shariq Ansari
1adef98c57 chore: Serbian (Cyrillic) translations 2025-07-29 12:31:37 +05:30
Shariq Ansari
dfd3c8f2bf chore: Polish translations 2025-07-29 12:31:36 +05:30
Shariq Ansari
015c592978 chore: Italian translations 2025-07-29 12:31:34 +05:30
Shariq Ansari
8984cea367
Merge pull request #1087 from shariquerik/bug 2025-07-29 11:34:25 +05:30
Shariq Ansari
c3cf63dfb7 fix: format NumberChart component for better readability 2025-07-29 11:32:49 +05:30
Shariq Ansari
c532b61ef6 fix: fields should not be mandatory in single doctype 2025-07-29 11:32:37 +05:30
Shariq Ansari
491fbb7801
Merge pull request #1084 from shariquerik/exchange_rate_fix-1 2025-07-28 18:05:34 +05:30
Shariq Ansari
4c7a40d8bc fix: fixed labels 2025-07-28 18:01:36 +05:30
Shariq Ansari
c866d1e836
Merge pull request #1082 from shariquerik/exchange_rate_fix 2025-07-28 17:57:59 +05:30
Shariq Ansari
c72160b2a4 fix: remove unused Link component import from GeneralSettings.vue 2025-07-28 17:52:58 +05:30
Shariq Ansari
3143f14e0b
Merge branch 'develop' into exchange_rate_fix 2025-07-28 17:51:03 +05:30
Shariq Ansari
e06a365029 fix: update description for mandatory fields in deal value forecasting 2025-07-28 17:49:52 +05:30
Shariq Ansari
36e79e49da fix: added exchange rate provider & currency together in settings modal 2025-07-28 17:48:26 +05:30
Shariq Ansari
5bee7022b2 fix: removed currency exchange rate settings and moved it to crm settings 2025-07-28 17:47:20 +05:30
Shariq Ansari
e578513eaf fix: Improve error handling in document update process 2025-07-28 16:43:46 +05:30
Meer Uzair
8de2a69a99 fix: Correct field name from 'agent' to 'user' in CRM Telephony Agent creation
- The `CRM Telephony Agent` doctype requires the field `user` (not `agent`) as per its schema (`reqd: 1` + `autoname: field:user`).
- This fixes the `ValidationError: User is required` by using the correct field name when creating a new agent.
2025-07-28 16:23:52 +05:30
frappe-pr-bot
890299c2f3 chore: update POT file 2025-07-28 16:23:52 +05:30
Shariq Ansari
2a6f1c402b chore: Serbian (Latin) translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
25d2b1889f chore: Serbian (Cyrillic) translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
ee843fae26 chore: Serbian (Latin) translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
fea35205b7 chore: Bosnian translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
ee28543180 chore: Indonesian translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
4d7a66f1d8 chore: Swedish translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
3b9ff8d58f chore: Serbian (Cyrillic) translations 2025-07-28 16:23:52 +05:30
Shariq Ansari
b46d5a4e5e fix: update exchange rate fetching logic to include service provider context and improve error handling 2025-07-28 16:20:08 +05:30
Shariq Ansari
23a823f2bb feat: enhance document handling with error triggering and settings helpers via app 2025-07-28 16:19:49 +05:30
Shariq Ansari
56ef00536a
Merge pull request #1077 from MeerUzairWasHere/patch-1 2025-07-28 15:56:07 +05:30
Shariq Ansari
0081006a64
Merge pull request #1076 from frappe/pot_develop_2025-07-27 2025-07-28 15:53:51 +05:30
Shariq Ansari
1c98d81c2c
Merge pull request #1074 from frappe/l10n_develop3 2025-07-28 15:53:33 +05:30
Shariq Ansari
7e9d9a5fed fix: moved get_exchange_rate api to exchange rate settings 2025-07-28 13:00:31 +05:30
Shariq Ansari
45826e0a88 fix: added currency exchange rate settings 2025-07-28 12:59:34 +05:30
Shariq Ansari
8815433962 chore: Serbian (Latin) translations 2025-07-28 12:31:04 +05:30
Shariq Ansari
e4261820d4 chore: Serbian (Cyrillic) translations 2025-07-28 12:31:03 +05:30
Shariq Ansari
5bfcaf4809 fix: get latest exchange rate 2025-07-28 12:12:34 +05:30
Meer Uzair
7f4f6ff651
fix: Correct field name from 'agent' to 'user' in CRM Telephony Agent creation
- The `CRM Telephony Agent` doctype requires the field `user` (not `agent`) as per its schema (`reqd: 1` + `autoname: field:user`).
- This fixes the `ValidationError: User is required` by using the correct field name when creating a new agent.
2025-07-27 22:58:43 +05:30
frappe-pr-bot
7d9b7e92fb chore: update POT file 2025-07-27 09:38:13 +00:00
Shariq Ansari
63963414c1 chore: Serbian (Latin) translations 2025-07-26 12:04:14 +05:30
Shariq Ansari
f0ee245f75 chore: Bosnian translations 2025-07-26 12:04:13 +05:30
Shariq Ansari
a4710a8b85 chore: Indonesian translations 2025-07-26 12:04:12 +05:30
Shariq Ansari
201ad61e6d chore: Swedish translations 2025-07-26 12:04:10 +05:30
Shariq Ansari
bbb9eefc9a chore: Serbian (Cyrillic) translations 2025-07-26 12:04:08 +05:30
Shariq Ansari
d53b503805
Merge pull request #1070 from shariquerik/log-call-fix 2025-07-24 13:14:47 +05:30
Shariq Ansari
b1d90952c4 refactor: MultiActionButton dropdown 2025-07-24 13:10:58 +05:30
Shariq Ansari
742bd6e213 fix: update label for call log actions to "Log a Call" 2025-07-24 12:47:14 +05:30
Shariq Ansari
e0abb53d4c
Merge pull request #1068 from shariquerik/primary-mobile-no-fix 2025-07-24 12:32:26 +05:30
Shariq Ansari
8e08e6f415 fix: make mobile no primary on create of contact 2025-07-24 12:29:54 +05:30
Shariq Ansari
e883ea1346
Merge pull request #1066 from shariquerik/compatibility 2025-07-24 11:56:24 +05:30
Shariq Ansari
0327b37d2f docs: add compatibility section with supported Frappe and ERPNext versions in README 2025-07-24 11:54:01 +05:30
Shariq Ansari
699feead15
Merge pull request #1064 from frappe/l10n_develop3 2025-07-24 11:28:50 +05:30
Shariq Ansari
a08fabaed9 chore: Bosnian translations 2025-07-24 11:26:11 +05:30
Shariq Ansari
61e276df37 chore: Croatian translations 2025-07-24 11:26:09 +05:30
Shariq Ansari
076762e2f3
Merge pull request #1059 from shariquerik/refactor-1 2025-07-23 13:35:55 +05:30
Shariq Ansari
9761989ea4 fix: improve exchange rate fetching with retry logic and default return value 2025-07-23 13:33:41 +05:30
Shariq Ansari
5ae7698704
Merge pull request #1057 from frappe/l10n_develop3 2025-07-23 13:23:30 +05:30
Shariq Ansari
c84ac29332 refactor: rename update_close_date to update_closed_date and adjust related logic 2025-07-23 13:21:02 +05:30
Shariq Ansari
cf8a1ce8a3 refactor: cache assignees 2025-07-23 13:12:12 +05:30
Shariq Ansari
1507701981 chore: Serbian (Latin) translations 2025-07-23 10:58:29 +05:30
Shariq Ansari
b4796efed1 chore: Bosnian translations 2025-07-23 10:58:28 +05:30
Shariq Ansari
273f7d7f07 chore: Croatian translations 2025-07-23 10:58:26 +05:30
Shariq Ansari
8474630f4c chore: Thai translations 2025-07-23 10:58:25 +05:30
Shariq Ansari
907ec5fa3c chore: Persian translations 2025-07-23 10:58:24 +05:30
Shariq Ansari
43001bee9f chore: Indonesian translations 2025-07-23 10:58:22 +05:30
Shariq Ansari
5479a6b885 chore: Portuguese, Brazilian translations 2025-07-23 10:58:21 +05:30
Shariq Ansari
594f7922f9 chore: Vietnamese translations 2025-07-23 10:58:19 +05:30
Shariq Ansari
7aa9f768ea chore: Chinese Simplified translations 2025-07-23 10:58:18 +05:30
Shariq Ansari
b796a30e1f chore: Turkish translations 2025-07-23 10:58:16 +05:30
Shariq Ansari
5508eb013a chore: Swedish translations 2025-07-23 10:58:14 +05:30
Shariq Ansari
a3c7af7c8c chore: Serbian (Cyrillic) translations 2025-07-23 10:58:13 +05:30
Shariq Ansari
d689bf21d2 chore: Russian translations 2025-07-23 10:58:12 +05:30
Shariq Ansari
05ea067361 chore: Portuguese translations 2025-07-23 10:58:10 +05:30
Shariq Ansari
2af8710a6f chore: Polish translations 2025-07-23 10:58:08 +05:30
Shariq Ansari
6abc9f7f9f chore: Dutch translations 2025-07-23 10:58:07 +05:30
Shariq Ansari
7de21b7015 chore: Italian translations 2025-07-23 10:58:06 +05:30
Shariq Ansari
f9bae5d8ff chore: Hungarian translations 2025-07-23 10:58:04 +05:30
Shariq Ansari
29322577aa chore: German translations 2025-07-23 10:58:03 +05:30
Shariq Ansari
a4f96c7c5b chore: Czech translations 2025-07-23 10:58:01 +05:30
Shariq Ansari
b3fe85b8a0 chore: Arabic translations 2025-07-23 10:58:00 +05:30
Shariq Ansari
efc4213363 chore: Spanish translations 2025-07-23 10:57:59 +05:30
Shariq Ansari
b983648f35 chore: French translations 2025-07-23 10:57:57 +05:30
Shariq Ansari
e62eac91d1
Merge pull request #1054 from frappe/l10n_develop3 2025-07-22 10:53:04 +05:30
Shariq Ansari
e6f1bd50db chore: Esperanto translations 2025-07-22 10:52:15 +05:30
Shariq Ansari
c5458fb29c chore: Serbian (Latin) translations 2025-07-22 10:52:14 +05:30
Shariq Ansari
8e3daf482d chore: Bosnian translations 2025-07-22 10:52:13 +05:30
Shariq Ansari
03edd1a011 chore: Croatian translations 2025-07-22 10:52:11 +05:30
Shariq Ansari
246ed83a3d chore: Thai translations 2025-07-22 10:52:10 +05:30
Shariq Ansari
53440476c4 chore: Persian translations 2025-07-22 10:52:09 +05:30
Shariq Ansari
14f3fd3cd7 chore: Indonesian translations 2025-07-22 10:52:08 +05:30
Shariq Ansari
6c9d56808b chore: Portuguese, Brazilian translations 2025-07-22 10:52:06 +05:30
Shariq Ansari
08b3107d92 chore: Vietnamese translations 2025-07-22 10:52:05 +05:30
Shariq Ansari
dc8fa27ea1 chore: Chinese Simplified translations 2025-07-22 10:52:04 +05:30
Shariq Ansari
445d7050b7 chore: Turkish translations 2025-07-22 10:52:02 +05:30
Shariq Ansari
bfe0df4df3 chore: Swedish translations 2025-07-22 10:52:01 +05:30
Shariq Ansari
71bcca71cd chore: Serbian (Cyrillic) translations 2025-07-22 10:52:00 +05:30
Shariq Ansari
53864ac12c chore: Russian translations 2025-07-22 10:51:59 +05:30
Shariq Ansari
f438500a57 chore: Portuguese translations 2025-07-22 10:51:57 +05:30
Shariq Ansari
cef8aacf2f chore: Polish translations 2025-07-22 10:51:56 +05:30
Shariq Ansari
1ab414c4bf chore: Dutch translations 2025-07-22 10:51:54 +05:30
Shariq Ansari
fdffd7bf3a chore: Italian translations 2025-07-22 10:51:53 +05:30
Shariq Ansari
6fc52aba6d chore: Hungarian translations 2025-07-22 10:51:52 +05:30
Shariq Ansari
dfd4cc068a chore: German translations 2025-07-22 10:51:51 +05:30
Shariq Ansari
5ae21c8580 chore: Czech translations 2025-07-22 10:51:49 +05:30
Shariq Ansari
178a934fa3 chore: Arabic translations 2025-07-22 10:51:48 +05:30
Shariq Ansari
1ce508a4b1 chore: Spanish translations 2025-07-22 10:51:47 +05:30
Shariq Ansari
d0678ec7c2 chore: French translations 2025-07-22 10:51:46 +05:30
Shariq Ansari
8036ffeafc
Merge pull request #1053 from shariquerik/updated-crowdin-yml 2025-07-22 10:39:58 +05:30
Shariq Ansari
a5a37c6181 fix: updated crowdin.yml 2025-07-22 10:38:28 +05:30
Shariq Ansari
945cdc0ae3
Merge pull request #1050 from frappe/pot_develop_2025-07-20 2025-07-22 10:22:17 +05:30
frappe-pr-bot
8de2e89b68 chore: update POT file 2025-07-20 09:37:27 +00:00
Shariq Ansari
0f49470bf6
Merge pull request #1041 from shariquerik/dashboard-fix-1 2025-07-15 15:11:01 +05:30
Shariq Ansari
7826565ce7 fix: reset to default 2025-07-15 15:09:48 +05:30
Shariq Ansari
713571469b fix: only show edit button to system manager 2025-07-15 14:55:10 +05:30
Shariq Ansari
2f34fdd409 fix: set status correctly 2025-07-15 14:52:19 +05:30
Shariq Ansari
6fefa16ac3
Merge pull request #1033 from shariquerik/editable-dashboard 2025-07-15 13:28:50 +05:30
Shariq Ansari
b344f412c9 fix: forecasting chart is breaking if no data 2025-07-15 13:21:46 +05:30
Shariq Ansari
1e245e7719
Merge pull request #1038 from shariquerik/to-status-type-1 2025-07-15 12:39:07 +05:30
Shariq Ansari
28facd66c4 fix: to status type is not accessible 2025-07-15 12:37:44 +05:30
Shariq Ansari
c8f01f08ed fix: updated default manager dashboard 2025-07-15 12:33:54 +05:30
Shariq Ansari
a89525f77e
Merge pull request #1036 from shariquerik/to-status-type 2025-07-15 12:18:21 +05:30
Shariq Ansari
38a2fa87c3 fix: to status type is not accessible 2025-07-15 12:17:00 +05:30
Shariq Ansari
46e6ed2e6f fix: minor fix 2025-07-15 12:11:08 +05:30
Shariq Ansari
266952c404 fix: decreased rowHeight to 42 2025-07-15 12:07:19 +05:30
Shariq Ansari
b77e59589a build(deps): bump frappeui to 0.1.171 2025-07-15 12:06:41 +05:30
Shariq Ansari
1a5ae397dc
Merge pull request #1034 from shariquerik/exchange_rate_org 2025-07-15 11:51:21 +05:30
Shariq Ansari
7c4718ad02 fix: removed currency exchange and added exchange rate field 2025-07-15 11:41:02 +05:30
Shariq Ansari
d79341b6d9 fix: renamed blank card to spacer 2025-07-14 20:29:46 +05:30
Shariq Ansari
84738ba00c fix: allow force reset manager dashboard 2025-07-14 20:22:43 +05:30
Shariq Ansari
3b0a8d8e4b fix: updated default manager dashboard 2025-07-14 20:16:06 +05:30
Shariq Ansari
2584cca128 fix: minor fixes 2025-07-14 20:12:52 +05:30
Shariq Ansari
05b8cea206 fix: create default manager dashboard if not exists 2025-07-14 19:14:18 +05:30
Shariq Ansari
6e3d23a8e1 fix: add default Manager Dashboard on install 2025-07-14 18:28:51 +05:30
Shariq Ansari
2a38d0fb5f fix: disabel save button if not dirty, reset to old items if cancel 2025-07-14 18:15:04 +05:30
Shariq Ansari
97724c776b build(deps): bump frappeui to 0.1.170 2025-07-14 17:58:01 +05:30
Shariq Ansari
9b072058cc build(deps): bump frappeui to 0.1.170 2025-07-14 17:57:43 +05:30
Shariq Ansari
d2e65feaa6 fix: save edited dashboard 2025-07-14 17:41:00 +05:30
Shariq Ansari
37c2d3a2b0 feat: allow adding existing charts 2025-07-14 17:40:13 +05:30
Shariq Ansari
0909423fe9 fix: rename card to chart 2025-07-14 17:38:17 +05:30
Shariq Ansari
139bcb101c fix: added remove chart button 2025-07-14 17:37:36 +05:30
Shariq Ansari
0f06715d0c fix: use tooltip from data 2025-07-14 17:36:50 +05:30
Shariq Ansari
4a783fcba8 fix: show edit button to manager 2025-07-14 14:22:53 +05:30
Shariq Ansari
fd38f0ac98 fix: load dashboard layout from CRM Dashboard doctype and render 2025-07-14 14:16:10 +05:30
Shariq Ansari
ed2208fe75 fix: added crm dashboard doctype to store dashboard layout 2025-07-14 14:13:50 +05:30
Shariq Ansari
4320142132 fix: update status type patch was not working 2025-07-14 12:27:14 +05:30
Shariq Ansari
160649bf97 fix: use GridLayout from frappe-ui to display dashboard 2025-07-14 12:18:14 +05:30
Shariq Ansari
e7a2efd14a
Merge pull request #1029 from frappe/pot_develop_2025-07-13 2025-07-13 15:21:23 +05:30
Shariq Ansari
81614418d4
Merge pull request #1030 from shariquerik/deal-status-type 2025-07-13 15:15:19 +05:30
frappe-pr-bot
bb08f3d377 chore: update POT file 2025-07-13 09:38:02 +00:00
Shariq Ansari
5232da6ec3 fix: updated funnel query to get status change count 2025-07-13 13:07:31 +05:30
Shariq Ansari
e59547da30 fix: store deal status type in status log 2025-07-13 12:43:03 +05:30
Shariq Ansari
de85ccfc51 fix: added deals by ongoing & won stages bar 2025-07-13 12:35:16 +05:30
Shariq Ansari
f82019e510 fix: used closed_date instead of closed_on and set closed_date if status type is Won 2025-07-13 12:11:50 +05:30
Shariq Ansari
7fc26a5202 fix: used expected closed date & deal value for forecasting 2025-07-13 12:09:35 +05:30
Shariq Ansari
dcb1e47564 fix: added closed_date removed closed_on & added expected_deal_value & expected_closure_date field 2025-07-13 12:02:34 +05:30
Shariq Ansari
61259f3d2e fix: avg time to close a deal number card 2025-07-13 11:52:04 +05:30
Shariq Ansari
2dd2608c09 fix: added two more number cards 2025-07-13 11:35:14 +05:30
Shariq Ansari
81dc4e1138 fix: get ongoing deals and won deals based on closed_on date 2025-07-11 17:09:21 +05:30
Shariq Ansari
cb1f9f760c fix: use status.type instead of status in all query 2025-07-11 17:00:59 +05:30
Shariq Ansari
51530b7608 fix: show lost reason modal if status of type Lost is set 2025-07-11 16:35:47 +05:30
Shariq Ansari
4e6d4a1d77 patch: added patch to update deal status type 2025-07-11 16:16:40 +05:30
Shariq Ansari
efc5dd93e9 fix: added type in default deak status while installing 2025-07-11 16:07:52 +05:30
Shariq Ansari
210a9d8d06 fix: added type of deal status field 2025-07-11 16:05:57 +05:30
Shariq Ansari
9bd855ee2e
Merge pull request #1024 from shariquerik/save-lead 2025-07-10 18:33:18 +05:30
Shariq Ansari
970c215f40 fix: cannot save data fields in lead page 2025-07-10 18:27:01 +05:30
Shariq Ansari
7d157046ac
Merge pull request #1022 from shariquerik/dashboard-fix 2025-07-10 17:39:14 +05:30
Shariq Ansari
1ae7018f79 fix: apply user filter if sales user 2025-07-10 17:38:36 +05:30
Shariq Ansari
6802567291
Merge pull request #979 from nextchamp-saqib/dashboard 2025-07-10 17:21:58 +05:30
Shariq Ansari
cbc127e947 fix: better description 2025-07-10 17:20:25 +05:30
Shariq Ansari
d91d4765b5 fix: make autocomplete non clickable if disabled 2025-07-10 17:06:58 +05:30
Shariq Ansari
328959cc39 style: better spacing 2025-07-10 16:59:09 +05:30
Shariq Ansari
36320f61ab fix: remove mandatory from currency field in crm settings 2025-07-10 16:36:02 +05:30
Shariq Ansari
faeacb9a7d chore: minor fix 2025-07-10 16:27:00 +05:30
Shariq Ansari
5dcd416007 fix: added forecasting & currency setting in general settings 2025-07-10 16:15:27 +05:30
Shariq Ansari
33e4072430 fix: added disabled & placement prop in Link component 2025-07-10 16:14:32 +05:30
Shariq Ansari
c7fbd6f8f1 refactor: general settings 2025-07-10 15:27:55 +05:30
Shariq Ansari
a5d3694386 refactor: get exchange rate api 2025-07-10 15:23:59 +05:30
Shariq Ansari
b3075416e2 fix: show dashboard to manager only 2025-07-10 13:59:30 +05:30
Shariq Ansari
743d97d690
Merge pull request #1018 from zaqouttahir/fix-bug-edit-bulk 2025-07-10 12:29:07 +05:30
Shariq Ansari
2cb09dde4b fix: use exchange rate in deal to calculate the deal value 2025-07-09 18:22:52 +05:30
Shariq Ansari
d7ba5a5f62 fix: store current day exchange rate when currency is updated in deal & organization 2025-07-09 18:22:13 +05:30
Shariq Ansari
a00bba35f8 fix: remove crm currency exchange doctype 2025-07-09 18:20:49 +05:30
Shariq Ansari
71db65d21c fix: make currency read only once set 2025-07-09 17:49:44 +05:30
Shariq Ansari
37d820a67c chore: updated/added number card tooltip 2025-07-09 17:47:56 +05:30
Shariq Ansari
4f02f0a4d7 fix: convert to system currency and show deal value 2025-07-09 15:53:06 +05:30
Shariq Ansari
f4b81b3761 fix: added date field in currency exchange doctype 2025-07-09 14:48:49 +05:30
Shariq Ansari
0be737914a fix: store currency exchange in deal & organization 2025-07-09 14:38:56 +05:30
Shariq Ansari
1b0d966db0 fix: created currency exchange doctype 2025-07-09 14:37:16 +05:30
Shariq Ansari
27f87883f7 fix: fieldtype of value is not changing based on selected field's fieldtype 2025-07-08 19:16:58 +05:30
Shariq Ansari
f747e076ab fix: only show sales user filter to manager 2025-07-08 12:58:46 +05:30
Shariq Ansari
4b12918ba5 fix: added filters and translated titles 2025-07-08 12:29:20 +05:30
zaqouttahir
c104b1b8b4 fix: set fieldname to handle edit value modal 2025-07-07 13:13:04 +03:00
Shariq Ansari
9d4106cd81 chore: cleanup 2025-07-07 14:33:11 +05:30
Shariq Ansari
eddf8c9295 fix: show avg time number card based on closed_on date 2025-07-07 14:30:29 +05:30
Shariq Ansari
6b3e42a44e
Merge pull request #1015 from frappe/pot_develop_2025-07-06 2025-07-07 14:16:21 +05:30
Shariq Ansari
9b1d4832b6
Merge branch 'develop' into dashboard 2025-07-06 15:25:40 +05:30
Shariq Ansari
4d2f054e40 fix: added avg time to close number card 2025-07-06 15:24:43 +05:30
Shariq Ansari
6450b69ae7 fix: capture closed on datetime when deal marked as Won 2025-07-06 15:24:23 +05:30
frappe-pr-bot
223187c7ea chore: update POT file 2025-07-06 09:36:54 +00:00
Shariq Ansari
3b34f73cb3 fix: added more charts for dashboard 2025-07-06 14:44:12 +05:30
Shariq Ansari
40c5c92230
Merge pull request #1013 from shariquerik/primary-mobile-no 2025-07-05 14:33:10 +05:30
Shariq Ansari
6760798f18 fix: useDocument in organization page 2025-07-05 14:17:26 +05:30
Shariq Ansari
42ea1ad16e fix: useDocument in contact page 2025-07-05 14:09:37 +05:30
Shariq Ansari
96200aebe6 fix: update primary mobile_no & email in deal if contact is updated 2025-07-05 13:21:21 +05:30
Shariq Ansari
bcfe4b6a49 fix: made mobile_no, email & phone readonly since it captures primary contacts data 2025-07-05 12:30:40 +05:30
Shariq Ansari
cb92e5e68d
Merge branch 'develop' into dashboard 2025-07-03 14:28:28 +05:30
Shariq Ansari
1fa6b5bb51 fix: added first cut queries for some charts and number cards 2025-07-03 14:27:21 +05:30
Shariq Ansari
cafc4fb22f
Merge pull request #1009 from shariquerik/products-table-fix 2025-07-02 17:50:58 +05:30
Shariq Ansari
39eb5600d9 fix: grid field is not getting set 2025-07-02 17:45:46 +05:30
Shariq Ansari
0b97462dc9
Merge pull request #1007 from shariquerik/lost-reason-fix 2025-07-02 17:26:54 +05:30
Shariq Ansari
cab80edf60 fix: check reason.reason not reason 2025-07-02 17:15:59 +05:30
Shariq Ansari
6f3b58d1a5
Merge pull request #1005 from shariquerik/frappeui-update 2025-07-02 17:09:33 +05:30
Shariq Ansari
fc89c7b93c build(deps): bump frappeui to 0.1.166 2025-07-02 17:03:28 +05:30
Shariq Ansari
4a57c4eb84
Merge pull request #1003 from shariquerik/default-lost-reason 2025-07-02 16:59:23 +05:30
Shariq Ansari
96cbdea820 fix: add default lost reason on install 2025-07-02 16:57:26 +05:30
Shariq Ansari
a3a54aef94
Merge pull request #984 from shariquerik/lost-reasons 2025-07-02 16:03:30 +05:30
Shariq Ansari
c96e5ff6c5 fix: update default probability from deal status 2025-07-02 15:58:12 +05:30
Shariq Ansari
144470877d fix: allow creating lost reason from lost reason modal field 2025-07-02 15:42:25 +05:30
Shariq Ansari
391844512a fix: intercept data tab's before save and side panel's before field change to show lost reason modal 2025-07-02 15:28:21 +05:30
Shariq Ansari
d89c304b13 fix: show lost reason modal if status changed to lost 2025-07-02 14:11:11 +05:30
Shariq Ansari
881126c7f1 refactor: statusOptions code 2025-07-02 14:10:14 +05:30
Shariq Ansari
5bbec00803 fix: renamed other_lost_reason to lost_notes 2025-07-02 13:59:34 +05:30
Shariq Ansari
7730e46cfc fix: removed unused triggerOnChange 2025-07-02 13:58:51 +05:30
Shariq Ansari
97b2253e9d fix: made lost notes as text and non mandatory if lost reason is not Other 2025-07-02 11:10:53 +05:30
Shariq Ansari
1afb2a783b
Merge branch 'develop' into lost-reasons 2025-07-01 19:34:35 +05:30
Shariq Ansari
0fdbfa3ad4
Merge pull request #998 from shariquerik/prettydate-fix 2025-07-01 19:23:24 +05:30
Shariq Ansari
a7dc5e05b3 fix: show absolute day count not in decimels 2025-07-01 19:20:04 +05:30
Shariq Ansari
92d7280728 chore: resolved conflict 2025-07-01 16:53:40 +05:30
Shariq Ansari
5d01b88a1e feat: created lost reason doctype 2025-07-01 16:51:01 +05:30
Shariq Ansari
2b47e3f4c9
Merge pull request #994 from shariquerik/forecasting-fix-1 2025-07-01 16:49:48 +05:30
Shariq Ansari
485360f291 fix: show forcasted sales section in sidepanel if forecasting is enabled 2025-07-01 16:37:04 +05:30
Shariq Ansari
17fdbb05ce fix: add mandatory fields in convert to deal modal if not added 2025-07-01 16:05:08 +05:30
Shariq Ansari
adc22efcb1 fix: show error message on convert to deal modal 2025-07-01 16:04:40 +05:30
Shariq Ansari
4c70b1a06b fix: mandatory error 2025-07-01 15:06:02 +05:30
Shariq Ansari
4f58aa110a fix: made deal value mandatory if forecasting is enabled 2025-07-01 15:05:37 +05:30
Shariq Ansari
4d3fe722e8 fix: added default probability to Lost status 2025-07-01 13:16:40 +05:30
Shariq Ansari
6320e580ae refactor: moved convert to deal modal into separate component 2025-07-01 13:07:36 +05:30
Shariq Ansari
611f4cde70 fix: prettyDate is not accurate 2025-07-01 12:58:57 +05:30
Shariq Ansari
6d3268a61e fix: add default probabilities in deal status 2025-07-01 12:07:44 +05:30
Shariq Ansari
bf0a1ecebd
Merge pull request #991 from shariquerik/edit-call-log 2025-07-01 11:52:18 +05:30
Shariq Ansari
693c086930 fix: show edit call log button in call log details modal 2025-07-01 11:49:42 +05:30
Shariq Ansari
7c307a9134
Merge pull request #988 from shariquerik/call-log-on-before-create 2025-06-30 20:06:01 +05:30
Shariq Ansari
aae7e0e36c fix: pass reference doc to call log modal to get reference doc in on before create 2025-06-30 20:00:05 +05:30
Shariq Ansari
2014a3d6de
Merge pull request #985 from shariquerik/on-before-create 2025-06-30 19:41:35 +05:30
Shariq Ansari
2e5c1bc3b5 fix: added on before create hook in all modals 2025-06-30 19:16:24 +05:30
Shariq Ansari
ac13b7a3bd fix: added on before create hook in call log modal 2025-06-30 18:43:19 +05:30
Shariq Ansari
6b7bdf5afb feat: added on before create hook in document.js 2025-06-30 18:42:20 +05:30
Shariq Ansari
3eba628a8b
Merge branch 'develop' into dashboard 2025-06-30 12:57:13 +05:30
Shariq Ansari
9949478b36 fix: added breadcrumb and made header sticky 2025-06-30 12:56:47 +05:30
Shariq Ansari
ff657ec34c
Merge pull request #980 from frappe/pot_develop_2025-06-29 2025-06-30 12:42:55 +05:30
Shariq Ansari
da4698d431
Merge branch 'develop' into pot_develop_2025-06-29 2025-06-30 12:42:12 +05:30
Shariq Ansari
20d47ae323
Merge pull request #978 from shariquerik/dynamic-app-alias 2025-06-30 11:59:29 +05:30
Shariq Ansari
f4f799f636 feat: create dynamic alias to use components from frontend vue apps 2025-06-30 11:49:31 +05:30
frappe-pr-bot
cc411f036d chore: update POT file 2025-06-29 09:36:46 +00:00
Saqib Ansari
62d5c2a91f feat: initialize dashboard boilerplate 2025-06-28 10:57:58 +05:30
Shariq Ansari
8350c5ee36
Merge pull request #971 from shariquerik/email-template-settings 2025-06-26 17:54:04 +05:30
Shariq Ansari
65435cf2b5 fix: delete icon issue & more cleanup 2025-06-26 17:49:10 +05:30
Shariq Ansari
af4c64e633 build(deps): bump frappeui to 0.1.162 2025-06-26 17:11:37 +05:30
Shariq Ansari
41b913debe fix: cannot change role of user with Admin access 2025-06-26 17:07:26 +05:30
Shariq Ansari
a3b9368953 fix: give Sales Manager & Sales User role if System Manager access is given 2025-06-26 16:54:33 +05:30
Shariq Ansari
28ece820ed style: better spacing 2025-06-26 16:46:23 +05:30
Shariq Ansari
cca420b1a0 style: minor changes 2025-06-25 21:19:49 +05:30
Shariq Ansari
05803c79b4 fix: make email template row clickable 2025-06-25 17:02:41 +05:30
Shariq Ansari
5932ccafec fix: add new email template from email selector modal 2025-06-25 15:23:30 +05:30
Shariq Ansari
7cee017e20 fix: removed email template page and related components 2025-06-25 15:15:34 +05:30
Shariq Ansari
b15a8d9c8a fix: added email template icon 2025-06-25 15:03:32 +05:30
Shariq Ansari
7e6d5c3e54 fix: Duplicate email template 2025-06-24 19:46:27 +05:30
Shariq Ansari
dd3d297dab fix: Edit email template 2025-06-24 19:37:13 +05:30
Shariq Ansari
e4f728d809 fix: Create email template 2025-06-24 18:52:36 +05:30
Shariq Ansari
cd7bab9184 feat: Create/Edit & List page for email template & implemented delete from list 2025-06-24 18:52:00 +05:30
Shariq Ansari
ec6b1558b1 fix: only show search if users are more than 10 2025-06-24 15:36:54 +05:30
Shariq Ansari
1c3ee8b557 refactor: added email templates in settings modal 2025-06-24 15:35:48 +05:30
Shariq Ansari
1db7f69f89
Merge pull request #960 from shariquerik/users-fix-1 2025-06-24 12:12:20 +05:30
Shariq Ansari
3c1ce1fe27 fix: make header sticky 2025-06-24 12:11:39 +05:30
Shariq Ansari
2d05b6a282
Merge pull request #957 from shariquerik/users-fix 2025-06-24 11:45:46 +05:30
Shariq Ansari
b5ed9692df fix: user with System Manager role is admin 2025-06-24 11:44:13 +05:30
Shariq Ansari
9a326d791b
Merge pull request #953 from shariquerik/users 2025-06-23 21:00:32 +05:30
Shariq Ansari
7fbd240d97 fix: added search in users page 2025-06-23 20:55:35 +05:30
Shariq Ansari
58d4691354
Merge pull request #846 from pratikb64/delete-from-record-view 2025-06-23 13:50:19 +05:30
Pratik
594295b7c8 style: switch button position 2025-06-23 08:13:06 +00:00
Shariq Ansari
2c45673f54
Merge pull request #845 from shariquerik/agents 2025-06-23 13:22:56 +05:30
Shariq Ansari
7827afe606
Merge pull request #946 from kalungia/dev 2025-06-23 13:21:59 +05:30
Shariq Ansari
bc7498e02b
Merge branch 'develop' into dev 2025-06-23 13:20:01 +05:30
Shariq Ansari
e5dd85aefb
Merge pull request #948 from frappe/pot_develop_2025-06-22 2025-06-23 13:19:09 +05:30
Shariq Ansari
cf1fce3dc0 fix: renaming fix and removed CRM User code 2025-06-23 13:18:16 +05:30
Pratik
480cc07cd9 refactor: remove unnecessary functions & components 2025-06-23 07:35:15 +00:00
Shariq Ansari
84d4327e80 fix: use change password modal in place 2025-06-23 13:04:12 +05:30
Pratik
34102ef6ef refactor: change labels & function names 2025-06-23 05:15:40 +00:00
Pratik
ca985a0b76 Merge branch 'develop' of https://github.com/pratikb64/crm into delete-from-record-view 2025-06-23 04:26:50 +00:00
frappe-pr-bot
4b4a154261 chore: update POT file 2025-06-22 09:36:52 +00:00
Shariq Ansari
e957327877 fix: use crmUsers in all link field 2025-06-20 18:57:09 +05:30
Shariq Ansari
2fdea90ad4 fix: loading state in Users page 2025-06-20 18:55:42 +05:30
Shariq Ansari
ad1aee9c9e fix: add user is actually add role 2025-06-20 18:55:14 +05:30
Shariq Ansari
bd7451e86f fix: if role is set to sales user then remove modules and set FCRM 2025-06-20 17:56:14 +05:30
Shariq Ansari
0230360145 fix: use text-ink-gray-8 instead of 9 2025-06-20 17:47:07 +05:30
Shariq Ansari
eee1190f10 fix: dark mode fixes for email account setting 2025-06-20 17:42:10 +05:30
Shariq Ansari
96c8aae01e chore: fixed warning 2025-06-20 17:25:14 +05:30
Shariq Ansari
0ad65be961 fix: layout change 2025-06-20 17:20:12 +05:30
Shariq Ansari
0f8d484e28 fix: existingEmail is a list 2025-06-20 17:18:36 +05:30
Shariq Ansari
364c369199 fix: updated users page to update user directly and removed unnecessary code 2025-06-20 16:46:41 +05:30
Shariq Ansari
901bcb8460 fix: removed CRM User doctype and moved api's to user.py 2025-06-20 16:45:48 +05:30
Shariq Ansari
d06ac91052 fix: use multi select user input and show already exist error if user with email exist or invited 2025-06-20 16:43:48 +05:30
Shariq Ansari
f818a4c1d6 fix: created multi select user input 2025-06-20 16:42:45 +05:30
Shariq Ansari
85191e10c8 fix: use crmUsers in comment box 2025-06-20 16:41:08 +05:30
Shariq Ansari
001a3231e1 fix: get users and crm users 2025-06-20 15:50:47 +05:30
Shariq Ansari
d951dff5a9 fix: added tooltip with shortcut 2025-06-19 15:08:53 +05:30
Shariq Ansari
dc82f837aa fix: moved change password modal in global modals 2025-06-19 15:07:57 +05:30
Shariq Ansari
7f1db0b444 fix: change password validation messsage 2025-06-19 12:56:50 +05:30
Abraham Kalungi
a317950567 fix: prevent TypeError when concatenating first and last name in WhatsApp messages
The last name on CRM Leads can be empty. In cases where it is, an error occurs: can only concatenate str (not "NoneType") to str. This prevents retrieving messages until a last name is added.
2025-06-18 15:31:00 +02:00
Shariq Ansari
4c7269e357 fix: updated components.d.ts 2025-06-17 23:46:53 +05:30
Shariq Ansari
15fd763de8 fix: use validateIsImageFile from utils 2025-06-17 23:46:28 +05:30
Shariq Ansari
0c314674fc
Merge branch 'develop' into agents 2025-06-17 23:42:42 +05:30
Shariq Ansari
efd03141f0
chore: resolved conflict 2025-06-17 23:42:20 +05:30
Shariq Ansari
675bcb549d
Discard changes to frontend/src/components/Settings/ProfileImageEditor.vue 2025-06-17 23:40:58 +05:30
Shariq Ansari
56425254a9 fix: updated components.d.ts 2025-06-17 23:35:51 +05:30
Shariq Ansari
22856351fd fix: only show users to manager 2025-06-17 23:35:51 +05:30
Shariq Ansari
dd1229309f fix: updated components.d.ts 2025-06-17 23:35:51 +05:30
Shariq Ansari
fad7c5985c refactor: profile page 2025-06-17 23:34:45 +05:30
Shariq Ansari
3234102e55 fix: capture onboarding step event of setting up password 2025-06-17 23:34:45 +05:30
Shariq Ansari
fb2f105520 feat: update password modal 2025-06-17 23:34:45 +05:30
Shariq Ansari
03abe0b5cd fix: create crm user on accepting invite 2025-06-17 23:34:44 +05:30
Shariq Ansari
6873c6db4e feat: add existing users 2025-06-17 23:34:44 +05:30
Shariq Ansari
08bab927a2 fix: filter out existing emails 2025-06-17 23:34:44 +05:30
Shariq Ansari
d244567b30 fix: open invite user page from users add new agent dropdown option 2025-06-17 23:34:44 +05:30
Shariq Ansari
2b1b21d2e2 fix: show role options based on logged in user's role 2025-06-17 23:34:44 +05:30
Shariq Ansari
12213de478 fix: show role options based on logged in user's role 2025-06-17 23:34:44 +05:30
Shariq Ansari
2a2c832e0b fix: updated roles in Invite user page 2025-06-17 23:34:44 +05:30
Shariq Ansari
b534aae70b fix: renamed component names from Agent to User 2025-06-17 23:34:44 +05:30
Shariq Ansari
6d3e4406ae fix: renamed & added role with filter 2025-06-17 23:34:44 +05:30
Shariq Ansari
463d60b650 fix: renamed Agent to User in Settings 2025-06-17 23:34:44 +05:30
Shariq Ansari
e9812495e9 fix: update agent fields based on user 2025-06-17 23:34:44 +05:30
Shariq Ansari
0a836c78bb fix: activate/deactivate agent 2025-06-17 23:34:44 +05:30
Shariq Ansari
5c7f835e4c fix: renamed invite members to invite agent 2025-06-17 23:34:44 +05:30
Shariq Ansari
346849631e fix: show and allow changing role from agents settings page 2025-06-17 23:34:44 +05:30
Shariq Ansari
bf166bdaad fix: added get user role in users store 2025-06-17 23:34:44 +05:30
Shariq Ansari
123f183f68 fix: removed x button from settings modal 2025-06-17 23:34:44 +05:30
Shariq Ansari
bea1505c63 fix: added Agents page in settings 2025-06-17 23:34:44 +05:30
Shariq Ansari
fac5ed5579 feat: added crm agent doctype 2025-06-17 23:34:44 +05:30
Shariq Ansari
96fefbd8a3
Merge pull request #937 from shariquerik/forecasting-fix 2025-06-16 17:48:08 +05:30
Shariq Ansari
548018997e build(deps): bump frappeui to 0.1.156 2025-06-16 17:42:28 +05:30
Shariq Ansari
9f3477e1cd fix: updated button with icon 2025-06-16 17:42:01 +05:30
Shariq Ansari
baa03246e6 fix: fixed breaking button with icon and open email box 2025-06-16 17:06:06 +05:30
Shariq Ansari
a17b1cd0e2 fix: added mandatory field error toast 2025-06-16 16:32:15 +05:30
Shariq Ansari
49d82870c4 fix: hide calendar icon in side panel date fields 2025-06-16 13:41:11 +05:30
Shariq Ansari
6a72a4467a build(deps): bump frappeui to 0.1.154 2025-06-16 13:40:53 +05:30
Shariq Ansari
efbed6e0b6 fix: renamed Expected Closure Date to Close Date 2025-06-16 11:44:08 +05:30
Shariq Ansari
5270670b65
Merge pull request #933 from frappe/pot_develop_2025-06-15 2025-06-16 11:39:47 +05:30
frappe-pr-bot
b82f4ca02b chore: update POT file 2025-06-15 09:36:57 +00:00
Pratik Badhe
0f451c7e3a
Merge pull request #930 from pratikb64/git-error 2025-06-13 18:26:31 +05:30
Pratik
824dc8dcdd fix: git command error 2025-06-13 12:51:04 +00:00
Shariq Ansari
20405be86c
Merge pull request #920 from shariquerik/forecasting 2025-06-13 15:04:23 +05:30
Shariq Ansari
4edfa951dc fix: asterisk is not visible if label is big enough 2025-06-13 15:02:50 +05:30
Shariq Ansari
029c16d1d0 fix: circular import 2025-06-13 14:46:44 +05:30
Shariq Ansari
b3acff8cba fix: create standard forecasting script on install and added in patch 2025-06-13 14:42:09 +05:30
Shariq Ansari
a98b0e3a00 fix: set close date to now if status is Won 2025-06-13 14:20:50 +05:30
Shariq Ansari
b1cbcbd98d
Merge pull request #923 from shariquerik/lead-org-avatar-fix 2025-06-12 18:09:41 +05:30
Shariq Ansari
e079980598 fix: removed leads organization logo 2025-06-12 18:07:07 +05:30
Shariq Ansari
2e27c0459c fix: set close date reqd as 1 or 0 based on enabled_forecasting 2025-06-11 19:30:08 +05:30
Shariq Ansari
d87a237789 feat: added enable forecasting settings 2025-06-11 19:03:22 +05:30
Shariq Ansari
e9e0aa357b fix: trigger on change on status change 2025-06-11 14:20:11 +05:30
Shariq Ansari
9af300bba8 fix: added throwError global method 2025-06-11 14:19:16 +05:30
Shariq Ansari
7d79cbf5bd fix: update and reset value in triggerOnChange method 2025-06-11 14:18:25 +05:30
Shariq Ansari
fdca27bb81 fix: added probability field in deal status 2025-06-11 13:01:22 +05:30
Shariq Ansari
01f0213693 fix: added deal value field 2025-06-11 13:00:02 +05:30
Shariq Ansari
5d29a49120
Merge pull request #916 from mahsem/develop 2025-06-11 11:13:25 +05:30
mahsem
33e6b80d5a
fix: add context for Integrations 2025-06-09 22:11:22 +02:00
Shariq Ansari
100d931535
Merge pull request #913 from shariquerik/future-date-time-ago 2025-06-09 16:51:02 +05:30
Shariq Ansari
dba6dd1983 fix: future date is not captured in pretty date 2025-06-09 16:49:09 +05:30
Shariq Ansari
dc8898e1da
Merge pull request #907 from frappe/pot_develop_2025-06-08 2025-06-09 14:09:56 +05:30
frappe-pr-bot
ed79bf55eb chore: update POT file 2025-06-08 09:36:10 +00:00
Shariq Ansari
fb644f5fbe
Merge pull request #905 from NagariaHussain/fix/image-upload
refactor: DRY up validate image file
2025-06-08 12:49:03 +05:30
Hussain Nagaria
ab409dfd2c fix: yet another unused import due to merge conflict 2025-06-08 12:37:15 +05:30
Hussain Nagaria
42285dd911 fix: unused import due to merge conflict 2025-06-08 12:36:39 +05:30
Md Hussain Nagaria
db2a6c65b7
Merge branch 'develop' into fix/image-upload 2025-06-08 12:34:30 +05:30
Hussain Nagaria
c6ad10857a refactor: DRY up validate image file
* Also, allows more types of image files
2025-06-08 12:30:51 +05:30
Shariq Ansari
f128a55f97
Merge pull request #902 from shariquerik/onload-onsave 2025-06-06 21:15:42 +05:30
Shariq Ansari
2a817e5861 fix: only load assignee if docname is passed 2025-06-06 21:14:20 +05:30
Shariq Ansari
5e616f1a50 refactor: statusOptions code 2025-06-06 21:09:27 +05:30
Shariq Ansari
c6e9d71e1f fix: allow snake & camel case for on load, on save, convert to deal, on create lead 2025-06-06 21:01:20 +05:30
Shariq Ansari
f58d44bf9c fix: use document.doc in status dropdown 2025-06-06 20:49:58 +05:30
Shariq Ansari
f72ab39c93 fix: onLoad & onSave 2025-06-06 20:35:25 +05:30
Shariq Ansari
6c706e6162
Merge pull request #899 from shariquerik/minor-fix 2025-06-06 17:51:43 +05:30
Shariq Ansari
8f81d207b8 fix: activity is not loading 2025-06-06 17:50:48 +05:30
Shariq Ansari
b74c5f384d
Merge pull request #896 from shariquerik/custom-actions 2025-06-06 17:24:13 +05:30
Shariq Ansari
df412d51fe fix: add custom actions using class based script in mobile view 2025-06-06 17:19:27 +05:30
Shariq Ansari
8942bb7e48 feat: add custom statuses using class based script 2025-06-06 17:17:11 +05:30
Shariq Ansari
ca60679126 feat: add custom actions using class based script 2025-06-06 16:38:39 +05:30
Shariq Ansari
8db846ad5d fix: trigger onload method if controller is loaded 2025-06-06 16:37:55 +05:30
Shariq Ansari
bb6a90058b
Merge pull request #894 from shariquerik/refactor-assignees 2025-06-06 14:58:47 +05:30
Shariq Ansari
44df09fac2 fix: removed setuAssignees code 2025-06-06 14:06:02 +05:30
Shariq Ansari
e214ce8bfb refactor: render assignees from document.js
reload assignees if lead_owner/deal_owner is changed
2025-06-05 18:55:28 +05:30
Shariq Ansari
6d281922e4 fix: load assignees in document.js 2025-06-05 18:53:57 +05:30
Shariq Ansari
58f09331b0 fix: remove multiple assignees not working 2025-06-05 18:52:39 +05:30
Shariq Ansari
9780a6b63e
Merge pull request #891 from shariquerik/fixes-2 2025-06-05 16:14:38 +05:30
Shariq Ansari
71f764c224 refactor: set default values for new lead, deal and contact 2025-06-05 16:08:51 +05:30
Shariq Ansari
9362997246 refactor: organization modal code refactor 2025-06-05 16:08:13 +05:30
Shariq Ansari
28ea88f61e refactor: call log modal code refactor 2025-06-05 16:07:40 +05:30
Shariq Ansari
a25ff14dd4 fix: activity is not loading 2025-06-05 15:31:36 +05:30
Shariq Ansari
d86caee7af
Merge pull request #887 from shariquerik/new-doc-issue 2025-06-05 13:56:57 +05:30
Shariq Ansari
c4caabe722 fix: moved address modal to global modals and control it using modals.js 2025-06-04 19:15:12 +05:30
Shariq Ansari
8dcb77634b fix: moved quick entry modal related logic to modals.js & GlobalModals for all pages 2025-06-04 19:14:15 +05:30
Shariq Ansari
c4feed116d fix: handle new document for lead/deal/contact/organization 2025-06-04 19:00:57 +05:30
Shariq Ansari
e220767179 fix: global modals not working in mobile view 2025-06-04 18:44:51 +05:30
Shariq Ansari
571126c36d fix: moved modal related code to modal.js 2025-06-04 18:34:19 +05:30
Shariq Ansari
832323f25e fix: handle new document for call log 2025-06-04 12:50:19 +05:30
Shariq Ansari
3b73432d8c fix: handle controllers for new document 2025-06-04 12:49:14 +05:30
Shariq Ansari
3aa341370b fix: await scripts.list.promise 2025-06-04 12:48:20 +05:30
Ankush Menat
895da1a812
fix: remove invasive settings (#884) 2025-06-02 19:03:54 +05:30
Shariq Ansari
d34ee6fe48
Merge pull request #875 from Ocheretovich/patch-2 2025-06-02 12:37:56 +05:30
Shariq Ansari
7298fe378c
Merge pull request #878 from frappe/pot_develop_2025-06-01 2025-06-02 12:37:01 +05:30
Shariq Ansari
2da0b48c29
Merge pull request #874 from Ocheretovich/patch-1 2025-06-02 12:36:01 +05:30
frappe-pr-bot
165509f5a0 chore: update POT file 2025-06-01 09:36:30 +00:00
Ocheretovich
c9b9dbb092
Update README.md 2025-05-29 13:06:52 +03:00
Ocheretovich
0cc1d5da8f
Update README.md 2025-05-29 13:03:21 +03:00
Pratik
c70dced268 refactor: internationalization & code clean up 2025-05-29 06:02:35 +00:00
Pratik
df698387dc Merge remote-tracking branch 'origin/develop' into delete-from-record-view 2025-05-29 05:54:17 +00:00
Pratik
716dc056d6 refactor: move delete button 2025-05-29 05:52:36 +00:00
Pratik Badhe
cf91f3f72a
Merge pull request #872 from pratikb64/export-filter-fix 2025-05-28 17:18:19 +05:30
Pratik
51b87d0ac6 fix: export with filter 2025-05-28 11:09:22 +00:00
Shariq Ansari
c83d7adddd
Merge pull request #868 from shariquerik/prettydate 2025-05-28 13:58:41 +05:30
Shariq Ansari
549665bc61 fix: use prettydate method instead of useTimeAgo 2025-05-28 13:50:01 +05:30
Pratik
7f5f43f0c2 style: fix dark mode styles 2025-05-27 05:22:30 +00:00
Pratik
af41469d58 feat: add list view & handle bulk delete, unlink 2025-05-26 15:24:24 +00:00
Shariq Ansari
43e1309bd8
Merge pull request #865 from shariquerik/form-script-fix 2025-05-26 17:56:12 +05:30
Shariq Ansari
91f7cf05fc fix: handle script load while setting up script 2025-05-26 17:50:24 +05:30
Shariq Ansari
875431a620 fix: moved setupHelperMethods from setupFormController to evaluateFormClass 2025-05-26 16:57:02 +05:30
Shariq Ansari
db0c0d98bc fix: pass getDoc function instead of document.doc to keep the reactivity 2025-05-26 16:46:24 +05:30
Shariq Ansari
5406f4a11b
Merge pull request #863 from shariquerik/convert-to-deal-script 2025-05-26 15:40:01 +05:30
Shariq Ansari
bfdd3273fe feat: intercept convert to deal via form script 2025-05-26 14:30:15 +05:30
Shariq Ansari
8798103e7e
Merge pull request #859 from frappe/pot_develop_2025-05-25 2025-05-26 12:08:46 +05:30
Shariq Ansari
203b5ab1ac
Merge branch 'develop' into pot_develop_2025-05-25 2025-05-26 12:04:31 +05:30
Shariq Ansari
ed1b26207b
Merge pull request #858 from shariquerik/create-call-log-script 2025-05-26 11:58:49 +05:30
frappe-pr-bot
e0166a08e2 chore: update POT file 2025-05-25 09:35:42 +00:00
Shariq Ansari
8af4e9b5e8 feat: intercept create lead from call log via form script 2025-05-23 21:49:12 +05:30
Shariq Ansari
900c1d3570
Merge pull request #856 from shariquerik/table-multiselect-fix 2025-05-23 20:39:42 +05:30
Shariq Ansari
b95a17a4e0 fix: set default value as empty array 2025-05-23 20:26:43 +05:30
Shariq Ansari
0f0b012a44
Merge pull request #849 from shariquerik/communication-date 2025-05-22 18:53:20 +05:30
Shariq Ansari
b291f82e4d fix: show communication date instead of creation 2025-05-22 18:13:07 +05:30
Pratik Badhe
86b7222916
Merge pull request #847 from pratikb64/filter-selected-filters 2025-05-22 16:41:57 +05:30
Pratik
7a12b80dd2 fix: hide selected filters from filter list 2025-05-22 10:50:39 +00:00
Pratik
4a836a58ee feat: handle bulk delete 2025-05-22 07:15:22 +00:00
Pratik
b47fc5b93b feat: handle linked docs while deleting 2025-05-21 14:20:48 +00:00
Shariq Ansari
f3b9103a51
Merge pull request #843 from shariquerik/about 2025-05-21 18:00:12 +05:30
Shariq Ansari
dc3ccdddd4 fix: added about link in standard_dropdown_items in hook.py 2025-05-21 17:56:54 +05:30
Shariq Ansari
807eb4a7d9 fix: removed doc & telegram link from user dropdown 2025-05-21 17:51:26 +05:30
Shariq Ansari
a24283eb5e feat: added action to open about modal in user dropdown 2025-05-21 17:50:53 +05:30
Shariq Ansari
fd7116b2e1 feat: show about details in about modal 2025-05-21 17:49:09 +05:30
Shariq Ansari
2e1289df28
Merge pull request #841 from shariquerik/update-toast 2025-05-20 14:37:38 +05:30
Shariq Ansari
6064ca5a4f fix: use toast.create api instead of createToast 2025-05-20 14:35:02 +05:30
Shariq Ansari
3db1b3c0f3 chore: update frappe-ui 2025-05-20 14:21:47 +05:30
Shariq Ansari
06ffa203ef
Merge pull request #838 from shariquerik/invite-member-fix-5 2025-05-20 14:09:38 +05:30
Shariq Ansari
dd1db8f782 fix: added update your password step in onboarding 2025-05-20 14:07:26 +05:30
Shariq Ansari
fe8e309399 feat: added password control 2025-05-20 14:07:15 +05:30
Shariq Ansari
e7a20374c7 fix: set default value as 0 in int field 2025-05-20 14:05:58 +05:30
Shariq Ansari
4cfa0f512b fix: do not show contacts in dropdown in invite member page 2025-05-20 14:05:47 +05:30
Shariq Ansari
64b4f6b759 fix: only set FCRM module if user is Sales User 2025-05-20 14:05:37 +05:30
Shariq Ansari
2d421e6052 fix: allow read permission for form script 2025-05-20 14:05:14 +05:30
Shariq Ansari
cd8dd683fa
Merge pull request #837 from frappe/revert-836-invite-member-fix-3 2025-05-20 13:54:44 +05:30
Shariq Ansari
a2bdc7ab93
Revert "fix: Invite Member Page" 2025-05-20 13:53:35 +05:30
Shariq Ansari
d4132c2411
Merge pull request #836 from shariquerik/invite-member-fix-3 2025-05-20 13:47:44 +05:30
Shariq Ansari
4c6e273268 fix: added update your password step in onboarding 2025-05-20 13:26:02 +05:30
Shariq Ansari
043f174e05 fix: updated components.d.ts 2025-05-20 13:24:42 +05:30
Shariq Ansari
26e9fac1ed feat: added password control 2025-05-20 13:23:26 +05:30
Shariq Ansari
88f33db249 fix: set default value as 0 in int field 2025-05-20 13:17:52 +05:30
Shariq Ansari
55a67bbc0c fix: do not show contacts in dropdown in invite member page 2025-05-20 13:17:20 +05:30
Shariq Ansari
08f042589d fix: only set FCRM module if user is Sales User 2025-05-20 13:16:58 +05:30
Shariq Ansari
52f540a014 fix: allow read permission for form script 2025-05-20 13:15:15 +05:30
Shariq Ansari
e85ef93480
Merge pull request #835 from frappe/revert-833-invite-member-fix-2 2025-05-20 13:09:32 +05:30
Shariq Ansari
a757f80263
Revert "fix: Invite Member Page" 2025-05-20 13:04:02 +05:30
Shariq Ansari
b9b8ff0e10
Merge pull request #833 from shariquerik/invite-member-fix-2 2025-05-20 12:47:27 +05:30
Shariq Ansari
e0aad074ec fix: added update your password step in onboarding 2025-05-19 21:58:03 +05:30
Shariq Ansari
ad88b4e046 feat: added password control 2025-05-19 21:02:10 +05:30
Shariq Ansari
5156814e7a fix: set default value as 0 in int field 2025-05-19 20:41:06 +05:30
Shariq Ansari
f988d16215 fix: use toast.create api instead of createToast 2025-05-19 20:40:11 +05:30
Shariq Ansari
f5a3fccad3 fix: do not show contacts in dropdown in invite member page 2025-05-19 19:05:37 +05:30
Shariq Ansari
e3f0079578 fix: only set FCRM module if user is Sales User 2025-05-19 18:02:30 +05:30
Shariq Ansari
b831ea3c47 fix: allow read permission for form script 2025-05-19 17:58:17 +05:30
Shariq Ansari
a88545b8b9
Merge pull request #831 from shariquerik/onboarding-fixes 2025-05-19 16:51:44 +05:30
Shariq Ansari
44523a0392 fix: store firstLead & firstDeal per user 2025-05-19 16:44:42 +05:30
Shariq Ansari
dbc207a9a6
Merge branch 'develop' into onboarding-fixes 2025-05-19 16:23:35 +05:30
Shariq Ansari
e68d861ee5
Merge pull request #828 from shariquerik/esm-toast 2025-05-19 16:21:16 +05:30
Shariq Ansari
7851bbadfa
Merge branch 'develop' into esm-toast 2025-05-19 16:13:55 +05:30
Shariq Ansari
9223d00af3
Merge pull request #826 from frappe/pot_develop_2025-05-18 2025-05-19 16:12:59 +05:30
Shariq Ansari
740c21532a fix: update Vue compiler options for custom lucide elements 2025-05-19 16:11:19 +05:30
Shariq Ansari
9fdd8bbc17 fix: add @tiptap/extension-paragraph dependency version 2.12.0 2025-05-19 15:56:43 +05:30
Shariq Ansari
0978fa58a2 fix: wrap layout and dialogs in FrappeUIProvider 2025-05-19 15:55:54 +05:30
Shariq Ansari
1395a12d32 build(deps): bump frappeui to 0.1.145 2025-05-19 15:55:37 +05:30
Shariq Ansari
9aab0e7417 fix: update package.json and config files to use ES module syntax 2025-05-19 15:55:07 +05:30
Shariq Ansari
ddc5810c71 fix: change heading to paragraph in invitation email template 2025-05-19 13:40:26 +05:30
Shariq Ansari
21c349e1d7 fix: added dependsOn value on dependent step to gray out 2025-05-19 13:39:06 +05:30
Shariq Ansari
a7784c2985 fix: get filtered steps based on condition 2025-05-19 13:06:38 +05:30
frappe-pr-bot
0cc69d90f0 chore: update POT file 2025-05-18 09:35:38 +00:00
Shariq Ansari
f125737d30
Merge pull request #821 from shariquerik/call-issue 2025-05-15 11:25:13 +05:30
Shariq Ansari
18aef2376a fix: cannot make call 2025-05-15 11:24:27 +05:30
Shariq Ansari
c8287ff107
Merge pull request #818 from shariquerik/contact-not-loading-1 2025-05-15 01:18:00 +05:30
Shariq Ansari
baf344a697 fix: remove updateField event from various components 2025-05-15 01:15:25 +05:30
Shariq Ansari
8c94049e3c fix: contact/organization page not loading 2025-05-15 01:01:48 +05:30
Shariq Ansari
646c76c3cb
Merge pull request #814 from shariquerik/product-details 2025-05-14 23:42:46 +05:30
Shariq Ansari
adbb9f5765 fix: pass doctype argument to get_product_details_script in create_product_details_script 2025-05-14 20:13:34 +05:30
Shariq Ansari
d3a6cc968f fix: add patch to create default scripts 2025-05-14 20:00:48 +05:30
Shariq Ansari
d6ff40cc6a chore: formatting fix 2025-05-14 19:58:39 +05:30
Shariq Ansari
fdd6c46b5f fix: create product detail script on install 2025-05-14 19:57:51 +05:30
Shariq Ansari
26c892c2a0 fix: added products table in crm lead 2025-05-14 19:57:09 +05:30
Shariq Ansari
3516e1ff44 fix: update field visibility logic and disable inputs based on read-only status 2025-05-14 19:08:29 +05:30
Shariq Ansari
0047077074 feat: enhance FormattedInput component with description slot and useAttrs for better attribute handling 2025-05-14 19:01:43 +05:30
Shariq Ansari
8459fac184 fix: set discount amount and net amount fields to read-only 2025-05-14 17:16:32 +05:30
Shariq Ansari
afe828f012 fix: update mandatory field indicator color and replace FormControl with FormattedInput for various field types 2025-05-14 16:55:19 +05:30
Shariq Ansari
60ed0a2043 fix: cache controller on document level not on doctype level 2025-05-14 15:21:47 +05:30
Shariq Ansari
2c9bc07dec fix: added default percent & int to 0 2025-05-14 15:21:07 +05:30
Shariq Ansari
91ba11b565 feat: added callback to update link field value after creating new 2025-05-14 14:04:25 +05:30
Shariq Ansari
8f79427720 fix: select text on focus 2025-05-14 13:34:21 +05:30
Shariq Ansari
32f3aaf38f fix: show formatted percent, currency & float only when not focused 2025-05-14 13:19:54 +05:30
Shariq Ansari
76aaf7f37d feat: added global create document modal for link field 2025-05-12 19:27:41 +05:30
Shariq Ansari
7d37c606cc fix: added columns for product code field 2025-05-12 17:51:36 +05:30
Shariq Ansari
6bce89f277 fix: right aligned number fields 2025-05-12 17:47:49 +05:30
Shariq Ansari
5420fcfe29 fix: render correct currency format 2025-05-12 17:45:05 +05:30
Shariq Ansari
8507c20481 fix: do not show qty and other fields 2025-05-12 17:20:28 +05:30
Shariq Ansari
914dd8bf93 fix: handle this.doc.getRow effectively 2025-05-12 16:37:33 +05:30
Shariq Ansari
960ebdc727 fix: handle field change for float, percent & currency 2025-05-12 16:34:48 +05:30
Shariq Ansari
74ef956638 fix: removed total quantity field 2025-05-12 16:32:09 +05:30
Shariq Ansari
a6323f42af fix: added logic to update amount, net amount, total and net total 2025-05-12 11:58:07 +05:30
Shariq Ansari
bc1c20c91f fix: added crm products table in crm deal and total field 2025-05-12 11:58:07 +05:30
Shariq Ansari
43297373ed fix: added crm product doctype 2025-05-12 11:58:07 +05:30
Shariq Ansari
5228755f7f fix: show formatted percent, currency & float in grid 2025-05-12 11:58:07 +05:30
Shariq Ansari
7ded0a0742 fix: show formatted percent, currency if read only 2025-05-12 11:53:03 +05:30
Shariq Ansari
d74ff9ab62
Merge pull request #811 from shariquerik/required-field-modal-fix 2025-05-12 11:30:32 +05:30
Shariq Ansari
6ef27106df
Merge branch 'develop' into required-field-modal-fix 2025-05-12 11:19:37 +05:30
Shariq Ansari
35a27101c1 fix: error if section is removed and saved 2025-05-12 11:18:47 +05:30
Shariq Ansari
6fbe75c8ad
Merge pull request #803 from frappe/pot_develop_2025-05-04 2025-05-09 20:30:58 +05:30
Shariq Ansari
89fd754efc
Merge pull request #806 from shariquerik/form-script-refactor 2025-05-09 20:24:49 +05:30
Shariq Ansari
576763fe5b fix: enhance error and warning messages with localization support 2025-05-09 18:00:46 +05:30
Shariq Ansari
c67ec08e1a fix: update toast messages for document update success and error handling 2025-05-09 17:53:28 +05:30
Shariq Ansari
6f49573f2f fix: add loading state check to prevent rendering issues in SidePanelLayout 2025-05-09 17:50:43 +05:30
Shariq Ansari
12c3290f19 fix: streamline trigger functions to use a unified handler for controller actions 2025-05-09 17:38:10 +05:30
Shariq Ansari
53c0706a3a feat: implement runSequentially utility for sequential function execution 2025-05-09 17:06:41 +05:30
Shariq Ansari
556386e446 fix: cache controllers and use Promise.all for concurrent execution 2025-05-09 15:22:40 +05:30
Shariq Ansari
07b2d9f792 fix: loop through controllers with multiple instances of multiple scripts and run trigger methods 2025-05-08 18:32:56 +05:30
Shariq Ansari
a2081da296 fix: provide array of instances in controllers if multiple script exist 2025-05-08 18:32:13 +05:30
Shariq Ansari
dde7db9489 fix: remove deprecated setupForm warning and error handling 2025-05-07 19:07:54 +05:30
Shariq Ansari
f947f55fc6 fix: do not show non value fields in dropdown 2025-05-07 19:05:10 +05:30
Shariq Ansari
7bbac6c703 fix: use dayjs for date field default value 2025-05-07 18:26:23 +05:30
Shariq Ansari
420ecb6147 fix: update all fields default value 2025-05-07 18:18:14 +05:30
Shariq Ansari
dcb2787498 feat: handle default value in grid 2025-05-07 18:12:22 +05:30
Shariq Ansari
336083a00f feat: added trigger function on row add & remove 2025-05-07 18:06:38 +05:30
Shariq Ansari
727d0a9acd fix: add doctype, idx, parent, parenttype & parentfield in new grid row 2025-05-07 16:59:22 +05:30
Shariq Ansari
29894ffcca fix: handle commented class declations 2025-05-07 16:38:01 +05:30
Shariq Ansari
e804fa39ba fix: exclude Float & Currency from read only formcontrol 2025-05-07 14:01:39 +05:30
Shariq Ansari
f866284240 fix: allow empty actions 2025-05-07 13:59:55 +05:30
Shariq Ansari
9e3124d29e fix: added triggerOnRefresh & getActions method 2025-05-07 12:15:49 +05:30
Shariq Ansari
d7e0eb09b3 fix: getRow should be available in parent & child instances 2025-05-07 12:14:47 +05:30
Shariq Ansari
5fcd447bc8 fix: added this.meta 2025-05-07 12:13:56 +05:30
Shariq Ansari
6f04b85663 fix: added this.doc.trigger & this.doc.getRow with row.trigger 2025-05-06 13:03:56 +05:30
Shariq Ansari
47262761fe fix: handle section.contacts also 2025-05-06 12:59:31 +05:30
frappe-pr-bot
b46e7a2185 chore: update POT file 2025-05-04 09:35:32 +00:00
Shariq Ansari
2d484c1ad2 fix: handle onchange of grid row field in modal 2025-05-03 15:35:10 +05:30
Shariq Ansari
275fa90a4d fix: added trigger method to call methods from same or different class instance 2025-05-02 18:25:51 +05:30
Shariq Ansari
f8956c70bf fix: handle onchange of grid row field 2025-05-02 16:56:57 +05:30
Shariq Ansari
39fa9c78f8 fix: parse multiple class in form script 2025-05-02 15:56:47 +05:30
Shariq Ansari
d96a29543e fix: added deprecation warning if using old formScript syntax 2025-05-02 08:03:42 +05:30
Shariq Ansari
d2d4abe91f fix: avoid none values 2025-05-02 07:28:40 +05:30
Shariq Ansari
5f567cf138 fix: added change emit in Table bulti select 2025-05-02 06:51:52 +05:30
Shariq Ansari
7bf7d94127 fix: added fieldChange method in almost all fieldtypes 2025-05-01 18:25:25 +05:30
Shariq Ansari
5b8d0d2aeb fix: check if script exist 2025-05-01 18:07:40 +05:30
Shariq Ansari
d37e585205 fix: trigger on change in Field & SidePanelLayout for Select field 2025-05-01 18:03:04 +05:30
Shariq Ansari
a30503ca5f fix: use document to load doc data in sidepanel layout 2025-05-01 18:01:53 +05:30
Shariq Ansari
e65899e384 fix: use document to load doc data in DataFields 2025-05-01 17:57:17 +05:30
Shariq Ansari
16a3f3d66c fix: created triggerOnChange method 2025-05-01 17:56:18 +05:30
Shariq Ansari
1e2f325c55 fix: setup form script in document.js 2025-05-01 17:55:54 +05:30
Shariq Ansari
ccd240f4e8 fix: created document composable to get any doctype record 2025-05-01 17:54:13 +05:30
Shariq Ansari
7b34c5eb66 fix: load script and setup class instances 2025-05-01 17:52:11 +05:30
Shariq Ansari
6da3761e76 fix: check if setupForm exist 2025-05-01 17:27:00 +05:30
Shariq Ansari
b03abdd2eb fix: get scripts api 2025-05-01 17:22:26 +05:30
Shariq Ansari
6ea4e985ef
Merge pull request #787 from frappe/pot_develop_2025-04-27 2025-04-28 12:28:29 +05:30
frappe-pr-bot
699d6cb08c chore: update POT file 2025-04-27 09:35:22 +00:00
Pratik Badhe
ac70deaf19
Merge pull request #781 from pratikb64/call-log-fix
fix: international call log issue
2025-04-23 16:12:59 +05:30
Pratik
4907db44eb fix: international call log issue 2025-04-23 15:54:54 +05:30
Pratik Badhe
81154d1f50
Merge pull request #776 from pratikb64/email-acc-localization
chore: add localization support for email account settings
2025-04-22 15:39:14 +05:30
Pratik
5eb46f6b6c chore: add localization support for email account settings 2025-04-22 15:33:28 +05:30
Shariq Ansari
001a6617f5
Merge pull request #771 from shariquerik/contact-not-loading 2025-04-22 13:11:42 +05:30
Shariq Ansari
c009373a43 fix: do not show error page while loading 2025-04-22 12:59:24 +05:30
Shariq Ansari
cef20e37c2 fix: contact page not loading 2025-04-22 12:57:20 +05:30
Shariq Ansari
20d16c6a32
Merge pull request #759 from frappe/pot_develop_2025-04-20 2025-04-21 14:36:29 +05:30
Shariq Ansari
2fc3daee70
Merge branch 'develop' into pot_develop_2025-04-20 2025-04-21 14:30:57 +05:30
Shariq Ansari
a7955ba9c5
Merge pull request #761 from shariquerik/data-tab-dirty-fix 2025-04-21 11:53:36 +05:30
Shariq Ansari
84e773eab9 fix: do not show error page while loading 2025-04-21 11:46:42 +05:30
Shariq Ansari
da4d3032be fix: mark data tab form dirty by watching field updates 2025-04-21 11:46:19 +05:30
frappe-pr-bot
d89e71ac2f chore: update POT file 2025-04-20 09:35:21 +00:00
Pratik Badhe
de806ee6d9
Merge pull request #753 from pratikb64/email-account-dark-mode
fix: dark mode email account css
2025-04-16 18:10:27 +05:30
Pratik
9c45877999 fix: dark mode email account css 2025-04-16 18:00:28 +05:30
Shariq Ansari
2059ecdb40
Merge pull request #726 from pratikb64/fix-export-logic 2025-04-14 11:19:59 +05:30
Shariq Ansari
52d66b5de4
Merge branch 'develop' into fix-export-logic 2025-04-14 11:15:25 +05:30
Shariq Ansari
fb9b026ad6 fix: restrict app in apps page if no access to FCRM module 2025-04-14 11:05:43 +05:30
Shariq Ansari
8f1b6f6b67
Merge pull request #742 from shariquerik/restrict-app-if-no-module-access-2 2025-04-14 10:36:12 +05:30
Shariq Ansari
0bd448a399 revert: restrict app in apps page if no access to FCRM module 2025-04-14 10:35:28 +05:30
Shariq Ansari
2b395a05ea
Merge pull request #734 from frappe/pot_develop_2025-04-13 2025-04-13 20:14:27 +05:30
Shariq Ansari
dce17de000
Merge pull request #735 from shariquerik/restrict-app-if-no-module-access-1 2025-04-13 20:07:47 +05:30
Shariq Ansari
3881179f72 fix: restrict app in apps page if no access to FCRM module 2025-04-13 19:59:23 +05:30
frappe-pr-bot
da0a502756 chore: update POT file 2025-04-13 09:36:51 +00:00
Pratik
cbf00e29ac refactor: make function names clearer 2025-04-11 18:09:15 +05:30
Pratik
a466766c5c refactor: remove unnecessary watchers 2025-04-08 18:22:44 +05:30
Shariq Ansari
a4781509c4
Merge branch 'develop' into fix-export-logic 2025-04-08 16:37:26 +05:30
Shariq Ansari
8a9361d822 revert: module validation 2025-04-08 16:01:23 +05:30
Shariq Ansari
e2522a492a
Merge pull request #728 from shariquerik/restrict-doc-access
fix: added ErrorPage if user does not have access to doc
2025-04-08 15:41:10 +05:30
Shariq Ansari
bab551c511
Merge branch 'develop' into restrict-doc-access 2025-04-08 15:39:37 +05:30
Shariq Ansari
c63bb16704 ci: added backport to main-hotfix ci 2025-04-08 15:36:32 +05:30
Shariq Ansari
fa56dc4791 fix: show error page if there is no access 2025-04-08 15:28:54 +05:30
Shariq Ansari
e92ee3b730 fix: check read access before loading data 2025-04-08 15:28:19 +05:30
Shariq Ansari
bb794f4887 fix: added ErrorPage component 2025-04-08 15:27:50 +05:30
Pratik
a227389e3e fix: export logic 2025-04-08 15:07:27 +05:30
Shariq Ansari
d9f0b067ca
Merge pull request #722 from shariquerik/added-mergify
ci: added mergify.yml for backport
2025-04-07 21:17:59 +05:30
Shariq Ansari
c0b708462a ci: added mergify.yml for backport 2025-04-07 18:00:59 +05:30
Shariq Ansari
adb0dfff47
Merge pull request #721 from shariquerik/restrict-app-if-no-module-access
fix: restrict app in apps page if no access to FCRM module
2025-04-07 17:37:02 +05:30
Shariq Ansari
6139cb5cb9 fix: restrict app in apps page if no access to FCRM module 2025-04-07 17:31:17 +05:30
Shariq Ansari
61d7924c54
Merge pull request #701 from frappe/pot_develop_2025-03-30
chore: update POT file
2025-04-07 16:51:16 +05:30
Shariq Ansari
899b09ac40
Merge branch 'develop' into pot_develop_2025-03-30 2025-04-07 16:51:07 +05:30
Shariq Ansari
debc9fc1cb
Merge pull request #716 from shariquerik/make-create-call
fix: Create & Make call
2025-04-07 16:49:38 +05:30
Shariq Ansari
5c76adedf3
Merge pull request #712 from shariquerik/dynamic-link
feat: Dynamic Link field support
2025-04-07 16:49:30 +05:30
Shariq Ansari
1ebb26e4c2
Merge pull request #708 from frappe/pot_develop_2025-04-06
chore: update POT file
2025-04-07 16:44:59 +05:30
Shariq Ansari
67378c1f52
Merge pull request #719 from pratikb64/default-assigned-to
fix: default "assigned to" in deals and leads list view
2025-04-07 16:44:32 +05:30
Pratik
469a22ef5f fix: default "assigned to" in deals and leads list view 2025-04-07 16:37:19 +05:30
Shariq Ansari
fdceb51fdc fix: added multi action button to make and create call 2025-04-07 15:34:46 +05:30
Shariq Ansari
97a132e05f fix: show call tab always 2025-04-07 15:32:34 +05:30
Shariq Ansari
26fabddcbe fix: handle feather icon in multi action button 2025-04-07 15:32:09 +05:30
Shariq Ansari
40370067b2 fix: dynamic variant 2025-04-07 14:13:55 +05:30
Shariq Ansari
f0bf6962e7 fix: do not show dropdown if only one option 2025-04-07 14:07:41 +05:30
Shariq Ansari
3b432a0209 fix: added multi action button 2025-04-07 13:58:58 +05:30
Shariq Ansari
c7a03922a0 feat: Dynamic Link field support 2025-04-07 13:16:52 +05:30
frappe-pr-bot
e70b4c091e chore: update POT file 2025-04-06 09:35:28 +00:00
Pratik Badhe
7e38d5e405
Merge pull request #707 from pratikb64/kanban-filter-fix
fix: kanban filter
2025-04-04 17:14:46 +05:30
Pratik
f810e82b45 fix: kanban filter 2025-04-04 17:07:54 +05:30
Pratik Badhe
dff9f93a6b
Merge pull request #704 from pratikb64/make-fields-mandatory
fix: add mandatory fields
2025-04-04 10:26:29 +05:30
Shariq Ansari
c4109ad6ac build(deps): bump frappeui to 0.1.123 2025-04-04 10:09:18 +05:30
Pratik
7a6efb900e fix: add mandatory fields 2025-04-01 17:26:46 +05:30
frappe-pr-bot
e080e47a35 chore: update POT file 2025-03-30 09:35:00 +00:00
Pratik Badhe
82599f91d8
Merge pull request #698 from pratikb64/email-settings-fix
fix: ui alignment
2025-03-28 15:34:36 +05:30
Pratik
8fa156f625 fix: ui alignment 2025-03-28 15:33:40 +05:30
Pratik Badhe
55112cefa9
Merge pull request #697 from pratikb64/email-setting-fix
fix: broken images
2025-03-27 17:38:28 +05:30
Pratik
152c7c8a91 fix: broken images 2025-03-27 17:37:39 +05:30
Pratik Badhe
aa1c0da80e
Merge pull request #696 from pratikb64/add-email-setting
feat: add email account
2025-03-27 15:33:20 +05:30
Pratik
87174f207d feat: add email account 2025-03-27 15:32:37 +05:30
Shariq Ansari
400f879d29 fix: only allow invite by email for Sales Manager & Sales User role 2025-03-26 14:44:40 +05:30
245 changed files with 94293 additions and 15187 deletions

45
.mergify.yml Normal file
View File

@ -0,0 +1,45 @@
pull_request_rules:
- name: Auto-close PRs on stable branch
conditions:
- and:
- and:
- author!=shariquerik
- author!=frappe-pr-bot
- author!=mergify[bot]
- or:
- base=main
actions:
close:
comment:
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on develop branch.
- name: backport to develop
conditions:
- label="backport develop"
actions:
backport:
branches:
- develop
assignees:
- "{{ author }}"
- name: backport to main-hotfix
conditions:
- label="backport main-hotfix"
actions:
backport:
branches:
- main-hotfix
assignees:
- "{{ author }}"
- name: backport to main
conditions:
- label="backport main"
actions:
backport:
branches:
- main
assignees:
- "{{ author }}"

View File

@ -8,7 +8,7 @@
**Simplify Sales, Amplify Relationships**
![GitHub release (latest by date)](https://img.shields.io/github/v/release/frappe/crm)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/frappe/crm)](https://github.com/frappe/crm/releases)
<div>
<picture>
@ -84,6 +84,14 @@ The motivation behind building Frappe CRM stems from the need for a simple, cust
- [Frappe Framework](https://github.com/frappe/frappe): A full-stack web application framework.
- [Frappe UI](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface.
### Compatibility
This app is compatible with the following versions of Frappe and ERPNext:
| CRM branch | Stability | Frappe branch | ERPNext branch |
| :-------------------- | :-------- | :------------------- | :------------------- |
| main - v1.x | stable | v15.x | v15.x |
| develop - future/v2.x | unstable | develop - future/v16 | develop - future/v16 |
## Getting Started (Production)
### Managed Hosting
@ -181,6 +189,7 @@ You need Docker, docker-compose and git setup on your machine. Refer [Docker doc
- [Discuss Forum](https://discuss.frappe.io/c/frappe-crm)
- [Documentation](https://docs.frappe.io/crm)
- [YouTube](https://www.youtube.com/channel/UCn3bV5kx77HsVwtnlCeEi_A)
- [X/Twitter](https://x.com/frappetech)
<br>
<br>

View File

@ -1,9 +1,10 @@
from bs4 import BeautifulSoup
import frappe
from frappe.translate import get_all_translations
from frappe.utils import validate_email_address, split_emails, cstr
from frappe.utils.telemetry import POSTHOG_HOST_FIELD, POSTHOG_PROJECT_FIELD
from bs4 import BeautifulSoup
from frappe.core.api.file import get_max_file_size
from frappe.translate import get_all_translations
from frappe.utils import cstr, split_emails, validate_email_address
from frappe.utils.modules import get_modules_from_all_apps_for_user
from frappe.utils.telemetry import POSTHOG_HOST_FIELD, POSTHOG_PROJECT_FIELD
@frappe.whitelist(allow_guest=True)
@ -63,9 +64,14 @@ def check_app_permission():
if frappe.session.user == "Administrator":
return True
allowed_modules = get_modules_from_all_apps_for_user()
allowed_modules = [x["module_name"] for x in allowed_modules]
if "FCRM" not in allowed_modules:
return False
roles = frappe.get_roles()
if any(
role in ["System Manager", "Sales User", "Sales Manager", "Sales Master Manager"] for role in roles
role in ["System Manager", "Sales User", "Sales Manager"] for role in roles
):
return True
@ -93,9 +99,14 @@ def accept_invitation(key: str | None = None):
@frappe.whitelist()
def invite_by_email(emails: str, role: str):
frappe.only_for("Sales Manager")
frappe.only_for(["Sales Manager", "System Manager"])
if role not in ["System Manager", "Sales Manager", "Sales User"]:
frappe.throw("Cannot invite for this role")
if not emails:
return
email_string = validate_email_address(emails, throw=False)
email_list = split_emails(email_string)
if not email_list:
@ -103,7 +114,10 @@ def invite_by_email(emails: str, role: str):
existing_members = frappe.db.get_all("User", filters={"email": ["in", email_list]}, pluck="email")
existing_invites = frappe.db.get_all(
"CRM Invitation",
filters={"email": ["in", email_list], "role": ["in", ["Sales Manager", "Sales User"]]},
filters={
"email": ["in", email_list],
"role": ["in", ["System Manager", "Sales Manager", "Sales User"]],
},
pluck="email",
)
@ -112,6 +126,12 @@ def invite_by_email(emails: str, role: str):
for email in to_invite:
frappe.get_doc(doctype="CRM Invitation", email=email, role=role).insert(ignore_permissions=True)
return {
"existing_members": existing_members,
"existing_invites": existing_invites,
"to_invite": to_invite,
}
@frappe.whitelist()
def get_file_uploader_defaults(doctype: str):

View File

@ -124,6 +124,7 @@ def get_deal_activities(name):
activity = {
"activity_type": "communication",
"communication_type": communication.communication_type,
"communication_date": communication.communication_date or communication.creation,
"creation": communication.creation,
"data": {
"subject": communication.subject,
@ -255,6 +256,7 @@ def get_lead_activities(name):
activity = {
"activity_type": "communication",
"communication_type": communication.communication_type,
"communication_date": communication.communication_date or communication.creation,
"creation": communication.creation,
"data": {
"subject": communication.subject,

View File

@ -14,32 +14,16 @@ def update_deals_email_mobile_no(doc):
)
for linked_deal in linked_deals:
deal = frappe.get_cached_doc("CRM Deal", linked_deal.parent)
deal = frappe.db.get_values("CRM Deal", linked_deal.parent, ["email", "mobile_no"], as_dict=True)[0]
if deal.email != doc.email_id or deal.mobile_no != doc.mobile_no:
deal.email = doc.email_id
deal.mobile_no = doc.mobile_no
deal.save(ignore_permissions=True)
@frappe.whitelist()
def get_contact(name):
Contact = frappe.qb.DocType("Contact")
query = frappe.qb.from_(Contact).select("*").where(Contact.name == name).limit(1)
contact = query.run(as_dict=True)
if not len(contact):
frappe.throw(_("Contact not found"), frappe.DoesNotExistError)
contact = contact.pop()
contact["doctype"] = "Contact"
contact["email_ids"] = frappe.get_all(
"Contact Email", filters={"parent": name}, fields=["name", "email_id", "is_primary"]
)
contact["phone_nos"] = frappe.get_all(
"Contact Phone", filters={"parent": name}, fields=["name", "phone", "is_primary_mobile_no"]
)
return contact
frappe.db.set_value(
"CRM Deal",
linked_deal.parent,
{
"email": doc.email_id,
"mobile_no": doc.mobile_no,
},
)
@frappe.whitelist()

1142
crm/api/dashboard.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ import json
import frappe
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.form.assign_to import set_status
from frappe.model import no_value_fields
from frappe.model.document import get_controller
from frappe.utils import make_filter_tuple
@ -10,6 +11,7 @@ from pypika import Criterion
from crm.api.views import get_views
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
from crm.utils import get_dynamic_linked_docs, get_linked_docs
@frappe.whitelist()
@ -418,16 +420,23 @@ def get_data(
rows.append(field)
for kc in kanban_columns:
column_filters = {column_field: kc.get("name")}
# Start with base filters
column_filters = []
# Convert and add the main filters first
if filters:
base_filters = convert_filter_to_tuple(doctype, filters)
column_filters.extend(base_filters)
# Add the column-specific filter
if column_field and kc.get("name"):
column_filters.append([doctype, column_field, "=", kc.get("name")])
order = kc.get("order")
if (column_field in filters and filters.get(column_field) != kc.get("name")) or kc.get("delete"):
if kc.get("delete"):
column_data = []
else:
column_filters.update(filters.copy())
page_length = 20
if kc.get("page_length"):
page_length = kc.get("page_length")
page_length = kc.get("page_length", 20)
if order:
column_data = get_records_based_on_order(
@ -437,26 +446,20 @@ def get_data(
column_data = frappe.get_list(
doctype,
fields=rows,
filters=convert_filter_to_tuple(doctype, column_filters),
filters=column_filters,
order_by=order_by,
page_length=page_length,
)
new_filters = filters.copy()
new_filters.update({column_field: kc.get("name")})
all_count = frappe.get_list(
doctype,
filters=convert_filter_to_tuple(doctype, new_filters),
filters=column_filters,
fields="count(*) as total_count",
)[0].total_count
kc["all_count"] = all_count
kc["count"] = len(column_data)
for d in column_data:
getCounts(d, doctype)
if order:
column_data = sorted(
column_data,
@ -658,6 +661,25 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False, only_re
return fields_meta
@frappe.whitelist()
def remove_assignments(doctype, name, assignees, ignore_permissions=False):
assignees = json.loads(assignees)
if not assignees:
return
for assign_to in assignees:
set_status(
doctype,
name,
todo=None,
assign_to=assign_to,
status="Cancelled",
ignore_permissions=ignore_permissions,
)
@frappe.whitelist()
def get_assigned_users(doctype, name, default_assigned_to=None):
assigned_users = frappe.get_all(
"ToDo",
@ -725,3 +747,98 @@ def getCounts(d, doctype):
"FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
)
return d
@frappe.whitelist()
def get_linked_docs_of_document(doctype, docname):
doc = frappe.get_doc(doctype, docname)
linked_docs = get_linked_docs(doc)
dynamic_linked_docs = get_dynamic_linked_docs(doc)
linked_docs.extend(dynamic_linked_docs)
linked_docs = list({doc["reference_docname"]: doc for doc in linked_docs}.values())
docs_data = []
for doc in linked_docs:
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
title = data.get("title")
if data.doctype == "CRM Call Log":
title = f"Call from {data.get('from')} to {data.get('to')}"
if data.doctype == "CRM Deal":
title = data.get("organization")
docs_data.append(
{
"doc": data.doctype,
"title": title or data.get("name"),
"reference_docname": doc["reference_docname"],
"reference_doctype": doc["reference_doctype"],
}
)
return docs_data
def remove_doc_link(doctype, docname):
linked_doc_data = frappe.get_doc(doctype, docname)
linked_doc_data.update(
{
"reference_doctype": None,
"reference_docname": None,
}
)
linked_doc_data.save(ignore_permissions=True)
def remove_contact_link(doctype, docname):
linked_doc_data = frappe.get_doc(doctype, docname)
linked_doc_data.update(
{
"contact": None,
"contacts": [],
}
)
linked_doc_data.save(ignore_permissions=True)
@frappe.whitelist()
def remove_linked_doc_reference(items, remove_contact=None, delete=False):
if isinstance(items, str):
items = frappe.parse_json(items)
for item in items:
if remove_contact:
remove_contact_link(item["doctype"], item["docname"])
else:
remove_doc_link(item["doctype"], item["docname"])
if delete:
frappe.delete_doc(item["doctype"], item["docname"])
return "success"
@frappe.whitelist()
def delete_bulk_docs(doctype, items, delete_linked=False):
from frappe.desk.reportview import delete_bulk
items = frappe.parse_json(items)
for doc in items:
linked_docs = get_linked_docs_of_document(doctype, doc)
for linked_doc in linked_docs:
remove_linked_doc_reference(
[
{
"doctype": linked_doc["reference_doctype"],
"docname": linked_doc["reference_docname"],
}
],
remove_contact=doctype == "Contact",
delete=delete_linked,
)
if len(items) > 10:
frappe.enqueue("frappe.desk.reportview.delete_bulk", doctype=doctype, items=items)
else:
delete_bulk(doctype, items)
return "success"

View File

@ -23,11 +23,32 @@ def get_users():
if frappe.session.user == user.name:
user.session_user = True
user.is_manager = "Sales Manager" in frappe.get_roles(user.name) or user.name == "Administrator"
user.roles = frappe.get_roles(user.name)
user.is_agent = frappe.db.exists("CRM Telephony Agent", {"user": user.name})
user.role = ""
return users
if "System Manager" in user.roles:
user.role = "System Manager"
elif "Sales Manager" in user.roles:
user.role = "Sales Manager"
elif "Sales User" in user.roles:
user.role = "Sales User"
elif "Guest" in user.roles:
user.role = "Guest"
if frappe.session.user == user.name:
user.session_user = True
user.is_telephony_agent = frappe.db.exists("CRM Telephony Agent", {"user": user.name})
crm_users = []
# crm users are users with role Sales User or Sales Manager
for user in users:
if "Sales User" in user.roles or "Sales Manager" in user.roles:
crm_users.append(user)
return users, crm_users
@frappe.whitelist()

99
crm/api/settings.py Normal file
View File

@ -0,0 +1,99 @@
import frappe
@frappe.whitelist()
def create_email_account(data):
service = data.get("service")
service_config = email_service_config.get(service)
if not service_config:
return "Service not supported"
try:
email_doc = frappe.get_doc(
{
"doctype": "Email Account",
"email_id": data.get("email_id"),
"email_account_name": data.get("email_account_name"),
"service": service,
"enable_incoming": data.get("enable_incoming"),
"enable_outgoing": data.get("enable_outgoing"),
"default_incoming": data.get("default_incoming"),
"default_outgoing": data.get("default_outgoing"),
"email_sync_option": "ALL",
"initial_sync_count": 100,
"create_contact": 1,
"track_email_status": 1,
"use_tls": 1,
"use_imap": 1,
"smtp_port": 587,
**service_config,
}
)
if service == "Frappe Mail":
email_doc.api_key = data.get("api_key")
email_doc.api_secret = data.get("api_secret")
email_doc.frappe_mail_site = data.get("frappe_mail_site")
email_doc.append_to = "CRM Lead"
else:
email_doc.append("imap_folder", {"append_to": "CRM Lead", "folder_name": "INBOX"})
email_doc.password = data.get("password")
# validate whether the credentials are correct
email_doc.get_incoming_server()
# if correct credentials, save the email account
email_doc.save()
except Exception as e:
frappe.throw(str(e))
email_service_config = {
"Frappe Mail": {
"domain": None,
"password": None,
"awaiting_password": 0,
"ascii_encode_password": 0,
"login_id_is_different": 0,
"login_id": None,
"use_imap": 0,
"use_ssl": 0,
"validate_ssl_certificate": 0,
"use_starttls": 0,
"email_server": None,
"incoming_port": 0,
"always_use_account_email_id_as_sender": 1,
"use_tls": 0,
"use_ssl_for_outgoing": 0,
"smtp_server": None,
"smtp_port": None,
"no_smtp_authentication": 0,
},
"GMail": {
"email_server": "imap.gmail.com",
"use_ssl": 1,
"smtp_server": "smtp.gmail.com",
},
"Outlook": {
"email_server": "imap-mail.outlook.com",
"use_ssl": 1,
"smtp_server": "smtp-mail.outlook.com",
},
"Sendgrid": {
"smtp_server": "smtp.sendgrid.net",
"smtp_port": 587,
},
"SparkPost": {
"smtp_server": "smtp.sparkpostmail.com",
},
"Yahoo": {
"email_server": "imap.mail.yahoo.com",
"use_ssl": 1,
"smtp_server": "smtp.mail.yahoo.com",
"smtp_port": 587,
},
"Yandex": {
"email_server": "imap.yandex.com",
"use_ssl": 1,
"smtp_server": "smtp.yandex.com",
"smtp_port": 587,
},
}

84
crm/api/user.py Normal file
View File

@ -0,0 +1,84 @@
import frappe
@frappe.whitelist()
def add_existing_users(users, role="Sales User"):
"""
Add existing users to the CRM by assigning them a role (Sales User or Sales Manager).
:param users: List of user names to be added
"""
frappe.only_for(["System Manager", "Sales Manager"])
users = frappe.parse_json(users)
for user in users:
add_user(user, role)
@frappe.whitelist()
def update_user_role(user, new_role):
"""
Update the role of the user to Sales Manager, Sales User, or System Manager.
:param user: The name of the user
:param new_role: The new role to assign (Sales Manager or Sales User)
"""
frappe.only_for(["System Manager", "Sales Manager"])
if new_role not in ["System Manager", "Sales Manager", "Sales User"]:
frappe.throw("Cannot assign this role")
user_doc = frappe.get_doc("User", user)
if new_role == "System Manager":
user_doc.append_roles("System Manager", "Sales Manager", "Sales User")
user_doc.set("block_modules", [])
if new_role == "Sales Manager":
user_doc.append_roles("Sales Manager", "Sales User")
user_doc.remove_roles("System Manager")
if new_role == "Sales User":
user_doc.append_roles("Sales User")
user_doc.remove_roles("Sales Manager", "System Manager")
update_module_in_user(user_doc, "FCRM")
user_doc.save(ignore_permissions=True)
@frappe.whitelist()
def add_user(user, role):
"""
Add a user means adding role (Sales User or/and Sales Manager) to the user.
:param user: The name of the user to be added
:param role: The role to be assigned (Sales User or Sales Manager)
"""
update_user_role(user, role)
@frappe.whitelist()
def remove_user(user):
"""
Remove a user means removing Sales User & Sales Manager roles from the user.
:param user: The name of the user to be removed
"""
frappe.only_for(["System Manager", "Sales Manager"])
user_doc = frappe.get_doc("User", user)
roles = [d.role for d in user_doc.roles]
if "Sales User" in roles:
user_doc.remove_roles("Sales User")
if "Sales Manager" in roles:
user_doc.remove_roles("Sales Manager")
user_doc.save(ignore_permissions=True)
frappe.msgprint(f"User {user} has been removed from CRM roles.")
def update_module_in_user(user, module):
block_modules = frappe.get_all(
"Module Def",
fields=["name as module"],
filters={"name": ["!=", module]},
)
if block_modules:
user.set("block_modules", block_modules)

View File

@ -335,5 +335,5 @@ def get_from_name(message):
else:
from_name = doc.get("lead_name")
else:
from_name = doc.get("first_name") + " " + doc.get("last_name")
from_name = " ".join(filter(None, [doc.get("first_name"), doc.get("last_name")]))
return from_name

View File

@ -41,13 +41,15 @@
"fieldname": "from",
"fieldtype": "Data",
"in_list_view": 1,
"label": "From"
"label": "From",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Initiated\nRinging\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled"
"options": "Initiated\nRinging\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled",
"reqd": 1
},
{
"fieldname": "start_time",
@ -69,13 +71,15 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Type",
"options": "Incoming\nOutgoing"
"options": "Incoming\nOutgoing",
"reqd": 1
},
{
"fieldname": "to",
"fieldtype": "Data",
"in_list_view": 1,
"label": "To"
"label": "To",
"reqd": 1
},
{
"description": "Call duration in seconds",
@ -153,7 +157,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-01-22 17:57:59.289548",
"modified": "2025-04-01 16:01:54.479309",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Call Log",

View File

@ -190,11 +190,20 @@ def get_call_log(name):
@frappe.whitelist()
def create_lead_from_call_log(call_log):
def create_lead_from_call_log(call_log, lead_details=None):
lead = frappe.new_doc("CRM Lead")
lead.first_name = "Lead from call " + call_log.get("from")
lead.mobile_no = call_log.get("from")
lead.lead_owner = frappe.session.user
lead_details = frappe.parse_json(lead_details or "{}")
if not lead_details.get("lead_owner"):
lead_details["lead_owner"] = frappe.session.user
if not lead_details.get("mobile_no"):
lead_details["mobile_no"] = call_log.get("from") or ""
if not lead_details.get("first_name"):
lead_details["first_name"] = "Lead from call " + (
lead_details.get("mobile_no") or call_log.get("name")
)
lead.update(lead_details)
lead.save(ignore_permissions=True)
# link call log with lead

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Dashboard", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,105 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:title",
"creation": "2025-07-14 12:19:49.725022",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"title",
"private",
"column_break_exbw",
"user",
"section_break_hfza",
"layout"
],
"fields": [
{
"fieldname": "column_break_exbw",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_hfza",
"fieldtype": "Section Break"
},
{
"default": "[]",
"fieldname": "layout",
"fieldtype": "Code",
"label": "Layout",
"options": "JSON"
},
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Name",
"unique": 1
},
{
"depends_on": "private",
"fieldname": "user",
"fieldtype": "Link",
"label": "User",
"mandatory_depends_on": "private",
"options": "User"
},
{
"default": "0",
"fieldname": "private",
"fieldtype": "Check",
"label": "Private"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-14 12:36:10.831351",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Dashboard",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}

View File

@ -0,0 +1,33 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class CRMDashboard(Document):
pass
def default_manager_dashboard_layout():
"""
Returns the default layout for the CRM Manager Dashboard.
"""
return '[{"name":"total_leads","type":"number_chart","tooltip":"Total number of leads","layout":{"x":0,"y":0,"w":4,"h":3,"i":"total_leads"}},{"name":"ongoing_deals","type":"number_chart","tooltip":"Total number of ongoing deals","layout":{"x":8,"y":0,"w":4,"h":3,"i":"ongoing_deals"}},{"name":"won_deals","type":"number_chart","tooltip":"Total number of won deals","layout":{"x":12,"y":0,"w":4,"h":3,"i":"won_deals"}},{"name":"average_won_deal_value","type":"number_chart","tooltip":"Average value of won deals","layout":{"x":16,"y":0,"w":4,"h":3,"i":"average_won_deal_value"}},{"name":"average_deal_value","type":"number_chart","tooltip":"Average deal value of ongoing and won deals","layout":{"x":0,"y":2,"w":4,"h":3,"i":"average_deal_value"}},{"name":"average_time_to_close_a_lead","type":"number_chart","tooltip":"Average time taken to close a lead","layout":{"x":4,"y":0,"w":4,"h":3,"i":"average_time_to_close_a_lead"}},{"name":"average_time_to_close_a_deal","type":"number_chart","layout":{"x":4,"y":2,"w":4,"h":3,"i":"average_time_to_close_a_deal"}},{"name":"spacer","type":"spacer","layout":{"x":8,"y":2,"w":12,"h":3,"i":"spacer"}},{"name":"sales_trend","type":"axis_chart","layout":{"x":0,"y":4,"w":10,"h":9,"i":"sales_trend"}},{"name":"forecasted_revenue","type":"axis_chart","layout":{"x":10,"y":4,"w":10,"h":9,"i":"forecasted_revenue"}},{"name":"funnel_conversion","type":"axis_chart","layout":{"x":0,"y":11,"w":10,"h":9,"i":"funnel_conversion"}},{"name":"deals_by_stage_donut","type":"donut_chart","layout":{"x":10,"y":11,"w":10,"h":9,"i":"deals_by_stage_donut"}},{"name":"lost_deal_reasons","type":"axis_chart","layout":{"x":0,"y":32,"w":20,"h":9,"i":"lost_deal_reasons"}},{"name":"leads_by_source","type":"donut_chart","layout":{"x":0,"y":18,"w":10,"h":9,"i":"leads_by_source"}},{"name":"deals_by_source","type":"donut_chart","layout":{"x":10,"y":18,"w":10,"h":9,"i":"deals_by_source"}},{"name":"deals_by_territory","type":"axis_chart","layout":{"x":0,"y":25,"w":10,"h":9,"i":"deals_by_territory"}},{"name":"deals_by_salesperson","type":"axis_chart","layout":{"x":10,"y":25,"w":10,"h":9,"i":"deals_by_salesperson"}}]'
def create_default_manager_dashboard(force=False):
"""
Creates the default CRM Manager Dashboard if it does not exist.
"""
if not frappe.db.exists("CRM Dashboard", "Manager Dashboard"):
doc = frappe.new_doc("CRM Dashboard")
doc.title = "Manager Dashboard"
doc.layout = default_manager_dashboard_layout()
doc.insert(ignore_permissions=True)
elif force:
doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard")
doc.layout = default_manager_dashboard_layout()
doc.save(ignore_permissions=True)
return doc.layout

View File

@ -0,0 +1,30 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestCRMDashboard(UnitTestCase):
"""
Unit tests for CRMDashboard.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestCRMDashboard(IntegrationTestCase):
"""
Integration tests for CRMDashboard.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -1,18 +1,5 @@
import frappe
from crm.api.doc import get_assigned_users, get_fields_meta
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
@frappe.whitelist()
def get_deal(name):
deal = frappe.get_doc("CRM Deal", name).as_dict()
deal["fields_meta"] = get_fields_meta("CRM Deal")
deal["_form_script"] = get_form_script("CRM Deal")
deal["_assign"] = get_assigned_users("CRM Deal", deal.name)
return deal
@frappe.whitelist()
def get_deal_contacts(name):
@ -30,24 +17,12 @@ def get_deal_contacts(name):
is_primary = contact.is_primary
contact = frappe.get_doc("Contact", contact.contact).as_dict()
def get_primary_email(contact):
for email in contact.email_ids:
if email.is_primary:
return email.email_id
return contact.email_ids[0].email_id if contact.email_ids else ""
def get_primary_mobile_no(contact):
for phone in contact.phone_nos:
if phone.is_primary:
return phone.phone
return contact.phone_nos[0].phone if contact.phone_nos else ""
_contact = {
"name": contact.name,
"image": contact.image,
"full_name": contact.full_name,
"email": get_primary_email(contact),
"mobile_no": get_primary_mobile_no(contact),
"email": contact.email_id,
"mobile_no": contact.mobile_no,
"is_primary": is_primary,
}
deal_contacts.append(_contact)

View File

@ -5,4 +5,68 @@ frappe.ui.form.on("CRM Deal", {
refresh(frm) {
frm.add_web_link(`/crm/deals/${frm.doc.name}`, __("Open in Portal"));
},
update_total: function (frm) {
let total = 0;
let total_qty = 0;
let net_total = 0;
frm.doc.products.forEach((d) => {
total += d.amount;
total_qty += d.qty;
net_total += d.net_amount;
});
frappe.model.set_value(frm.doctype, frm.docname, "total", total);
frappe.model.set_value(
frm.doctype,
frm.docname,
"net_total",
net_total || total
);
}
});
frappe.ui.form.on("CRM Products", {
products_add: function (frm, cdt, cdn) {
frm.trigger("update_total");
},
products_remove: function (frm, cdt, cdn) {
frm.trigger("update_total");
},
product_code: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, "product_name", d.product_code);
},
rate: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
if (d.rate && d.qty) {
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
}
frm.trigger("update_total");
},
qty: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
if (d.rate && d.qty) {
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
}
frm.trigger("update_total");
},
discount_percentage: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
if (d.discount_percentage && d.amount) {
discount_amount = (d.discount_percentage / 100) * d.amount;
frappe.model.set_value(
cdt,
cdn,
"discount_amount",
discount_amount
);
frappe.model.set_value(
cdt,
cdn,
"net_amount",
d.amount - discount_amount
);
}
frm.trigger("update_total");
}
});

View File

@ -11,11 +11,18 @@
"naming_series",
"organization",
"next_step",
"probability",
"column_break_ijan",
"status",
"close_date",
"deal_owner",
"lost_reason",
"lost_notes",
"section_break_jgpm",
"probability",
"expected_deal_value",
"deal_value",
"column_break_kpxa",
"expected_closure_date",
"closed_date",
"contacts_tab",
"contacts",
"contact",
@ -32,6 +39,7 @@
"column_break_xbyf",
"territory",
"currency",
"exchange_rate",
"annual_revenue",
"industry",
"person_section",
@ -43,6 +51,12 @@
"mobile_no",
"phone",
"gender",
"products_tab",
"products",
"section_break_ccbj",
"total",
"column_break_udbq",
"net_total",
"sla_tab",
"sla",
"sla_creation",
@ -82,11 +96,6 @@
"fieldtype": "Data",
"label": "Website"
},
{
"fieldname": "close_date",
"fieldtype": "Date",
"label": "Close Date"
},
{
"fieldname": "next_step",
"fieldtype": "Data",
@ -119,14 +128,16 @@
{
"fieldname": "email",
"fieldtype": "Data",
"label": "Email",
"options": "Email"
"label": "Primary Email",
"options": "Email",
"read_only": 1
},
{
"fieldname": "mobile_no",
"fieldtype": "Data",
"label": "Mobile No",
"options": "Phone"
"label": "Primary Mobile No",
"options": "Phone",
"read_only": 1
},
{
"default": "Qualification",
@ -239,8 +250,9 @@
{
"fieldname": "phone",
"fieldtype": "Data",
"label": "Phone",
"options": "Phone"
"label": "Primary Phone",
"options": "Phone",
"read_only": 1
},
{
"fieldname": "log_tab",
@ -334,11 +346,96 @@
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "products_tab",
"fieldtype": "Tab Break",
"label": "Products"
},
{
"fieldname": "products",
"fieldtype": "Table",
"label": "Products",
"options": "CRM Products"
},
{
"fieldname": "section_break_ccbj",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_udbq",
"fieldtype": "Column Break"
},
{
"fieldname": "total",
"fieldtype": "Currency",
"label": "Total",
"options": "currency",
"read_only": 1
},
{
"description": "Total after discount",
"fieldname": "net_total",
"fieldtype": "Currency",
"label": "Net Total",
"options": "currency",
"read_only": 1
},
{
"fieldname": "section_break_jgpm",
"fieldtype": "Section Break"
},
{
"fieldname": "deal_value",
"fieldtype": "Currency",
"label": "Deal Value",
"options": "currency"
},
{
"fieldname": "column_break_kpxa",
"fieldtype": "Column Break"
},
{
"fieldname": "lost_reason",
"fieldtype": "Link",
"label": "Lost Reason",
"mandatory_depends_on": "eval: doc.status == \"Lost\"",
"options": "CRM Lost Reason"
},
{
"fieldname": "lost_notes",
"fieldtype": "Text",
"label": "Lost Notes",
"mandatory_depends_on": "eval: doc.lost_reason == \"Other\""
},
{
"default": "1",
"description": "The rate used to convert the deal\u2019s currency to your crm's base currency (set in CRM Settings). It is set once when the currency is first added and doesn't change automatically.",
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate"
},
{
"fieldname": "expected_deal_value",
"fieldtype": "Currency",
"label": "Expected Deal Value",
"options": "currency"
},
{
"fieldname": "expected_closure_date",
"fieldtype": "Date",
"label": "Expected Closure Date"
},
{
"fieldname": "closed_date",
"fieldtype": "Date",
"label": "Closed Date"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-12-11 14:31:41.058895",
"modified": "2025-07-13 11:54:20.608489",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Deal",
@ -370,10 +467,11 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "organization",
"track_changes": 1
}
}

View File

@ -7,9 +7,8 @@ from frappe.desk.form.assign_to import add as assign
from frappe.model.document import Document
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
add_status_change_log,
)
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log
from crm.fcrm.doctype.fcrm_settings.fcrm_settings import get_exchange_rate
class CRMDeal(Document):
@ -24,6 +23,11 @@ class CRMDeal(Document):
self.assign_agent(self.deal_owner)
if self.has_value_changed("status"):
add_status_change_log(self)
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
self.closed_date = frappe.utils.nowdate()
self.validate_forcasting_fields()
self.validate_lost_reason()
self.update_exchange_rate()
def after_insert(self):
if self.deal_owner:
@ -133,6 +137,48 @@ class CRMDeal(Document):
if sla:
sla.apply(self)
def update_closed_date(self):
"""
Update the closed date based on the "Won" status.
"""
if self.status == "Won" and not self.closed_date:
self.closed_date = frappe.utils.nowdate()
def update_default_probability(self):
"""
Update the default probability based on the status.
"""
if not self.probability or self.probability == 0:
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
def validate_forcasting_fields(self):
self.update_closed_date()
self.update_default_probability()
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
if not self.expected_deal_value or self.expected_deal_value == 0:
frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError)
if not self.expected_closure_date:
frappe.throw(_("Expected Closure Date is required."), frappe.MandatoryError)
def validate_lost_reason(self):
"""
Validate the lost reason if the status is set to "Lost".
"""
if self.status and frappe.get_cached_value("CRM Deal Status", self.status, "type") == "Lost":
if not self.lost_reason:
frappe.throw(_("Please specify a reason for losing the deal."), frappe.ValidationError)
elif self.lost_reason == "Other" and not self.lost_notes:
frappe.throw(_("Please specify the reason for losing the deal."), frappe.ValidationError)
def update_exchange_rate(self):
if self.has_value_changed("currency") or not self.exchange_rate:
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
exchange_rate = 1
if self.currency and self.currency != system_currency:
exchange_rate = get_exchange_rate(self.currency, system_currency)
self.db_set("exchange_rate", exchange_rate)
@staticmethod
def default_list_data():
columns = [

View File

@ -7,8 +7,11 @@
"engine": "InnoDB",
"field_order": [
"deal_status",
"color",
"position"
"type",
"position",
"column_break_ojiu",
"probability",
"color"
],
"fields": [
{
@ -32,11 +35,30 @@
"fieldtype": "Int",
"in_list_view": 1,
"label": "Position"
},
{
"fieldname": "probability",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Probability"
},
{
"default": "Open",
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Open\nOngoing\nOn Hold\nWon\nLost"
},
{
"fieldname": "column_break_ojiu",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-01-19 21:56:44.552134",
"modified": "2025-07-11 16:03:28.077955",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Deal Status",
@ -68,7 +90,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@ -27,7 +27,9 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
if not tabs and type != "Required Fields":
tabs = get_default_layout(doctype)
has_tabs = tabs[0].get("sections") if tabs and tabs[0] else False
has_tabs = False
if isinstance(tabs, list) and len(tabs) > 0 and isinstance(tabs[0], dict):
has_tabs = any("sections" in tab for tab in tabs)
if not has_tabs:
tabs = [{"name": "first_tab", "sections": tabs}]
@ -45,9 +47,19 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldname in allowed_fields]
required_fields = []
if type == "Required Fields":
required_fields = [
field for field in frappe.get_meta(doctype, False).fields if field.reqd and not field.default
]
for tab in tabs:
for section in tab.get("sections"):
if section.get("columns"):
section["columns"] = [column for column in section.get("columns") if column]
for column in section.get("columns") if section.get("columns") else []:
column["fields"] = [field for field in column.get("fields") if field]
for field in column.get("fields") if column.get("fields") else []:
field = next((f for f in fields if f.fieldname == field), None)
if field:
@ -55,6 +67,32 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
handle_perm_level_restrictions(field, doctype, parent_doctype)
column["fields"][column.get("fields").index(field["fieldname"])] = field
# remove field from required_fields if it is already present
if (
type == "Required Fields"
and field.reqd
and any(f.get("fieldname") == field.get("fieldname") for f in required_fields)
):
required_fields = [
f for f in required_fields if f.get("fieldname") != field.get("fieldname")
]
if type == "Required Fields" and required_fields and tabs:
tabs[-1].get("sections").append(
{
"label": "Required Fields",
"name": "required_fields_section_" + str(random_string(4)),
"opened": True,
"hideLabel": True,
"columns": [
{
"name": "required_fields_column_" + str(random_string(4)),
"fields": [field.as_dict() for field in required_fields],
}
],
}
)
return tabs or []
@ -78,6 +116,8 @@ def get_sidepanel_sections(doctype):
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
add_forecasting_section(layout, doctype)
for section in layout:
section["name"] = section.get("name") or section.get("label")
for column in section.get("columns") if section.get("columns") else []:
@ -95,6 +135,38 @@ def get_sidepanel_sections(doctype):
return layout
def add_forecasting_section(layout, doctype):
if (
doctype == "CRM Deal"
and frappe.db.get_single_value("FCRM Settings", "enable_forecasting")
and not any(section.get("name") == "forecasted_sales_section" for section in layout)
):
contacts_section_index = next(
(
i
for i, section in enumerate(layout)
if section.get("name") == "contacts_section" or section.get("label") == "Contacts"
),
None,
)
if contacts_section_index is not None:
layout.insert(
contacts_section_index + 1,
{
"name": "forecasted_sales_section",
"label": "Forecasted Sales",
"opened": True,
"columns": [
{
"name": "column_" + str(random_string(4)),
"fields": ["expected_closure_date", "probability", "expected_deal_value"],
}
],
},
)
def handle_perm_level_restrictions(field, doctype, parent_doctype=None):
if field.permlevel == 0:
return

View File

@ -65,7 +65,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-16 19:40:19.340948",
"modified": "2025-05-19 17:57:24.610295",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Form Script",
@ -83,9 +83,19 @@
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@ -27,7 +27,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Role",
"options": "\nSales User\nSales Manager",
"options": "\nSales User\nSales Manager\nSystem Manager",
"reqd": 1
},
{
@ -66,7 +66,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-03 14:59:29.450018",
"modified": "2025-06-17 17:20:18.935395",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Invitation",
@ -106,7 +106,8 @@
"share": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@ -21,7 +21,7 @@ class CRMInvitation(Document):
if frappe.local.dev_server:
print(f"Invite link for {self.email}: {invite_link}")
title = f"Frappe CRM"
title = "Frappe CRM"
template = "crm_invitation"
frappe.sendmail(
@ -35,7 +35,7 @@ class CRMInvitation(Document):
@frappe.whitelist()
def accept_invitation(self):
frappe.only_for("System Manager")
frappe.only_for(["System Manager", "Sales Manager"])
self.accept()
def accept(self):
@ -44,12 +44,28 @@ class CRMInvitation(Document):
user = self.create_user_if_not_exists()
user.append_roles(self.role)
if self.role == "System Manager":
user.append_roles("Sales Manager", "Sales User")
elif self.role == "Sales Manager":
user.append_roles("Sales User")
if self.role == "Sales User":
self.update_module_in_user(user, "FCRM")
user.save(ignore_permissions=True)
self.status = "Accepted"
self.accepted_at = frappe.utils.now()
self.save(ignore_permissions=True)
def update_module_in_user(self, user, module):
block_modules = frappe.get_all(
"Module Def",
fields=["name as module"],
filters={"name": ["!=", module]},
)
if block_modules:
user.set("block_modules", block_modules)
def create_user_if_not_exists(self):
if not frappe.db.exists("User", self.email):
first_name = self.email.split("@")[0].title()

View File

@ -1,14 +0,0 @@
import frappe
from crm.api.doc import get_assigned_users, get_fields_meta
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
@frappe.whitelist()
def get_lead(name):
lead = frappe.get_doc("CRM Lead", name).as_dict()
lead["fields_meta"] = get_fields_meta("CRM Lead")
lead["_form_script"] = get_form_script("CRM Lead")
lead["_assign"] = get_assigned_users("CRM Lead", lead.name)
return lead

View File

@ -5,4 +5,68 @@ frappe.ui.form.on("CRM Lead", {
refresh(frm) {
frm.add_web_link(`/crm/leads/${frm.doc.name}`, __("Open in Portal"));
},
update_total: function (frm) {
let total = 0;
let total_qty = 0;
let net_total = 0;
frm.doc.products.forEach((d) => {
total += d.amount;
total_qty += d.qty;
net_total += d.net_amount;
});
frappe.model.set_value(frm.doctype, frm.docname, "total", total);
frappe.model.set_value(
frm.doctype,
frm.docname,
"net_total",
net_total || total
);
}
});
frappe.ui.form.on("CRM Products", {
products_add: function (frm, cdt, cdn) {
frm.trigger("update_total");
},
products_remove: function (frm, cdt, cdn) {
frm.trigger("update_total");
},
product_code: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, "product_name", d.product_code);
},
rate: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
if (d.rate && d.qty) {
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
}
frm.trigger("update_total");
},
qty: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
if (d.rate && d.qty) {
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
}
frm.trigger("update_total");
},
discount_percentage: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
if (d.discount_percentage && d.amount) {
discount_amount = (d.discount_percentage / 100) * d.amount;
frappe.model.set_value(
cdt,
cdn,
"discount_amount",
discount_amount
);
frappe.model.set_value(
cdt,
cdn,
"net_amount",
d.amount - discount_amount
);
}
frm.trigger("update_total");
}
});

View File

@ -37,6 +37,12 @@
"annual_revenue",
"image",
"converted",
"products_tab",
"products",
"section_break_ggwh",
"total",
"column_break_uisv",
"net_total",
"sla_tab",
"sla",
"sla_creation",
@ -285,12 +291,47 @@
"fieldtype": "Table",
"label": "Status Change Log",
"options": "CRM Status Change Log"
},
{
"fieldname": "products_tab",
"fieldtype": "Tab Break",
"label": "Products"
},
{
"fieldname": "products",
"fieldtype": "Table",
"label": "Products",
"options": "CRM Products"
},
{
"fieldname": "section_break_ggwh",
"fieldtype": "Section Break"
},
{
"fieldname": "total",
"fieldtype": "Currency",
"label": "Total",
"options": "currency",
"read_only": 1
},
{
"fieldname": "column_break_uisv",
"fieldtype": "Column Break"
},
{
"description": "Total after discount",
"fieldname": "net_total",
"fieldtype": "Currency",
"label": "Net Total",
"options": "currency",
"read_only": 1
}
],
"grid_page_length": 50,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-01-02 22:14:01.991054",
"modified": "2025-05-14 19:51:06.184569",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Lead",
@ -331,6 +372,7 @@
"share": 1
}
],
"row_format": "Dynamic",
"sender_field": "email",
"sender_name_field": "first_name",
"show_title_field_in_link": 1,
@ -339,4 +381,4 @@
"states": [],
"title_field": "lead_name",
"track_changes": 1
}
}

View File

@ -27,9 +27,10 @@
"label": "Details"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-01-02 22:13:30.498404",
"modified": "2025-06-30 16:53:51.721752",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Lead Source",
@ -44,7 +45,7 @@
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"role": "System Manager",
"share": 1,
"write": 1
},
@ -60,6 +61,15 @@
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1
},
{
"email": 1,
"export": 1,
@ -71,7 +81,8 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Lost Reason", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,79 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:lost_reason",
"creation": "2025-06-30 16:51:31.082360",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"lost_reason",
"description"
],
"fields": [
{
"fieldname": "lost_reason",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Lost Reason",
"reqd": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-06-30 16:59:15.094049",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Lost Reason",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class CRMLostReason(Document):
pass

View File

@ -0,0 +1,30 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestCRMLostReason(UnitTestCase):
"""
Unit tests for CRMLostReason.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestCRMLostReason(IntegrationTestCase):
"""
Integration tests for CRMLostReason.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -10,6 +10,7 @@
"organization_name",
"no_of_employees",
"currency",
"exchange_rate",
"annual_revenue",
"organization_logo",
"column_break_pnpp",
@ -74,12 +75,18 @@
"fieldtype": "Link",
"label": "Address",
"options": "Address"
},
{
"description": "The rate used to convert the organization\u2019s currency to your crm's base currency (set in CRM Settings). It is set once when the currency is first added and doesn't change automatically.",
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate"
}
],
"image_field": "organization_logo",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-17 18:37:10.341062",
"modified": "2025-07-15 11:40:12.175598",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Organization",
@ -111,7 +118,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@ -4,51 +4,65 @@
import frappe
from frappe.model.document import Document
from crm.fcrm.doctype.fcrm_settings.fcrm_settings import get_exchange_rate
class CRMOrganization(Document):
@staticmethod
def default_list_data():
columns = [
{
'label': 'Organization',
'type': 'Data',
'key': 'organization_name',
'width': '16rem',
},
{
'label': 'Website',
'type': 'Data',
'key': 'website',
'width': '14rem',
},
{
'label': 'Industry',
'type': 'Link',
'key': 'industry',
'options': 'CRM Industry',
'width': '14rem',
},
{
'label': 'Annual Revenue',
'type': 'Currency',
'key': 'annual_revenue',
'width': '14rem',
},
{
'label': 'Last Modified',
'type': 'Datetime',
'key': 'modified',
'width': '8rem',
},
]
rows = [
"name",
"organization_name",
"organization_logo",
"website",
"industry",
"currency",
"annual_revenue",
"modified",
]
return {'columns': columns, 'rows': rows}
def validate(self):
self.update_exchange_rate()
def update_exchange_rate(self):
if self.has_value_changed("currency") or not self.exchange_rate:
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
exchange_rate = 1
if self.currency and self.currency != system_currency:
exchange_rate = get_exchange_rate(self.currency, system_currency)
self.db_set("exchange_rate", exchange_rate)
@staticmethod
def default_list_data():
columns = [
{
"label": "Organization",
"type": "Data",
"key": "organization_name",
"width": "16rem",
},
{
"label": "Website",
"type": "Data",
"key": "website",
"width": "14rem",
},
{
"label": "Industry",
"type": "Link",
"key": "industry",
"options": "CRM Industry",
"width": "14rem",
},
{
"label": "Annual Revenue",
"type": "Currency",
"key": "annual_revenue",
"width": "14rem",
},
{
"label": "Last Modified",
"type": "Datetime",
"key": "modified",
"width": "8rem",
},
]
rows = [
"name",
"organization_name",
"organization_logo",
"website",
"industry",
"currency",
"annual_revenue",
"modified",
]
return {"columns": columns, "rows": rows}

View File

View File

@ -0,0 +1,9 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("CRM Product", {
product_code: function (frm) {
if (!frm.doc.product_name)
frm.set_value("product_name", frm.doc.product_code);
}
});

View File

@ -0,0 +1,105 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:product_code",
"creation": "2025-04-28 11:45:09.309636",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"naming_series",
"product_code",
"product_name",
"column_break_bpdj",
"disabled",
"standard_rate",
"image",
"section_break_rtwm",
"description"
],
"fields": [
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "CRM-PROD-.YYYY.-"
},
{
"fieldname": "product_code",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Product Code",
"reqd": 1,
"unique": 1
},
{
"fieldname": "product_name",
"fieldtype": "Data",
"label": "Product Name"
},
{
"fieldname": "column_break_bpdj",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "image",
"fieldtype": "Attach Image",
"label": "Image"
},
{
"fieldname": "section_break_rtwm",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description"
},
{
"fieldname": "standard_rate",
"fieldtype": "Currency",
"label": "Standard Selling Rate"
}
],
"grid_page_length": 50,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2025-04-28 12:47:25.087957",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Product",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "product_name,description",
"show_name_in_global_search": 1,
"show_preview_popup": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "product_name",
"track_changes": 1
}

View File

@ -0,0 +1,16 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class CRMProduct(Document):
def validate(self):
self.set_product_name()
def set_product_name(self):
if not self.product_name:
self.product_name = self.product_code
else:
self.product_name = self.product_name.strip()

View File

@ -0,0 +1,29 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestCRMProduct(UnitTestCase):
"""
Unit tests for CRMProduct.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestCRMProduct(IntegrationTestCase):
"""
Integration tests for CRMProduct.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -0,0 +1,136 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-04-28 12:50:49.812915",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"product_code",
"column_break_gvbc",
"product_name",
"section_break_fnvf",
"qty",
"column_break_ajac",
"rate",
"section_break_olqb",
"discount_percentage",
"column_break_uvra",
"discount_amount",
"section_break_cnpb",
"column_break_pozr",
"amount",
"column_break_ejqw",
"net_amount"
],
"fields": [
{
"fieldname": "column_break_gvbc",
"fieldtype": "Column Break"
},
{
"fieldname": "product_name",
"fieldtype": "Data",
"label": "Product Name",
"reqd": 1
},
{
"fieldname": "section_break_fnvf",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_olqb",
"fieldtype": "Section Break"
},
{
"bold": 1,
"fieldname": "discount_percentage",
"fieldtype": "Percent",
"label": "Discount %"
},
{
"fieldname": "discount_amount",
"fieldtype": "Currency",
"label": "Discount Amount",
"options": "currency",
"read_only": 1
},
{
"fieldname": "section_break_cnpb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_pozr",
"fieldtype": "Column Break"
},
{
"bold": 1,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
"options": "currency",
"reqd": 1
},
{
"bold": 1,
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"options": "currency",
"read_only": 1
},
{
"bold": 1,
"depends_on": "discount_percentage",
"description": "Amount after discount",
"fieldname": "net_amount",
"fieldtype": "Currency",
"label": "Net Amount",
"options": "currency",
"read_only": 1
},
{
"bold": 1,
"columns": 5,
"fieldname": "product_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Product",
"options": "CRM Product"
},
{
"bold": 1,
"default": "1",
"fieldname": "qty",
"fieldtype": "Float",
"label": "Quantity"
},
{
"fieldname": "column_break_ajac",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_uvra",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ejqw",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-05-14 18:52:26.183306",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Products",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,110 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class CRMProducts(Document):
pass
def create_product_details_script(doctype):
if not frappe.db.exists("CRM Form Script", "Product Details Script for " + doctype):
script = get_product_details_script(doctype)
frappe.get_doc(
{
"doctype": "CRM Form Script",
"name": "Product Details Script for " + doctype,
"dt": doctype,
"view": "Form",
"script": script,
"enabled": 1,
"is_standard": 1,
}
).insert()
def get_product_details_script(doctype):
doctype_class = "class " + doctype.replace(" ", "")
return (
doctype_class
+ " {"
+ """
update_total() {
let total = 0
let total_qty = 0
let net_total = 0
let discount_applied = false
this.doc.products.forEach((d) => {
total += d.amount
net_total += d.net_amount
if (d.discount_percentage > 0) {
discount_applied = true
}
})
this.doc.total = total
this.doc.net_total = net_total || total
if (!net_total && discount_applied) {
this.doc.net_total = net_total
}
}
}
class CRMProducts {
products_add() {
let row = this.doc.getRow('products')
row.trigger('qty')
this.doc.trigger('update_total')
}
products_remove() {
this.doc.trigger('update_total')
}
async product_code(idx) {
let row = this.doc.getRow('products', idx)
let a = await call("frappe.client.get_value", {
doctype: "CRM Product",
filters: { name: row.product_code },
fieldname: ["product_name", "standard_rate"],
})
row.product_name = a.product_name
if (a.standard_rate && !row.rate) {
row.rate = a.standard_rate
row.trigger("rate")
}
}
qty(idx) {
let row = this.doc.getRow('products', idx)
row.amount = row.qty * row.rate
row.trigger('discount_percentage', idx)
}
rate() {
let row = this.doc.getRow('products')
row.amount = row.qty * row.rate
row.trigger('discount_percentage')
}
discount_percentage(idx) {
let row = this.doc.getRow('products', idx)
if (!row.discount_percentage) {
row.net_amount = row.amount
row.discount_amount = 0
}
if (row.discount_percentage && row.amount) {
row.discount_amount = (row.discount_percentage / 100) * row.amount
row.net_amount = row.amount - row.discount_amount
}
this.doc.trigger('update_total')
}
}"""
)

View File

@ -13,6 +13,8 @@
"column_break_mwmz",
"duration",
"last_status_change_log",
"from_type",
"to_type",
"log_owner"
],
"fields": [
@ -61,18 +63,31 @@
"fieldtype": "Link",
"label": "Owner",
"options": "User"
},
{
"fieldname": "from_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "From Type"
},
{
"fieldname": "to_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "To Type"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-01-06 13:26:40.597277",
"modified": "2025-07-13 12:37:41.278584",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Status Change Log",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@ -1,15 +1,17 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from datetime import datetime
from frappe.utils import add_to_date, get_datetime
import frappe
from frappe.model.document import Document
from frappe.utils import add_to_date, get_datetime
class CRMStatusChangeLog(Document):
pass
def get_duration(from_date, to_date):
if not isinstance(from_date, datetime):
from_date = get_datetime(from_date)
@ -18,28 +20,45 @@ def get_duration(from_date, to_date):
duration = to_date - from_date
return duration.total_seconds()
def add_status_change_log(doc):
to_status_type = frappe.db.get_value("CRM Deal Status", doc.status, "type") if doc.status else None
if not doc.is_new():
previous_status = doc.get_doc_before_save().status if doc.get_doc_before_save() else None
previous_status_type = (
frappe.db.get_value("CRM Deal Status", previous_status, "type") if previous_status else None
)
if not doc.status_change_log and previous_status:
now_minus_one_minute = add_to_date(datetime.now(), minutes=-1)
doc.append("status_change_log", {
"from": previous_status,
"to": "",
"from_date": now_minus_one_minute,
"to_date": "",
"log_owner": frappe.session.user,
})
doc.append(
"status_change_log",
{
"from": previous_status,
"from_type": previous_status_type or "",
"to": "",
"to_type": "",
"from_date": now_minus_one_minute,
"to_date": "",
"log_owner": frappe.session.user,
},
)
last_status_change = doc.status_change_log[-1]
last_status_change.to = doc.status
last_status_change.to_type = to_status_type or ""
last_status_change.to_date = datetime.now()
last_status_change.log_owner = frappe.session.user
last_status_change.duration = get_duration(last_status_change.from_date, last_status_change.to_date)
doc.append("status_change_log", {
"from": doc.status,
"to": "",
"from_date": datetime.now(),
"to_date": "",
"log_owner": frappe.session.user,
})
doc.append(
"status_change_log",
{
"from": doc.status,
"from_type": to_status_type or "",
"to": "",
"to_type": "",
"from_date": datetime.now(),
"to_date": "",
"log_owner": frappe.session.user,
},
)

View File

@ -264,7 +264,7 @@ def create_customer_in_remote_site(customer, erpnext_crm_settings):
@frappe.whitelist()
def get_crm_form_script():
return """
async function setupForm({ doc, call, $dialog, updateField, createToast }) {
async function setupForm({ doc, call, $dialog, updateField, toast }) {
let actions = [];
let is_erpnext_integration_enabled = await call("frappe.client.get_single_value", {doctype: "ERPNext CRM Settings", field: "enabled"});
if (!["Lost", "Won"].includes(doc?.status) && is_erpnext_integration_enabled) {

View File

@ -19,7 +19,8 @@
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Title"
"label": "Title",
"reqd": 1
},
{
"fieldname": "content",
@ -49,7 +50,7 @@
"link_fieldname": "note"
}
],
"modified": "2024-01-19 21:56:30.123334",
"modified": "2025-04-01 15:30:14.742001",
"modified_by": "Administrator",
"module": "FCRM",
"name": "FCRM Note",

View File

@ -7,6 +7,13 @@
"field_order": [
"defaults_tab",
"restore_defaults",
"enable_forecasting",
"currency_tab",
"currency",
"exchange_rate_provider_section",
"service_provider",
"column_break_vqck",
"access_key",
"branding_tab",
"brand_name",
"brand_logo",
@ -28,7 +35,7 @@
{
"fieldname": "defaults_tab",
"fieldtype": "Tab Break",
"label": "Defaults"
"label": "Settings"
},
{
"fieldname": "branding_tab",
@ -56,12 +63,54 @@
"fieldname": "favicon",
"fieldtype": "Attach",
"label": "Favicon"
},
{
"default": "0",
"description": "It will make deal's \"Expected Closure Date\" & \"Expected Deal Value\" mandatory to get accurate forecasting insights",
"fieldname": "enable_forecasting",
"fieldtype": "Check",
"label": "Enable Forecasting"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "currency_tab",
"fieldtype": "Tab Break",
"label": "Currency"
},
{
"fieldname": "exchange_rate_provider_section",
"fieldtype": "Section Break",
"label": "Exchange Rate Provider"
},
{
"default": "frankfurter.app",
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host"
},
{
"depends_on": "eval:doc.service_provider == 'exchangerate.host';",
"fieldname": "access_key",
"fieldtype": "Data",
"label": "Access Key",
"mandatory_depends_on": "eval:doc.service_provider == 'exchangerate.host';"
},
{
"fieldname": "column_break_vqck",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-02-20 12:38:38.088477",
"modified": "2025-07-29 11:26:50.420614",
"modified_by": "Administrator",
"module": "FCRM",
"name": "FCRM Settings",
@ -95,7 +144,8 @@
"share": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@ -2,7 +2,9 @@
# For license information, please see license.txt
import frappe
import requests
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import delete_property_setter, make_property_setter
from frappe.model.document import Document
from crm.install import after_install
@ -15,6 +17,8 @@ class FCRMSettings(Document):
def validate(self):
self.do_not_allow_to_delete_if_standard()
self.setup_forecasting()
self.make_currency_read_only()
def do_not_allow_to_delete_if_standard(self):
if not self.has_value_changed("dropdown_items"):
@ -24,8 +28,53 @@ class FCRMSettings(Document):
standard_old_items = [d.name1 for d in old_items if d.is_standard]
deleted_standard_items = set(standard_old_items) - set(standard_new_items)
if deleted_standard_items:
standard_dropdown_items = get_standard_dropdown_items()
if not deleted_standard_items.intersection(standard_dropdown_items):
return
frappe.throw(_("Cannot delete standard items {0}").format(", ".join(deleted_standard_items)))
def setup_forecasting(self):
if self.has_value_changed("enable_forecasting"):
if not self.enable_forecasting:
delete_property_setter(
"CRM Deal",
"reqd",
"expected_closure_date",
)
delete_property_setter(
"CRM Deal",
"reqd",
"expected_deal_value",
)
else:
make_property_setter(
"CRM Deal",
"expected_closure_date",
"reqd",
1 if self.enable_forecasting else 0,
"Check",
)
make_property_setter(
"CRM Deal",
"expected_deal_value",
"reqd",
1 if self.enable_forecasting else 0,
"Check",
)
def make_currency_read_only(self):
if self.currency and self.has_value_changed("currency"):
make_property_setter(
"FCRM Settings",
"currency",
"read_only",
1,
"Check",
)
def get_standard_dropdown_items():
return [item.get("name1") for item in frappe.get_hooks("standard_dropdown_items")]
def after_migrate():
@ -51,3 +100,109 @@ def sync_table(key, hook):
crm_settings.set(key, items)
crm_settings.save()
def create_forecasting_script():
if not frappe.db.exists("CRM Form Script", "Forecasting Script"):
script = get_forecasting_script()
frappe.get_doc(
{
"doctype": "CRM Form Script",
"name": "Forecasting Script",
"dt": "CRM Deal",
"view": "Form",
"script": script,
"enabled": 1,
"is_standard": 1,
}
).insert()
def get_forecasting_script():
return """class CRMDeal {
async status() {
await this.doc.trigger('updateProbability')
}
async updateProbability() {
let status = await call("frappe.client.get_value", {
doctype: "CRM Deal Status",
fieldname: "probability",
filters: { name: this.doc.status },
})
this.doc.probability = status.probability
}
}"""
def get_exchange_rate(from_currency, to_currency, date=None):
if not date:
date = "latest"
api_used = "frankfurter"
api_endpoint = f"https://api.frankfurter.app/{date}?from={from_currency}&to={to_currency}"
res = requests.get(api_endpoint, timeout=5)
if res.ok:
data = res.json()
return data["rates"][to_currency]
# Fallback to exchangerate.host if Frankfurter API fails
settings = FCRMSettings("FCRM Settings")
if settings and settings.service_provider == "exchangerate.host":
api_used = "exchangerate.host"
if not settings.access_key:
frappe.throw(
_("Access Key is required for Service Provider: {0}").format(
frappe.bold(settings.service_provider)
)
)
params = {
"access_key": settings.access_key,
"from": from_currency,
"to": to_currency,
"amount": 1,
}
if date != "latest":
params["date"] = date
api_endpoint = "https://api.exchangerate.host/convert"
res = requests.get(api_endpoint, params=params, timeout=5)
if res.ok:
data = res.json()
return data["result"]
frappe.log_error(
title="Exchange Rate Fetch Error",
message=f"Failed to fetch exchange rate from {from_currency} to {to_currency} using {api_used} API.",
)
if api_used == "frankfurter":
user = frappe.session.user
is_manager = (
"System Manager" in frappe.get_roles(user)
or "Sales Manager" in frappe.get_roles(user)
or user == "Administrator"
)
if not is_manager:
frappe.throw(
_(
"Ask your manager to set up the Exchange Rate Provider, as default provider does not support currency conversion for {0} to {1}."
).format(from_currency, to_currency)
)
else:
frappe.throw(
_(
"Setup the Exchange Rate Provider as 'Exchangerate Host' in settings, as default provider does not support currency conversion for {0} to {1}."
).format(from_currency, to_currency)
)
frappe.throw(
_(
"Failed to fetch exchange rate from {0} to {1} on {2}. Please check your internet connection or try again later."
).format(from_currency, to_currency, date)
)

View File

@ -264,22 +264,6 @@ standard_dropdown_items = [
"route": "#",
"is_standard": 1,
},
{
"name1": "support_link",
"label": "Support",
"type": "Route",
"icon": "life-buoy",
"route": "https://t.me/frappecrm",
"is_standard": 1,
},
{
"name1": "docs_link",
"label": "Docs",
"type": "Route",
"icon": "book-open",
"route": "https://docs.frappe.io/crm",
"is_standard": 1,
},
{
"name1": "toggle_theme",
"label": "Toggle theme",
@ -303,6 +287,14 @@ standard_dropdown_items = [
"route": "#",
"is_standard": 1,
},
{
"name1": "about",
"label": "About",
"type": "Route",
"icon": "info",
"route": "#",
"is_standard": 1,
},
{
"name1": "separator",
"label": "",

View File

@ -4,6 +4,9 @@ import click
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from crm.fcrm.doctype.crm_dashboard.crm_dashboard import create_default_manager_dashboard
from crm.fcrm.doctype.crm_products.crm_products import create_product_details_script
def before_install():
pass
@ -18,7 +21,10 @@ def after_install(force=False):
add_email_template_custom_fields()
add_default_industries()
add_default_lead_sources()
add_default_lost_reasons()
add_standard_dropdown_items()
add_default_scripts()
create_default_manager_dashboard(force)
frappe.db.commit()
@ -65,30 +71,44 @@ def add_default_deal_statuses():
statuses = {
"Qualification": {
"color": "gray",
"type": "Open",
"probability": 10,
"position": 1,
},
"Demo/Making": {
"color": "orange",
"type": "Ongoing",
"probability": 25,
"position": 2,
},
"Proposal/Quotation": {
"color": "blue",
"type": "Ongoing",
"probability": 50,
"position": 3,
},
"Negotiation": {
"color": "yellow",
"type": "Ongoing",
"probability": 70,
"position": 4,
},
"Ready to Close": {
"color": "purple",
"type": "Ongoing",
"probability": 90,
"position": 5,
},
"Won": {
"color": "green",
"type": "Won",
"probability": 100,
"position": 6,
},
"Lost": {
"color": "red",
"type": "Lost",
"probability": 0,
"position": 7,
},
}
@ -100,6 +120,8 @@ def add_default_deal_statuses():
doc = frappe.new_doc("CRM Deal Status")
doc.deal_status = status
doc.color = statuses[status]["color"]
doc.type = statuses[status]["type"]
doc.probability = statuses[status]["probability"]
doc.position = statuses[status]["position"]
doc.insert()
@ -340,6 +362,44 @@ def add_default_lead_sources():
doc.insert()
def add_default_lost_reasons():
lost_reasons = [
{
"reason": "Pricing",
"description": "The prospect found the pricing to be too high or not competitive.",
},
{"reason": "Competition", "description": "The prospect chose a competitor's product or service."},
{
"reason": "Budget Constraints",
"description": "The prospect did not have the budget to proceed with the purchase.",
},
{
"reason": "Missing Features",
"description": "The prospect felt that the product or service was missing key features they needed.",
},
{
"reason": "Long Sales Cycle",
"description": "The sales process took too long, leading to loss of interest.",
},
{
"reason": "No Decision-Maker",
"description": "The prospect was not the decision-maker and could not proceed.",
},
{"reason": "Unresponsive Prospect", "description": "The prospect did not respond to follow-ups."},
{"reason": "Poor Fit", "description": "The prospect was not a good fit for the product or service."},
{"reason": "Other", "description": ""},
]
for reason in lost_reasons:
if frappe.db.exists("CRM Lost Reason", reason["reason"]):
continue
doc = frappe.new_doc("CRM Lost Reason")
doc.lost_reason = reason["reason"]
doc.description = reason["description"]
doc.insert()
def add_standard_dropdown_items():
crm_settings = frappe.get_single("FCRM Settings")
@ -353,3 +413,11 @@ def add_standard_dropdown_items():
crm_settings.append("dropdown_items", item)
crm_settings.save()
def add_default_scripts():
from crm.fcrm.doctype.fcrm_settings.fcrm_settings import create_forecasting_script
for doctype in ["CRM Lead", "CRM Deal"]:
create_product_details_script(doctype)
create_forecasting_script()

View File

@ -35,7 +35,7 @@ def set_default_calling_medium(medium):
frappe.get_doc(
{
"doctype": "CRM Telephony Agent",
"agent": frappe.session.user,
"user": frappe.session.user,
"default_medium": medium,
}
).insert(ignore_permissions=True)
@ -110,12 +110,12 @@ def get_contact_by_phone_number(phone_number):
number = parse_phone_number(phone_number)
if number.get("is_valid"):
return get_contact(number.get("national_number"))
return get_contact(number.get("national_number"), number.get("country"))
else:
return get_contact(phone_number, exact_match=True)
return get_contact(phone_number, number.get("country"), exact_match=True)
def get_contact(phone_number, exact_match=False):
def get_contact(phone_number, country="IN", exact_match=False):
if not phone_number:
return {"mobile_no": phone_number}
@ -149,11 +149,11 @@ def get_contact(phone_number, exact_match=False):
deal = frappe.db.get_value(
"CRM Contacts", {"contact": contact.name, "is_primary": 1}, "parent"
)
if are_same_phone_number(contact.mobile_no, phone_number, validate=not exact_match):
if are_same_phone_number(contact.mobile_no, phone_number, country, validate=not exact_match):
contact["deal"] = deal
return contact
# Else, return the first contact
if are_same_phone_number(contacts[0].mobile_no, phone_number, validate=not exact_match):
if are_same_phone_number(contacts[0].mobile_no, phone_number, country, validate=not exact_match):
return contacts[0]
# Else, Check if the number is associated with a lead
@ -173,7 +173,7 @@ def get_contact(phone_number, exact_match=False):
if len(leads):
for lead in leads:
if are_same_phone_number(lead.mobile_no, phone_number, validate=not exact_match):
if are_same_phone_number(lead.mobile_no, phone_number, country, validate=not exact_match):
lead["lead"] = lead.name
lead["full_name"] = lead.lead_name
return lead

View File

@ -242,19 +242,18 @@ def get_call_log_status(call_payload, direction="inbound"):
elif status == "failed":
return "Failed"
status = call_payload.get("DialCallStatus")
call_type = call_payload.get("CallType")
dial_call_status = call_payload.get("DialCallStatus")
status = call_payload.get("DialCallStatus") or call_payload.get("Status")
if call_type == "incomplete" and dial_call_status == "no-answer":
if call_type == "incomplete" and status == "no-answer":
status = "No Answer"
elif call_type == "client-hangup" and dial_call_status == "canceled":
elif call_type == "client-hangup" and status == "canceled":
status = "Canceled"
elif call_type == "incomplete" and dial_call_status == "failed":
elif call_type == "incomplete" and status == "failed":
status = "Failed"
elif call_type == "completed":
status = "Completed"
elif dial_call_status == "busy":
elif status == "busy":
status = "Ringing"
return status

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5582
crm/locale/cs.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5582
crm/locale/id.po Normal file

File diff suppressed because it is too large Load Diff

5582
crm/locale/it.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5582
crm/locale/nl.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5582
crm/locale/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5582
crm/locale/sr.po Normal file

File diff suppressed because it is too large Load Diff

5582
crm/locale/sr_CS.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5582
crm/locale/vi.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -11,4 +11,7 @@ crm.patches.v1_0.create_default_fields_layout #22/01/2025
crm.patches.v1_0.create_default_sidebar_fields_layout
crm.patches.v1_0.update_deal_quick_entry_layout
crm.patches.v1_0.update_layouts_to_new_format
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
crm.patches.v1_0.create_default_scripts # 13-06-2025
crm.patches.v1_0.update_deal_status_probabilities
crm.patches.v1_0.update_deal_status_type

View File

@ -0,0 +1,5 @@
from crm.install import add_default_scripts
def execute():
add_default_scripts()

View File

@ -0,0 +1,24 @@
import frappe
def execute():
deal_statuses = frappe.get_all("CRM Deal Status", fields=["name", "probability", "deal_status"])
for status in deal_statuses:
if status.probability is None or status.probability == 0:
if status.deal_status == "Qualification":
probability = 10
elif status.deal_status == "Demo/Making":
probability = 25
elif status.deal_status == "Proposal/Quotation":
probability = 50
elif status.deal_status == "Negotiation":
probability = 70
elif status.deal_status == "Ready to Close":
probability = 90
elif status.deal_status == "Won":
probability = 100
else:
probability = 0
frappe.db.set_value("CRM Deal Status", status.name, "probability", probability)

View File

@ -0,0 +1,44 @@
import frappe
def execute():
deal_statuses = frappe.get_all("CRM Deal Status", fields=["name", "type", "deal_status"])
openStatuses = ["New", "Open", "Unassigned", "Qualification"]
ongoingStatuses = [
"Demo/Making",
"Proposal/Quotation",
"Negotiation",
"Ready to Close",
"Demo Scheduled",
"Follow Up",
]
onHoldStatuses = ["On Hold", "Paused", "Stalled", "Awaiting Reply"]
wonStatuses = ["Won", "Closed Won", "Successful", "Completed"]
lostStatuses = [
"Lost",
"Closed",
"Closed Lost",
"Junk",
"Unqualified",
"Disqualified",
"Cancelled",
"No Response",
]
for status in deal_statuses:
if not status.type or status.type is None or status.type == "Open":
if status.deal_status in openStatuses:
type = "Open"
elif status.deal_status in ongoingStatuses:
type = "Ongoing"
elif status.deal_status in onHoldStatuses:
type = "On Hold"
elif status.deal_status in wonStatuses:
type = "Won"
elif status.deal_status in lostStatuses:
type = "Lost"
else:
type = "Ongoing"
frappe.db.set_value("CRM Deal Status", status.name, "type", type)

View File

@ -1,4 +1,4 @@
<h2>You have been invited to join Frappe CRM</h2>
<p>You have been invited to join Frappe CRM</p>
<p>
<a class="btn btn-primary" href="{{ invite_link }}">Accept Invitation</a>
</p>

View File

@ -1,12 +1,13 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import click
import frappe
def before_uninstall():
delete_email_template_custom_fields()
def delete_email_template_custom_fields():
if frappe.get_meta("Email Template").has_field("enabled"):
click.secho("* Uninstalling Custom Fields from Email Template")
@ -19,4 +20,4 @@ def delete_email_template_custom_fields():
for fieldname in fieldnames:
frappe.db.delete("Custom Field", {"name": "Email Template-" + fieldname})
frappe.clear_cache(doctype="Email Template")
frappe.clear_cache(doctype="Email Template")

View File

@ -1,4 +1,11 @@
import functools
import frappe
import phonenumbers
import requests
from frappe import _
from frappe.model.docstatus import DocStatus
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.utils import floor
from phonenumbers import NumberParseException
from phonenumbers import PhoneNumberFormat as PNF
@ -93,3 +100,170 @@ def seconds_to_duration(seconds):
return f"{seconds}s"
else:
return "0s"
# Extracted from frappe core frappe/model/delete_doc.py/check_if_doc_is_linked
def get_linked_docs(doc, method="Delete"):
from frappe.model.rename_doc import get_link_fields
link_fields = get_link_fields(doc.doctype)
ignored_doctypes = set()
if method == "Cancel" and (doc_ignore_flags := doc.get("ignore_linked_doctypes")):
ignored_doctypes.update(doc_ignore_flags)
if method == "Delete":
ignored_doctypes.update(frappe.get_hooks("ignore_links_on_delete"))
docs = []
for lf in link_fields:
link_dt, link_field, issingle = lf["parent"], lf["fieldname"], lf["issingle"]
if link_dt in ignored_doctypes or (link_field == "amended_from" and method == "Cancel"):
continue
try:
meta = frappe.get_meta(link_dt)
except frappe.DoesNotExistError:
frappe.clear_last_message()
# This mostly happens when app do not remove their customizations, we shouldn't
# prevent link checks from failing in those cases
continue
if issingle:
if frappe.db.get_single_value(link_dt, link_field) == doc.name:
docs.append({"doc": doc.name, "link_dt": link_dt, "link_field": link_field})
continue
fields = ["name", "docstatus"]
if meta.istable:
fields.extend(["parent", "parenttype"])
for item in frappe.db.get_values(link_dt, {link_field: doc.name}, fields, as_dict=True):
# available only in child table cases
item_parent = getattr(item, "parent", None)
linked_parent_doctype = item.parenttype if item_parent else link_dt
if linked_parent_doctype in ignored_doctypes:
continue
if method != "Delete" and (method != "Cancel" or not DocStatus(item.docstatus).is_submitted()):
# don't raise exception if not
# linked to a non-cancelled doc when deleting or to a submitted doc when cancelling
continue
elif link_dt == doc.doctype and (item_parent or item.name) == doc.name:
# don't raise exception if not
# linked to same item or doc having same name as the item
continue
else:
reference_docname = item_parent or item.name
docs.append(
{
"doc": doc.name,
"reference_doctype": linked_parent_doctype,
"reference_docname": reference_docname,
}
)
return docs
# Extracted from frappe core frappe/model/delete_doc.py/check_if_doc_is_dynamically_linked
def get_dynamic_linked_docs(doc, method="Delete"):
docs = []
for df in get_dynamic_link_map().get(doc.doctype, []):
ignore_linked_doctypes = doc.get("ignore_linked_doctypes") or []
if df.parent in frappe.get_hooks("ignore_links_on_delete") or (
df.parent in ignore_linked_doctypes and method == "Cancel"
):
# don't check for communication and todo!
continue
meta = frappe.get_meta(df.parent)
if meta.issingle:
# dynamic link in single doc
refdoc = frappe.db.get_singles_dict(df.parent)
if (
refdoc.get(df.options) == doc.doctype
and refdoc.get(df.fieldname) == doc.name
and (
# linked to an non-cancelled doc when deleting
(method == "Delete" and not DocStatus(refdoc.docstatus).is_cancelled())
# linked to a submitted doc when cancelling
or (method == "Cancel" and DocStatus(refdoc.docstatus).is_submitted())
)
):
docs.append({"doc": doc.name, "reference_doctype": df.parent, "reference_docname": df.parent})
else:
# dynamic link in table
df["table"] = ", `parent`, `parenttype`, `idx`" if meta.istable else ""
for refdoc in frappe.db.sql(
"""select `name`, `docstatus` {table} from `tab{parent}` where
`{options}`=%s and `{fieldname}`=%s""".format(**df),
(doc.doctype, doc.name),
as_dict=True,
):
# linked to an non-cancelled doc when deleting
# or linked to a submitted doc when cancelling
if (method == "Delete" and not DocStatus(refdoc.docstatus).is_cancelled()) or (
method == "Cancel" and DocStatus(refdoc.docstatus).is_submitted()
):
reference_doctype = refdoc.parenttype if meta.istable else df.parent
reference_docname = refdoc.parent if meta.istable else refdoc.name
if reference_doctype in frappe.get_hooks("ignore_links_on_delete") or (
reference_doctype in ignore_linked_doctypes and method == "Cancel"
):
# don't check for communication and todo!
continue
at_position = f"at Row: {refdoc.idx}" if meta.istable else ""
docs.append(
{
"doc": doc.name,
"reference_doctype": reference_doctype,
"reference_docname": reference_docname,
"at_position": at_position,
}
)
return docs
def is_admin(user: str | None = None) -> bool:
"""
Check whether `user` is an admin
:param user: User to check against, defaults to current user
:return: Whether `user` is an admin
"""
user = user or frappe.session.user
return user == "Administrator"
def is_sales_user(user: str | None = None) -> bool:
"""
Check whether `user` is an agent
:param user: User to check against, defaults to current user
:return: Whether `user` is an agent
"""
user = user or frappe.session.user
return is_admin() or "Sales Manager" in frappe.get_roles(user) or "Sales User" in frappe.get_roles(user)
def sales_user_only(fn):
"""Decorator to validate if user is an agent."""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
if not is_sales_user():
frappe.throw(
msg=_("You are not permitted to access this resource."),
title=_("Not Allowed"),
exc=frappe.PermissionError,
)
return fn(*args, **kwargs)
return wrapper

View File

@ -1,8 +1,10 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# GNU GPLv3 License. See license.txt
import os
import subprocess
import frappe
from frappe import safe_decode
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
from frappe.utils import cint, get_system_timezone
from frappe.utils.telemetry import capture
@ -49,3 +51,15 @@ def get_boot():
def get_default_route():
return "/crm"
def run_git_command(command):
try:
with open(os.devnull, "wb") as null_stream:
result = subprocess.check_output(command, shell=True, stdin=null_stream, stderr=null_stream)
return safe_decode(result).strip()
except Exception:
frappe.log_error(
title="Git Command Error",
)
return ""

View File

@ -1,3 +1,8 @@
files:
- source: /crm/locale/main.pot
translation: /crm/locale/%two_letters_code%.po
pull_request_title: "chore: sync translations from crowdin"
pull_request_labels:
- translation
commit_message: "chore: %language% translations"
append_commit_message: false

View File

@ -8,21 +8,21 @@ else
echo "Creating new bench..."
fi
bench init --skip-redis-config-generation frappe-bench
bench init --skip-redis-config-generation frappe-bench --version version-15
cd frappe-bench
# Use containers instead of localhost
bench set-mariadb-host mariadb
bench set-redis-cache-host redis:6379
bench set-redis-queue-host redis:6379
bench set-redis-socketio-host redis:6379
bench set-redis-cache-host redis://redis:6379
bench set-redis-queue-host redis://redis:6379
bench set-redis-socketio-host redis://redis:6379
# Remove redis, watch from Procfile
sed -i '/redis/d' ./Procfile
sed -i '/watch/d' ./Procfile
bench get-app crm --branch develop
bench get-app crm --branch main
bench new-site crm.localhost \
--force \
@ -32,8 +32,9 @@ bench new-site crm.localhost \
bench --site crm.localhost install-app crm
bench --site crm.localhost set-config developer_mode 1
bench --site crm.localhost clear-cache
bench --site crm.localhost set-config mute_emails 1
bench --site crm.localhost set-config server_script_enabled 1
bench --site crm.localhost clear-cache
bench use crm.localhost
bench start

@ -1 +1 @@
Subproject commit 3423aa5b5c38d3a1b143ae8ab08cbde7360f9a7c
Subproject commit b295b54aaa3a2a22f455df819d87433dc6b9ff6a

View File

@ -8,9 +8,12 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AboutModal: typeof import('./src/components/Modals/AboutModal.vue')['default']
Activities: typeof import('./src/components/Activities/Activities.vue')['default']
ActivityHeader: typeof import('./src/components/Activities/ActivityHeader.vue')['default']
ActivityIcon: typeof import('./src/components/Icons/ActivityIcon.vue')['default']
AddChartModal: typeof import('./src/components/Dashboard/AddChartModal.vue')['default']
AddExistingUserModal: typeof import('./src/components/Modals/AddExistingUserModal.vue')['default']
AddressIcon: typeof import('./src/components/Icons/AddressIcon.vue')['default']
AddressModal: typeof import('./src/components/Modals/AddressModal.vue')['default']
AllModals: typeof import('./src/components/Activities/AllModals.vue')['default']
@ -29,6 +32,8 @@ declare module 'vue' {
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
BrandSettings: typeof import('./src/components/Settings/General/BrandSettings.vue')['default']
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
CallLogDetailModal: typeof import('./src/components/Modals/CallLogDetailModal.vue')['default']
@ -37,6 +42,7 @@ declare module 'vue' {
CallUI: typeof import('./src/components/Telephony/CallUI.vue')['default']
CameraIcon: typeof import('./src/components/Icons/CameraIcon.vue')['default']
CertificateIcon: typeof import('./src/components/Icons/CertificateIcon.vue')['default']
ChangePasswordModal: typeof import('./src/components/Modals/ChangePasswordModal.vue')['default']
CheckCircleIcon: typeof import('./src/components/Icons/CheckCircleIcon.vue')['default']
CheckIcon: typeof import('./src/components/Icons/CheckIcon.vue')['default']
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default']
@ -52,16 +58,22 @@ declare module 'vue' {
ContactsIcon: typeof import('./src/components/Icons/ContactsIcon.vue')['default']
ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default']
ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default']
ConvertToDealModal: typeof import('./src/components/Modals/ConvertToDealModal.vue')['default']
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
CurrencySettings: typeof import('./src/components/Settings/General/CurrencySettings.vue')['default']
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
DashboardItem: typeof import('./src/components/Dashboard/DashboardItem.vue')['default']
DataFields: typeof import('./src/components/Activities/DataFields.vue')['default']
DataFieldsModal: typeof import('./src/components/Modals/DataFieldsModal.vue')['default']
DealModal: typeof import('./src/components/Modals/DealModal.vue')['default']
DealsIcon: typeof import('./src/components/Icons/DealsIcon.vue')['default']
DealsListView: typeof import('./src/components/ListViews/DealsListView.vue')['default']
DeclinedCallIcon: typeof import('./src/components/Icons/DeclinedCallIcon.vue')['default']
DeleteLinkedDocModal: typeof import('./src/components/DeleteLinkedDocModal.vue')['default']
DesendingIcon: typeof import('./src/components/Icons/DesendingIcon.vue')['default']
DesktopLayout: typeof import('./src/components/Layouts/DesktopLayout.vue')['default']
DetailsIcon: typeof import('./src/components/Icons/DetailsIcon.vue')['default']
@ -75,19 +87,28 @@ declare module 'vue' {
DropdownItem: typeof import('./src/components/DropdownItem.vue')['default']
DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default']
DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default']
EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default']
EditIcon: typeof import('./src/components/Icons/EditIcon.vue')['default']
EditValueModal: typeof import('./src/components/Modals/EditValueModal.vue')['default']
Email2Icon: typeof import('./src/components/Icons/Email2Icon.vue')['default']
EmailAccountCard: typeof import('./src/components/Settings/EmailAccountCard.vue')['default']
EmailAccountList: typeof import('./src/components/Settings/EmailAccountList.vue')['default']
EmailAdd: typeof import('./src/components/Settings/EmailAdd.vue')['default']
EmailArea: typeof import('./src/components/Activities/EmailArea.vue')['default']
EmailAtIcon: typeof import('./src/components/Icons/EmailAtIcon.vue')['default']
EmailConfig: typeof import('./src/components/Settings/EmailConfig.vue')['default']
EmailContent: typeof import('./src/components/Activities/EmailContent.vue')['default']
EmailEdit: typeof import('./src/components/Settings/EmailEdit.vue')['default']
EmailEditor: typeof import('./src/components/EmailEditor.vue')['default']
EmailIcon: typeof import('./src/components/Icons/EmailIcon.vue')['default']
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
EmailProviderIcon: typeof import('./src/components/Settings/EmailProviderIcon.vue')['default']
EmailTemplateIcon: typeof import('./src/components/Icons/EmailTemplateIcon.vue')['default']
EmailTemplatePage: typeof import('./src/components/Settings/EmailTemplate/EmailTemplatePage.vue')['default']
EmailTemplates: typeof import('./src/components/Settings/EmailTemplate/EmailTemplates.vue')['default']
EmailTemplateSelectorModal: typeof import('./src/components/Modals/EmailTemplateSelectorModal.vue')['default']
EmailTemplatesListView: typeof import('./src/components/ListViews/EmailTemplatesListView.vue')['default']
ERPNextIcon: typeof import('./src/components/Icons/ERPNextIcon.vue')['default']
ERPNextSettings: typeof import('./src/components/Settings/ERPNextSettings.vue')['default']
ErrorPage: typeof import('./src/components/ErrorPage.vue')['default']
ExotelCallUI: typeof import('./src/components/Telephony/ExotelCallUI.vue')['default']
ExportIcon: typeof import('./src/components/Icons/ExportIcon.vue')['default']
ExternalLinkIcon: typeof import('./src/components/Icons/ExternalLinkIcon.vue')['default']
@ -106,9 +127,12 @@ declare module 'vue' {
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
Filter: typeof import('./src/components/Filter.vue')['default']
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
GeneralSettings: typeof import('./src/components/Settings/GeneralSettings.vue')['default']
GeneralSettings: typeof import('./src/components/Settings/General/GeneralSettings.vue')['default']
GeneralSettingsPage: typeof import('./src/components/Settings/General/GeneralSettingsPage.vue')['default']
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
GridFieldsEditorModal: typeof import('./src/components/Controls/GridFieldsEditorModal.vue')['default']
@ -118,6 +142,7 @@ declare module 'vue' {
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
HomeActions: typeof import('./src/components/Settings/General/HomeActions.vue')['default']
Icon: typeof import('./src/components/Icon.vue')['default']
IconPicker: typeof import('./src/components/IconPicker.vue')['default']
ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default']
@ -125,21 +150,28 @@ declare module 'vue' {
InboxIcon: typeof import('./src/components/Icons/InboxIcon.vue')['default']
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
InviteMemberPage: typeof import('./src/components/Settings/InviteMemberPage.vue')['default']
InviteUserPage: typeof import('./src/components/Settings/InviteUserPage.vue')['default']
KanbanIcon: typeof import('./src/components/Icons/KanbanIcon.vue')['default']
KanbanSettings: typeof import('./src/components/Kanban/KanbanSettings.vue')['default']
KanbanView: typeof import('./src/components/Kanban/KanbanView.vue')['default']
KeyboardShortcut: typeof import('./src/components/KeyboardShortcut.vue')['default']
LayoutHeader: typeof import('./src/components/LayoutHeader.vue')['default']
LeadModal: typeof import('./src/components/Modals/LeadModal.vue')['default']
LeadsIcon: typeof import('./src/components/Icons/LeadsIcon.vue')['default']
LeadsListView: typeof import('./src/components/ListViews/LeadsListView.vue')['default']
LightningIcon: typeof import('./src/components/Icons/LightningIcon.vue')['default']
Link: typeof import('./src/components/Controls/Link.vue')['default']
LinkedDocsListView: typeof import('./src/components/ListViews/LinkedDocsListView.vue')['default']
LinkIcon: typeof import('./src/components/Icons/LinkIcon.vue')['default']
ListBulkActions: typeof import('./src/components/ListBulkActions.vue')['default']
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
LucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
LucidePenLine: typeof import('~icons/lucide/pen-line')['default']
LucideRefreshCcw: typeof import('~icons/lucide/refresh-ccw')['default']
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
@ -149,10 +181,13 @@ declare module 'vue' {
MobileLayout: typeof import('./src/components/Layouts/MobileLayout.vue')['default']
MobileSidebar: typeof import('./src/components/Mobile/MobileSidebar.vue')['default']
MoneyIcon: typeof import('./src/components/Icons/MoneyIcon.vue')['default']
MultiActionButton: typeof import('./src/components/MultiActionButton.vue')['default']
MultipleAvatar: typeof import('./src/components/MultipleAvatar.vue')['default']
MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default']
MultiSelectUserInput: typeof import('./src/components/Controls/MultiSelectUserInput.vue')['default']
MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default']
NestedPopover: typeof import('./src/components/NestedPopover.vue')['default']
NewEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/NewEmailTemplate.vue')['default']
NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default']
NoteIcon: typeof import('./src/components/Icons/NoteIcon.vue')['default']
NoteModal: typeof import('./src/components/Modals/NoteModal.vue')['default']
@ -162,6 +197,7 @@ declare module 'vue' {
OrganizationsIcon: typeof import('./src/components/Icons/OrganizationsIcon.vue')['default']
OrganizationsListView: typeof import('./src/components/ListViews/OrganizationsListView.vue')['default']
OutboundCallIcon: typeof import('./src/components/Icons/OutboundCallIcon.vue')['default']
Password: typeof import('./src/components/Controls/Password.vue')['default']
PauseIcon: typeof import('./src/components/Icons/PauseIcon.vue')['default']
PhoneIcon: typeof import('./src/components/Icons/PhoneIcon.vue')['default']
PinIcon: typeof import('./src/components/Icons/PinIcon.vue')['default']
@ -169,7 +205,6 @@ declare module 'vue' {
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
ProfileImageEditor: typeof import('./src/components/Settings/ProfileImageEditor.vue')['default']
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
@ -196,6 +231,7 @@ declare module 'vue' {
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
SortBy: typeof import('./src/components/SortBy.vue')['default']
SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default']
SquareAsterisk: typeof import('./src/components/Icons/SquareAsterisk.vue')['default']
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']
TableMultiselectInput: typeof import('./src/components/Controls/TableMultiselectInput.vue')['default']
@ -206,12 +242,14 @@ declare module 'vue' {
TaskPriorityIcon: typeof import('./src/components/Icons/TaskPriorityIcon.vue')['default']
TasksListView: typeof import('./src/components/ListViews/TasksListView.vue')['default']
TaskStatusIcon: typeof import('./src/components/Icons/TaskStatusIcon.vue')['default']
TelegramIcon: typeof import('./src/components/Icons/TelegramIcon.vue')['default']
TelephonySettings: typeof import('./src/components/Settings/TelephonySettings.vue')['default']
TerritoryIcon: typeof import('./src/components/Icons/TerritoryIcon.vue')['default']
TwilioCallUI: typeof import('./src/components/Telephony/TwilioCallUI.vue')['default']
UnpinIcon: typeof import('./src/components/Icons/UnpinIcon.vue')['default']
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
UserDropdown: typeof import('./src/components/UserDropdown.vue')['default']
Users: typeof import('./src/components/Settings/Users.vue')['default']
ViewBreadcrumbs: typeof import('./src/components/ViewBreadcrumbs.vue')['default']
ViewControls: typeof import('./src/components/ViewControls.vue')['default']
ViewModal: typeof import('./src/components/Modals/ViewModal.vue')['default']

View File

@ -2,6 +2,7 @@
"name": "crm-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --base=/assets/crm/frontend/ && yarn copy-html-entry",
@ -9,9 +10,10 @@
"serve": "vite preview"
},
"dependencies": {
"@tiptap/extension-paragraph": "^2.12.0",
"@twilio/voice-sdk": "^2.10.2",
"@vueuse/integrations": "^10.3.0",
"frappe-ui": "^0.1.121",
"frappe-ui": "^0.1.171",
"gemoji": "^8.1.0",
"lodash": "^4.17.21",
"mime": "^4.0.1",

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},

View File

@ -1,16 +1,17 @@
<template>
<Layout v-if="session().isLoggedIn">
<router-view />
</Layout>
<Dialogs />
<Toasts />
<FrappeUIProvider>
<Layout v-if="session().isLoggedIn">
<router-view :key="$route.fullPath"/>
</Layout>
<Dialogs />
</FrappeUIProvider>
</template>
<script setup>
import { Dialogs } from '@/utils/dialogs'
import { sessionStore as session } from '@/stores/session'
import { setTheme } from '@/stores/theme'
import { Toasts, setConfig } from 'frappe-ui'
import { FrappeUIProvider, setConfig } from 'frappe-ui'
import { computed, defineAsyncComponent, onMounted } from 'vue'
const MobileLayout = defineAsyncComponent(

View File

@ -250,14 +250,14 @@
</span>
<span v-if="activity.type">{{ __(activity.type) }}</span>
<span
v-if="activity.data.field_label"
v-if="activity.data?.field_label"
class="max-w-xs truncate font-medium text-ink-gray-8"
>
{{ __(activity.data.field_label) }}
</span>
<span v-if="activity.value">{{ __(activity.value) }}</span>
<span
v-if="activity.data.old_value"
v-if="activity.data?.old_value"
class="max-w-xs font-medium text-ink-gray-8"
>
<div
@ -273,7 +273,7 @@
</span>
<span v-if="activity.to">{{ __('to') }}</span>
<span
v-if="activity.data.value"
v-if="activity.data?.value"
class="max-w-xs font-medium text-ink-gray-8"
>
<div
@ -307,7 +307,7 @@
>
<div class="inline-flex flex-wrap gap-1 text-ink-gray-5">
<span
v-if="activity.data.field_label"
v-if="activity.data?.field_label"
class="max-w-xs truncate text-ink-gray-5"
>
{{ __(activity.data.field_label) }}
@ -320,7 +320,7 @@
{{ startCase(__(activity.type)) }}
</span>
<span
v-if="activity.data.old_value"
v-if="activity.data?.old_value"
class="max-w-xs font-medium text-ink-gray-8"
>
<div
@ -336,7 +336,7 @@
</span>
<span v-if="activity.to">{{ __('to') }}</span>
<span
v-if="activity.data.value"
v-if="activity.data?.value"
class="max-w-xs font-medium text-ink-gray-8"
>
<div
@ -365,7 +365,12 @@
</div>
</div>
<div v-else-if="title == 'Data'" class="h-full flex flex-col px-3 sm:px-10">
<DataFields :doctype="doctype" :docname="doc.data.name" />
<DataFields
:doctype="doctype"
:docname="docname"
@beforeSave="(data) => emit('beforeSave', data)"
@afterSave="(data) => emit('afterSave', data)"
/>
</div>
<div
v-else
@ -373,11 +378,7 @@
>
<component :is="emptyTextIcon" class="h-10 w-10" />
<span>{{ __(emptyText) }}</span>
<Button
v-if="title == 'Calls'"
:label="__('Make a Call')"
@click="makeCall(doc.data.mobile_no)"
/>
<MultiActionButton v-if="title == 'Calls'" :options="callActions" />
<Button
v-else-if="title == 'Notes'"
:label="__('Create Note')"
@ -437,10 +438,9 @@
:doc="doc"
/>
<FilesUploader
v-if="doc.data?.name"
v-model="showFilesUploader"
:doctype="doctype"
:docname="doc.data.name"
:docname="docname"
@after="
() => {
all_activities.reload()
@ -470,6 +470,7 @@ import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
import WhatsAppArea from '@/components/Activities/WhatsAppArea.vue'
import WhatsAppBox from '@/components/Activities/WhatsAppBox.vue'
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
import MultiActionButton from '@/components/MultiActionButton.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue'
import DotIcon from '@/components/Icons/DotIcon.vue'
@ -487,7 +488,8 @@ import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
import { timeAgo, formatDate, startCase } from '@/utils'
import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users'
import { whatsappEnabled } from '@/composables/settings'
import { whatsappEnabled, callEnabled } from '@/composables/settings'
import { useDocument } from '@/data/document'
import { capture } from '@/telemetry'
import { Button, Tooltip, createResource } from 'frappe-ui'
import { useElementVisibility } from '@vueuse/core'
@ -511,18 +513,27 @@ const props = defineProps({
type: String,
default: 'CRM Lead',
},
docname: {
type: String,
default: '',
},
tabs: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['beforeSave', 'afterSave'])
const route = useRoute()
const doc = defineModel()
const reload = defineModel('reload')
const tabIndex = defineModel('tabIndex')
const { document: _document } = useDocument(props.doctype, props.docname)
const doc = computed(() => _document.doc || {})
const reload_email = ref(false)
const modalRef = ref(null)
const showFilesUploader = ref(false)
@ -538,8 +549,8 @@ const changeTabTo = (tabName) => {
const all_activities = createResource({
url: 'crm.api.activities.get_activities',
params: { name: doc.value.data.name },
cache: ['activity', doc.value.data.name],
params: { name: props.docname },
cache: ['activity', props.docname],
auto: true,
transform: ([versions, calls, notes, tasks, attachments]) => {
return { versions, calls, notes, tasks, attachments }
@ -550,12 +561,12 @@ const showWhatsappTemplates = ref(false)
const whatsappMessages = createResource({
url: 'crm.api.whatsapp.get_whatsapp_messages',
cache: ['whatsapp_messages', doc.value.data.name],
cache: ['whatsapp_messages', props.docname],
params: {
reference_doctype: props.doctype,
reference_name: doc.value.data.name,
reference_name: props.docname,
},
auto: true,
auto: whatsappEnabled.value,
transform: (data) => sortByCreation(data),
onSuccess: () => nextTick(() => scroll()),
})
@ -568,7 +579,7 @@ onMounted(() => {
$socket.on('whatsapp_message', (data) => {
if (
data.reference_doctype === props.doctype &&
data.reference_name === doc.value.data.name
data.reference_name === props.docname
) {
whatsappMessages.reload()
}
@ -590,8 +601,8 @@ function sendTemplate(template) {
url: 'crm.api.whatsapp.send_whatsapp_template',
params: {
reference_doctype: props.doctype,
reference_name: doc.value.data.name,
to: doc.value.data.mobile_no,
reference_name: props.docname,
to: doc.value.mobile_no,
template,
},
auto: true,
@ -763,6 +774,7 @@ const whatsappBox = ref(null)
watch([reload, reload_email], ([reload_value, reload_email_value]) => {
if (reload_value || reload_email_value) {
all_activities.reload()
_document.reload()
reload.value = false
reload_email.value = false
}
@ -785,5 +797,23 @@ function scroll(hash) {
}, 500)
}
defineExpose({ emailBox, all_activities })
const callActions = computed(() => {
let actions = [
{
label: __('Log a Call'),
onClick: () => modalRef.value.createCallLog(),
},
{
label: __('Make a Call'),
onClick: () => makeCall(doc.value.mobile_no),
condition: () => callEnabled.value,
},
]
return actions.filter((action) =>
action.condition ? action.condition() : true,
)
})
defineExpose({ emailBox, all_activities, changeTabTo })
</script>

View File

@ -26,16 +26,11 @@
</template>
<span>{{ __('New Comment') }}</span>
</Button>
<Button
<MultiActionButton
v-else-if="title == 'Calls'"
variant="solid"
@click="makeCall(doc.data.mobile_no)"
>
<template #prefix>
<PhoneIcon class="h-4 w-4" />
</template>
<span>{{ __('Make a Call') }}</span>
</Button>
:options="callActions"
/>
<Button
v-else-if="title == 'Notes'"
variant="solid"
@ -97,6 +92,7 @@
</div>
</template>
<script setup>
import MultiActionButton from '@/components/MultiActionButton.vue'
import Email2Icon from '@/components/Icons/Email2Icon.vue'
import CommentIcon from '@/components/Icons/CommentIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
@ -136,10 +132,15 @@ const defaultActions = computed(() => {
label: __('New Comment'),
onClick: () => (props.emailBox.showComment = true),
},
{
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
label: __('Log a Call'),
onClick: () => props.modalRef.createCallLog(),
},
{
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
label: __('Make a Call'),
onClick: () => makeCall(props.doc.data.mobile_no),
onClick: () => makeCall(props.doc.mobile_no),
condition: () => callEnabled.value,
},
{
@ -172,4 +173,24 @@ const defaultActions = computed(() => {
function getTabIndex(name) {
return props.tabs.findIndex((tab) => tab.name === name)
}
const callActions = computed(() => {
let actions = [
{
label: __('Log a Call'),
icon: 'plus',
onClick: () => props.modalRef.createCallLog(),
},
{
label: __('Make a Call'),
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
onClick: () => makeCall(props.doc.mobile_no),
condition: () => callEnabled.value,
},
]
return actions.filter((action) =>
action.condition ? action.condition() : true,
)
})
</script>

View File

@ -4,7 +4,7 @@
v-model:reloadTasks="activities"
:task="task"
:doctype="doctype"
:doc="doc.data?.name"
:doc="doc?.name"
@after="redirect('tasks')"
/>
<NoteModal
@ -12,13 +12,21 @@
v-model:reloadNotes="activities"
:note="note"
:doctype="doctype"
:doc="doc.data?.name"
:doc="doc?.name"
@after="redirect('notes')"
/>
<CallLogModal
v-if="showCallLogModal"
v-model="showCallLogModal"
:data="callLog"
:referenceDoc="referenceDoc"
:options="{ afterInsert: () => activities.reload() }"
/>
</template>
<script setup>
import TaskModal from '@/components/Modals/TaskModal.vue'
import NoteModal from '@/components/Modals/NoteModal.vue'
import CallLogModal from '@/components/Modals/CallLogModal.vue'
import { call } from 'frappe-ui'
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@ -77,6 +85,22 @@ function showNote(n) {
showNoteModal.value = true
}
// Call Logs
const showCallLogModal = ref(false)
const callLog = ref({})
const referenceDoc = ref({})
function createCallLog() {
let doctype = props.doctype
let docname = props.doc?.name
referenceDoc.value = { ...props.doc }
callLog.value = {
reference_doctype: doctype,
reference_docname: docname,
}
showCallLogModal.value = true
}
// common
const route = useRoute()
const router = useRouter()
@ -95,5 +119,6 @@ defineExpose({
deleteTask,
updateTaskStatus,
showNote,
createCallLog,
})
</script>

View File

@ -50,10 +50,12 @@
togglePrivate(attachment.name, attachment.is_private)
"
>
<FeatherIcon
:name="attachment.is_private ? 'lock' : 'unlock'"
class="size-3 text-ink-gray-7"
/>
<template #icon>
<FeatherIcon
:name="attachment.is_private ? 'lock' : 'unlock'"
class="size-3 text-ink-gray-7"
/>
</template>
</Button>
</div>
</Tooltip>
@ -63,7 +65,12 @@
class="!size-5"
@click.stop="() => deleteAttachment(attachment.name)"
>
<FeatherIcon name="trash-2" class="size-3 text-ink-gray-7" />
<template #icon>
<FeatherIcon
name="trash-2"
class="size-3 text-ink-gray-7"
/>
</template>
</Button>
</div>
</Tooltip>

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