diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..f87390934 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# pythonindentation settings +[{*.py}] +indent_style = space +indent_size = 4 +max_line_length = 120 + +[{*.js,*.tsx,*.jsx,*.vue,*.css,*.scss,*.html}] +indent_style = space +indent_size = 2 +max_line_length = 120 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..4e3aff128 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,10 @@ +{ + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + + "env": { + "es6": true + } + } diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..3f68b22b9 --- /dev/null +++ b/.flake8 @@ -0,0 +1,72 @@ +[flake8] +ignore = + B007, + B950, + E101, + E111, + E114, + E116, + E117, + E121, + E122, + E123, + E124, + E125, + E126, + E127, + E128, + E131, + E201, + E202, + E203, + E211, + E221, + E222, + E223, + E224, + E225, + E226, + E228, + E231, + E241, + E242, + E251, + E261, + E262, + E265, + E266, + E271, + E272, + E273, + E274, + E301, + E302, + E303, + E305, + E306, + E401, + E402, + E501, + E502, + E701, + E702, + E703, + E741, + F403, + W191, + W291, + W292, + W293, + W391, + W503, + W504, + I001, + I005, + I004, + I003 + +per-file-ignores = + # syntax: [comma-separated path/to/file: comma-separated ERROR CODES] + __init__.py, hooks.py: F401 + +max-line-length = 200 diff --git a/.github/logo.png b/.github/logo.png index 6bf9bd93e..919f2bc65 100644 Binary files a/.github/logo.png and b/.github/logo.png differ diff --git a/.github/screenshots/CallLogs.png b/.github/screenshots/CallLogs.png deleted file mode 100644 index 0eab791a5..000000000 Binary files a/.github/screenshots/CallLogs.png and /dev/null differ diff --git a/.github/screenshots/CallUI.png b/.github/screenshots/CallUI.png deleted file mode 100644 index 7ebddd018..000000000 Binary files a/.github/screenshots/CallUI.png and /dev/null differ diff --git a/.github/screenshots/DealsList.png b/.github/screenshots/DealsList.png deleted file mode 100644 index c2911d703..000000000 Binary files a/.github/screenshots/DealsList.png and /dev/null differ diff --git a/.github/screenshots/Emailtemplates.png b/.github/screenshots/Emailtemplates.png deleted file mode 100644 index a30f08a89..000000000 Binary files a/.github/screenshots/Emailtemplates.png and /dev/null differ diff --git a/.github/screenshots/LeadPage.png b/.github/screenshots/LeadPage.png deleted file mode 100644 index 854035e45..000000000 Binary files a/.github/screenshots/LeadPage.png and /dev/null differ diff --git a/.github/screenshots/MainDealPage.png b/.github/screenshots/MainDealPage.png deleted file mode 100644 index fe32d4ec4..000000000 Binary files a/.github/screenshots/MainDealPage.png and /dev/null differ diff --git a/.github/screenshots/OpportunityPage.jpeg b/.github/screenshots/OpportunityPage.jpeg new file mode 100644 index 000000000..48eaf5953 Binary files /dev/null and b/.github/screenshots/OpportunityPage.jpeg differ diff --git a/.gitignore b/.gitignore index 206a011d4..56403dc46 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ __pycache__ dev-dist tags node_modules -crm/public/frontend +next_crm/public/frontend frontend/yarn.lock -crm/www/crm.html -build +next_crm/www/next-crm/index.html +build \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..a796939d5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,80 @@ +exclude: "node_modules|.git" +default_stages: [commit] +fail_fast: false + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + files: '^next_crm/.*\.py' + exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + - id: check-yaml + - id: check-merge-conflict + - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements + files: '^next_crm/.*\.py' + + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: ["--py310-plus"] + + - repo: https://github.com/psf/black + rev: 24.2.0 + hooks: + - id: black + files: '^next_crm/.*\.py' + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + files: '^next_crm/.*\.js|jsx|ts|tsx' + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + next_crm/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + next_crm/www/website_script.js| + next_crm/templates/includes/.*| + next_crm/public/js/lib/.*| + next_crm/website/doctype/website_theme/website_theme_template.scss + )$ + + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.56.0 + hooks: + - id: eslint + files: '^next_crm/.*\.js|jsx|ts|tsx' + args: ["--quiet"] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + next_crm/public/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.*| + next_crm/www/website_script.js| + next_crm/templates/includes/.*| + next_crm/public/js/lib/.* + )$ + + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + args: ["--profile", "black"] + files: '^next_crm/.*\.py' + + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + additional_dependencies: ["flake8-isort", "flake8-bugbear"] diff --git a/.releaserc b/.releaserc index 0c43b999a..ffcda2ea9 100644 --- a/.releaserc +++ b/.releaserc @@ -7,12 +7,12 @@ "@semantic-release/release-notes-generator", [ "@semantic-release/exec", { - "prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" crm/__init__.py' + "prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" next_crm/__init__.py' } ], [ "@semantic-release/git", { - "assets": ["crm/__init__.py"], + "assets": ["next_crm/__init__.py"], "message": "chore(release): Bumped to Version ${nextRelease.version}" } ], diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 000000000..65dc151d3 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,30 @@ +# Common large paths +node_modules/ +build/ +dist/ +vendor/ +.env/ +.venv/ +.tox/ +*.min.js +.npm/ +.yarn/ + +# Common test paths +test/ +tests/ +testsuite/ +*_test.go +test*.py + +# Semgrep rules folder +.frappe-semgrep + +# Semgrep-action log folder +.semgrep_logs/ + +# Github Actions +.github/ + +# Markdown files +*.md diff --git a/README.md b/README.md index ed962a2a1..9603f31bd 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,65 @@ +
+ Next CRM Logo +

Next CRM

+
+
- Screenshot 2022-09-18 at 9 16 08 PM + Screenshot of Opportunity page

- - issues - license

-
- Show more screenshots - Screenshot 2022-09-18 at 9 18 17 PM - Screenshot 2022-09-18 at 11 47 06 PM - Screenshot 2022-09-18 at 9 18 47 PM - Screenshot 2022-09-18 at 9 18 47 PM - Screenshot 2022-09-18 at 9 18 47 PM -
- ## Key Features - **Views:** Create custom views which is a combination of filters, sort and columns. - - **Pinned View:** Pin important leads and deals in the sidebar. + - **Pinned View:** Pin important leads and opportunities in the sidebar. - **Public View:** Share views with all users. - **Saved View:** Save views for later use. -- **Email Communication:** Send and receive emails directly from the Lead/Deal Page. +- **Email Communication:** Send and receive emails directly from the Lead/Opportunity Page. - **Email Templates:** Create and use email templates for faster communication. -- **Comments:** Add comments to leads and deals to keep track of the conversation. +- **Comments:** Add comments to leads and opportunities to keep track of the conversation. - **Notifications:** Get notified when someone mentions you in a comment. -- **Service Level Agreement:** Set SLA for leads and deals and get notified when the SLA is breached. -- **Assignment Rule:** Automatically assign leads and deals to users based on the criteria. -- **Tasks:** Create tasks for leads and deals. -- **Notes:** Add notes to leads and deals. +- **Service Level Agreement:** Set SLA for leads and opportunities and get notified when the SLA is breached. +- **Assignment Rule:** Automatically assign leads and opportunities to users based on the criteria. +- **ToDos:** Create todos for leads and opportunity. +- **Notes:** Add notes to leads and opportunity. - **Call Logs:** See the call logs with call details and recordings. -## Integrations - -- **Twilio:** Integrate Twilio to make and receive calls from the CRM. You can also record calls. It is a built-in integration. -- **WhatsApp:** Integrate WhatsApp to send and receive messages from the CRM. [Frappe WhatsApp](https://github.com/shridarpatil/frappe_whatsapp) is used for this integration. - ## Getting Started -### Managed Hosting - -Get started with your personal or business site with a few clicks on [Frappe Cloud](https://frappecloud.com/marketplace/apps/crm). - -### Docker (Recommended) - -The quickest way to set up Frappe CRM and take it for a test ride. - -Frappe framework is multi-tenant and supports multiple apps by default. This docker compose is just a standalone version with Frappe CRM pre-installed. Just put it behind your desired reverse-proxy if needed, and you're good to go. - -If you wish to use multiple Frappe apps or need multi-tenancy. Take a look at our production ready self-hosted workflow, or join us on Frappe Cloud to get first party support and hassle-free hosting. - -**Step 1**: Setup folder and download the required files - - mkdir frappe-crm - cd frappe-crm - -**Step 2**: Download the required files - -Docker Compose File: - - wget -O docker-compose.yml https://raw.githubusercontent.com/frappe/crm/develop/docker/docker-compose.yml - -Frappe CRM bench setup script - - wget -O init.sh https://raw.githubusercontent.com/frappe/crm/develop/docker/init.sh - -**Step 3**: Run the container and daemonize it - - docker compose up -d - -**Step 4**: The site [http://crm.localhost](http://crm.localhost) should now be available. The default credentials are: - -> username: administrator -> password: admin - -### Self-hosting - -If you prefer self-hosting, follow the official [Frappe Bench Installation](https://github.com/frappe/bench#installation) instructions. - -## Want to Just Try Out or Contribute? - -### Codespaces - -1. Open [this link](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=668199241&skip_quickstart=true&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&geo=SoutheastAsia) and click on "Create Codespace". -2. Wait for initialization (~15 mins). -3. Run `bench start` from the terminal tab. -4. Click on the link beside "8000" port under "Ports" tab. -5. Log in with "Administrator" as the username and "admin" as the password. -6. Go to `.github.dev/crm` to access the crm interface. - ### Local Setup 1. [Install Bench](https://github.com/frappe/bench). -2. Install Frappe CRM app: +2. [Install ERPNext](https://github.com/frappe/erpnext) +2. Get the Next CRM app: ```sh - $ bench get-app crm + $ bench get-app https://github.com/rtCamp/next-crm --branch next-develop ``` 3. Create a site with the crm app: ```sh - $ bench --site sitename.localhost install-app crm + $ bench --site sitename.localhost install-app next_crm ``` 4. Open the site in the browser: ```sh $ bench browse sitename.localhost --user Administrator ``` -5. Access the crm page at `sitename.localhost:8000/crm` in your web browser. - -## Need help? - -Join our [telegram group](https://t.me/frappecrm) for instant help. +5. Access the crm page at `sitename.localhost:8000/next-crm` in your web browser. -## Documentation +### Changes other than DocType -Check out the [official documentation](https://docs.frappe.io/crm) for more details. +1. App renamed to Next CRM +2. URL changed from /crm to /next-crm +3. Lead is compulsory to create Opportunity (being reconsidered) +4. ERPNext integration enabled by default -## License +### Removed Features +1. CRM Invitation – Permissions from the ERPNext CRM module are used directly. +2. Ability to link to ERPNext on a different site – Not required as this is tightly integrated with the ERPNext CRM module. -[GNU Affero General Public License v3.0](LICENSE) +We’d love your feedback! Please check it out and share your thoughts on Discuss Forum. diff --git a/crm/__init__.py b/crm/__init__.py deleted file mode 100644 index 3af1df468..000000000 --- a/crm/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -__version__ = "2.0.0-dev" -__title__ = "Frappe CRM" - diff --git a/crm/api/__init__.py b/crm/api/__init__.py deleted file mode 100644 index 58739de0d..000000000 --- a/crm/api/__init__.py +++ /dev/null @@ -1,127 +0,0 @@ -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 frappe.core.api.file import get_max_file_size - - -@frappe.whitelist(allow_guest=True) -def get_translations(): - if frappe.session.user != "Guest": - language = frappe.db.get_value("User", frappe.session.user, "language") - else: - language = frappe.db.get_single_value("System Settings", "language") - - return get_all_translations(language) - - -@frappe.whitelist() -def get_user_signature(): - user = frappe.session.user - user_email_signature = ( - frappe.db.get_value( - "User", - user, - "email_signature", - ) - if user - else None - ) - - signature = user_email_signature or frappe.db.get_value( - "Email Account", - {"default_outgoing": 1, "add_signature": 1}, - "signature", - ) - - if not signature: - return - - soup = BeautifulSoup(signature, "html.parser") - html_signature = soup.find("div", {"class": "ql-editor read-mode"}) - _signature = None - if html_signature: - _signature = html_signature.renderContents() - content = "" - if (cstr(_signature) or signature): - content = f'

{signature}

' - return content - - -@frappe.whitelist() -def get_posthog_settings(): - return { - "posthog_project_id": frappe.conf.get(POSTHOG_PROJECT_FIELD), - "posthog_host": frappe.conf.get(POSTHOG_HOST_FIELD), - "enable_telemetry": frappe.get_system_settings("enable_telemetry"), - "telemetry_site_age": frappe.utils.telemetry.site_age(), - } - - -def check_app_permission(): - if frappe.session.user == "Administrator": - return True - - roles = frappe.get_roles() - if any(role in ["System Manager", "Sales User", "Sales Manager", "Sales Master Manager"] for role in roles): - return True - - return False - - -@frappe.whitelist(allow_guest=True) -def accept_invitation(key: str = None): - if not key: - frappe.throw("Invalid or expired key") - - result = frappe.db.get_all("CRM Invitation", filters={"key": key}, pluck="name") - if not result: - frappe.throw("Invalid or expired key") - - invitation = frappe.get_doc("CRM Invitation", result[0]) - invitation.accept() - invitation.reload() - - if invitation.status == "Accepted": - frappe.local.login_manager.login_as(invitation.email) - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/crm" - - -@frappe.whitelist() -def invite_by_email(emails: str, role: str): - if not emails: - return - email_string = validate_email_address(emails, throw=False) - email_list = split_emails(email_string) - if not email_list: - return - 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"]]}, - pluck="email", - ) - - to_invite = list(set(email_list) - set(existing_members) - set(existing_invites)) - - for email in to_invite: - frappe.get_doc(doctype="CRM Invitation", email=email, role=role).insert(ignore_permissions=True) - - -@frappe.whitelist() -def get_file_uploader_defaults(doctype: str): - max_number_of_files = None - make_attachments_public = False - if doctype: - meta = frappe.get_meta(doctype) - max_number_of_files = meta.get("max_attachments") - make_attachments_public = meta.get("make_attachments_public") - - return { - 'allowed_file_types': frappe.get_system_settings("allowed_file_extensions"), - 'max_file_size': get_max_file_size(), - 'max_number_of_files': max_number_of_files, - 'make_attachments_public': bool(make_attachments_public), - } \ No newline at end of file diff --git a/crm/api/activities.py b/crm/api/activities.py deleted file mode 100644 index 8cc714ebc..000000000 --- a/crm/api/activities.py +++ /dev/null @@ -1,393 +0,0 @@ -import json - -from bs4 import BeautifulSoup -import frappe -from frappe import _ -from frappe.utils.caching import redis_cache -from frappe.desk.form.load import get_docinfo - -@frappe.whitelist() -def get_activities(name): - if frappe.db.exists("CRM Deal", name): - return get_deal_activities(name) - elif frappe.db.exists("CRM Lead", name): - return get_lead_activities(name) - else: - frappe.throw(_("Document not found"), frappe.DoesNotExistError) - -def get_deal_activities(name): - get_docinfo('', "CRM Deal", name) - docinfo = frappe.response["docinfo"] - deal_meta = frappe.get_meta("CRM Deal") - deal_fields = {field.fieldname: {"label": field.label, "options": field.options} for field in deal_meta.fields} - avoid_fields = [ - "lead", - "response_by", - "sla_creation", - "sla", - "first_response_time", - "first_responded_on", - ] - - doc = frappe.db.get_values("CRM Deal", name, ["creation", "owner", "lead"])[0] - lead = doc[2] - - activities = [] - calls = [] - notes = [] - tasks = [] - attachments = [] - creation_text = "created this deal" - - if lead: - activities, calls, notes, tasks, attachments = get_lead_activities(lead) - creation_text = "converted the lead to this deal" - - activities.append({ - "activity_type": "creation", - "creation": doc[0], - "owner": doc[1], - "data": creation_text, - "is_lead": False, - }) - - docinfo.versions.reverse() - - for version in docinfo.versions: - data = json.loads(version.data) - if not data.get("changed"): - continue - - if change := data.get("changed")[0]: - field = deal_fields.get(change[0], None) - - if not field or change[0] in avoid_fields or (not change[1] and not change[2]): - continue - - field_label = field.get("label") or change[0] - field_option = field.get("options") or None - - activity_type = "changed" - data = { - "field": change[0], - "field_label": field_label, - "old_value": change[1], - "value": change[2], - } - - if not change[1] and change[2]: - activity_type = "added" - data = { - "field": change[0], - "field_label": field_label, - "value": change[2], - } - elif change[1] and not change[2]: - activity_type = "removed" - data = { - "field": change[0], - "field_label": field_label, - "value": change[1], - } - - activity = { - "activity_type": activity_type, - "creation": version.creation, - "owner": version.owner, - "data": data, - "is_lead": False, - "options": field_option, - } - activities.append(activity) - - for comment in docinfo.comments: - activity = { - "name": comment.name, - "activity_type": "comment", - "creation": comment.creation, - "owner": comment.owner, - "content": comment.content, - "attachments": get_attachments('Comment', comment.name), - "is_lead": False, - } - activities.append(activity) - - for communication in docinfo.communications + docinfo.automated_messages: - activity = { - "activity_type": "communication", - "communication_type": communication.communication_type, - "creation": communication.creation, - "data": { - "subject": communication.subject, - "content": communication.content, - "sender_full_name": communication.sender_full_name, - "sender": communication.sender, - "recipients": communication.recipients, - "cc": communication.cc, - "bcc": communication.bcc, - "attachments": get_attachments('Communication', communication.name), - "read_by_recipient": communication.read_by_recipient, - "delivery_status": communication.delivery_status, - }, - "is_lead": False, - } - activities.append(activity) - - for attachment_log in docinfo.attachment_logs: - activity = { - "name": attachment_log.name, - "activity_type": "attachment_log", - "creation": attachment_log.creation, - "owner": attachment_log.owner, - "data": parse_attachment_log(attachment_log.content, attachment_log.comment_type), - "is_lead": False, - } - activities.append(activity) - - calls = calls + get_linked_calls(name) - notes = notes + get_linked_notes(name) - tasks = tasks + get_linked_tasks(name) - attachments = attachments + get_attachments('CRM Deal', name) - - activities.sort(key=lambda x: x["creation"], reverse=True) - activities = handle_multiple_versions(activities) - - return activities, calls, notes, tasks, attachments - -def get_lead_activities(name): - get_docinfo('', "CRM Lead", name) - docinfo = frappe.response["docinfo"] - lead_meta = frappe.get_meta("CRM Lead") - lead_fields = {field.fieldname: {"label": field.label, "options": field.options} for field in lead_meta.fields} - avoid_fields = [ - "converted", - "response_by", - "sla_creation", - "sla", - "first_response_time", - "first_responded_on", - ] - - doc = frappe.db.get_values("CRM Lead", name, ["creation", "owner"])[0] - activities = [{ - "activity_type": "creation", - "creation": doc[0], - "owner": doc[1], - "data": "created this lead", - "is_lead": True, - }] - - docinfo.versions.reverse() - - for version in docinfo.versions: - data = json.loads(version.data) - if not data.get("changed"): - continue - - if change := data.get("changed")[0]: - field = lead_fields.get(change[0], None) - - if not field or change[0] in avoid_fields or (not change[1] and not change[2]): - continue - - field_label = field.get("label") or change[0] - field_option = field.get("options") or None - - activity_type = "changed" - data = { - "field": change[0], - "field_label": field_label, - "old_value": change[1], - "value": change[2], - } - - if not change[1] and change[2]: - activity_type = "added" - data = { - "field": change[0], - "field_label": field_label, - "value": change[2], - } - elif change[1] and not change[2]: - activity_type = "removed" - data = { - "field": change[0], - "field_label": field_label, - "value": change[1], - } - - activity = { - "activity_type": activity_type, - "creation": version.creation, - "owner": version.owner, - "data": data, - "is_lead": True, - "options": field_option, - } - activities.append(activity) - - for comment in docinfo.comments: - activity = { - "name": comment.name, - "activity_type": "comment", - "creation": comment.creation, - "owner": comment.owner, - "content": comment.content, - "attachments": get_attachments('Comment', comment.name), - "is_lead": True, - } - activities.append(activity) - - for communication in docinfo.communications + docinfo.automated_messages: - activity = { - "activity_type": "communication", - "communication_type": communication.communication_type, - "creation": communication.creation, - "data": { - "subject": communication.subject, - "content": communication.content, - "sender_full_name": communication.sender_full_name, - "sender": communication.sender, - "recipients": communication.recipients, - "cc": communication.cc, - "bcc": communication.bcc, - "attachments": get_attachments('Communication', communication.name), - "read_by_recipient": communication.read_by_recipient, - "delivery_status": communication.delivery_status, - }, - "is_lead": True, - } - activities.append(activity) - - for attachment_log in docinfo.attachment_logs: - activity = { - "name": attachment_log.name, - "activity_type": "attachment_log", - "creation": attachment_log.creation, - "owner": attachment_log.owner, - "data": parse_attachment_log(attachment_log.content, attachment_log.comment_type), - "is_lead": True, - } - activities.append(activity) - - calls = get_linked_calls(name) - notes = get_linked_notes(name) - tasks = get_linked_tasks(name) - attachments = get_attachments('CRM Lead', name) - - activities.sort(key=lambda x: x["creation"], reverse=True) - activities = handle_multiple_versions(activities) - - return activities, calls, notes, tasks, attachments - - -def get_attachments(doctype, name): - return frappe.db.get_all( - "File", - filters={"attached_to_doctype": doctype, "attached_to_name": name}, - fields=["name", "file_name", "file_type", "file_url", "file_size", "is_private", "creation", "owner"], - ) or [] - -def handle_multiple_versions(versions): - activities = [] - grouped_versions = [] - old_version = None - for version in versions: - is_version = version["activity_type"] in ["changed", "added", "removed"] - if not is_version: - activities.append(version) - if not old_version: - old_version = version - if is_version: grouped_versions.append(version) - continue - if is_version and old_version.get("owner") and version["owner"] == old_version["owner"]: - grouped_versions.append(version) - else: - if grouped_versions: - activities.append(parse_grouped_versions(grouped_versions)) - grouped_versions = [] - if is_version: grouped_versions.append(version) - old_version = version - if version == versions[-1] and grouped_versions: - activities.append(parse_grouped_versions(grouped_versions)) - - return activities - -def parse_grouped_versions(versions): - version = versions[0] - if len(versions) == 1: - return version - other_versions = versions[1:] - version["other_versions"] = other_versions - return version - -def get_linked_calls(name): - calls = frappe.db.get_all( - "CRM Call Log", - filters={"reference_docname": name}, - fields=[ - "name", - "caller", - "receiver", - "from", - "to", - "duration", - "start_time", - "end_time", - "status", - "type", - "recording_url", - "creation", - "note", - ], - ) - return calls or [] - -def get_linked_notes(name): - notes = frappe.db.get_all( - "FCRM Note", - filters={"reference_docname": name}, - fields=['name', 'title', 'content', 'owner', 'modified'], - ) - return notes or [] - -def get_linked_tasks(name): - tasks = frappe.db.get_all( - "CRM Task", - filters={"reference_docname": name}, - fields=[ - "name", - "title", - "description", - "assigned_to", - "assigned_to", - "due_date", - "priority", - "status", - "modified", - ], - ) - return tasks or [] - -def parse_attachment_log(html, type): - soup = BeautifulSoup(html, "html.parser") - a_tag = soup.find("a") - type = "added" if type == "Attachment" else "removed" - if not a_tag: - return { - "type": type, - "file_name": html.replace("Removed ", ""), - "file_url": "", - "is_private": False, - } - - is_private = False - if "private/files" in a_tag["href"]: - is_private = True - - return { - "type": type, - "file_name": a_tag.text, - "file_url": a_tag["href"], - "is_private": is_private, - } \ No newline at end of file diff --git a/crm/api/auth.py b/crm/api/auth.py deleted file mode 100644 index d485ccc12..000000000 --- a/crm/api/auth.py +++ /dev/null @@ -1,38 +0,0 @@ -import frappe - -@frappe.whitelist(allow_guest=True) -def oauth_providers(): - from frappe.utils.html_utils import get_icon_html - from frappe.utils.password import get_decrypted_password - from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys - - out = [] - providers = frappe.get_all( - "Social Login Key", - filters={"enable_social_login": 1}, - fields=["name", "client_id", "base_url", "provider_name", "icon"], - order_by="name", - ) - - for provider in providers: - client_secret = get_decrypted_password("Social Login Key", provider.name, "client_secret") - if not client_secret: - continue - - icon = None - if provider.icon: - if provider.provider_name == "Custom": - icon = get_icon_html(provider.icon, small=True) - else: - icon = f"{provider.provider_name}" - - if provider.client_id and provider.base_url and get_oauth_keys(provider.name): - out.append( - { - "name": provider.name, - "provider_name": provider.provider_name, - "auth_url": get_oauth2_authorize_url(provider.name, "/crm"), - "icon": icon, - } - ) - return out \ No newline at end of file diff --git a/crm/api/comment.py b/crm/api/comment.py deleted file mode 100644 index 7d78f0d4e..000000000 --- a/crm/api/comment.py +++ /dev/null @@ -1,94 +0,0 @@ -from collections.abc import Iterable - -import frappe -from frappe import _ -from bs4 import BeautifulSoup -from crm.fcrm.doctype.crm_notification.crm_notification import notify_user - -def on_update(self, method): - notify_mentions(self) - - -def notify_mentions(doc): - """ - Extract mentions from `content`, and notify. - `content` must have `HTML` content. - """ - content = getattr(doc, "content", None) - if not content: - return - mentions = extract_mentions(content) - reference_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name) - for mention in mentions: - owner = frappe.get_cached_value("User", doc.owner, "full_name") - doctype = doc.reference_doctype - if doctype.startswith("CRM "): - doctype = doctype[4:].lower() - name = reference_doc.lead_name or name if doctype == "lead" else reference_doc.organization or reference_doc.lead_name or name - notification_text = f""" -
- { owner } - { _('mentioned you in {0}').format(doctype) } - { name } -
- """ - notify_user({ - "owner": doc.owner, - "assigned_to": mention.email, - "notification_type": "Mention", - "message": doc.content, - "notification_text": notification_text, - "reference_doctype": "Comment", - "reference_docname": doc.name, - "redirect_to_doctype": doc.reference_doctype, - "redirect_to_docname": doc.reference_name, - }) - - -def extract_mentions(html): - if not html: - return [] - soup = BeautifulSoup(html, "html.parser") - mentions = [] - for d in soup.find_all("span", attrs={"data-type": "mention"}): - mentions.append( - frappe._dict(full_name=d.get("data-label"), email=d.get("data-id")) - ) - return mentions - -@frappe.whitelist() -def add_attachments(name: str, attachments: Iterable[str | dict]) -> None: - """Add attachments to the given Comment - - :param name: Comment name - :param attachments: File names or dicts with keys "fname" and "fcontent" - """ - # loop through attachments - for a in attachments: - if isinstance(a, str): - attach = frappe.db.get_value("File", {"name": a}, ["file_url", "is_private"], as_dict=1) - file_args = { - "file_url": attach.file_url, - "is_private": attach.is_private, - } - elif isinstance(a, dict) and "fcontent" in a and "fname" in a: - # dict returned by frappe.attach_print() - file_args = { - "file_name": a["fname"], - "content": a["fcontent"], - "is_private": 1, - } - else: - continue - - file_args.update( - { - "attached_to_doctype": "Comment", - "attached_to_name": name, - "folder": "Home/Attachments", - } - ) - - _file = frappe.new_doc("File") - _file.update(file_args) - _file.save(ignore_permissions=True) \ No newline at end of file diff --git a/crm/api/contact.py b/crm/api/contact.py deleted file mode 100644 index 65bc06609..000000000 --- a/crm/api/contact.py +++ /dev/null @@ -1,183 +0,0 @@ -import frappe -from frappe import _ - - -def validate(doc, method): - set_primary_email(doc) - set_primary_mobile_no(doc) - doc.set_primary_email() - doc.set_primary("mobile_no") - update_deals_email_mobile_no(doc) - - -def set_primary_email(doc): - if not doc.email_ids: - return - - if len(doc.email_ids) == 1: - doc.email_ids[0].is_primary = 1 - - -def set_primary_mobile_no(doc): - if not doc.phone_nos: - return - - if len(doc.phone_nos) == 1: - doc.phone_nos[0].is_primary_mobile_no = 1 - - -def update_deals_email_mobile_no(doc): - linked_deals = frappe.get_all( - "CRM Contacts", - filters={"contact": doc.name, "is_primary": 1}, - fields=["parent"], - ) - - for linked_deal in linked_deals: - deal = frappe.get_cached_doc("CRM Deal", linked_deal.parent) - 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.whitelist() -def get_linked_deals(contact): - """Get linked deals for a contact""" - - if not frappe.has_permission("Contact", "read", contact): - frappe.throw("Not permitted", frappe.PermissionError) - - deal_names = frappe.get_all( - "CRM Contacts", - filters={"contact": contact, "parenttype": "CRM Deal"}, - fields=["parent"], - distinct=True, - ) - - # get deals data - deals = [] - for d in deal_names: - deal = frappe.get_cached_doc( - "CRM Deal", - d.parent, - fields=[ - "name", - "organization", - "currency", - "annual_revenue", - "status", - "email", - "mobile_no", - "deal_owner", - "modified", - ], - ) - deals.append(deal.as_dict()) - - return deals - - -@frappe.whitelist() -def create_new(contact, field, value): - """Create new email or phone for a contact""" - if not frappe.has_permission("Contact", "write", contact): - frappe.throw("Not permitted", frappe.PermissionError) - - contact = frappe.get_doc("Contact", contact) - - if field == "email": - contact.append("email_ids", {"email_id": value}) - elif field in ("mobile_no", "phone"): - contact.append("phone_nos", {"phone": value}) - else: - frappe.throw("Invalid field") - - contact.save() - return True - - -@frappe.whitelist() -def set_as_primary(contact, field, value): - """Set email or phone as primary for a contact""" - if not frappe.has_permission("Contact", "write", contact): - frappe.throw("Not permitted", frappe.PermissionError) - - contact = frappe.get_doc("Contact", contact) - - if field == "email": - for email in contact.email_ids: - if email.email_id == value: - email.is_primary = 1 - else: - email.is_primary = 0 - elif field in ("mobile_no", "phone"): - name = "is_primary_mobile_no" if field == "mobile_no" else "is_primary_phone" - for phone in contact.phone_nos: - if phone.phone == value: - phone.set(name, 1) - else: - phone.set(name, 0) - else: - frappe.throw("Invalid field") - - contact.save() - return True - - -@frappe.whitelist() -def search_emails(txt: str): - doctype = "Contact" - meta = frappe.get_meta(doctype) - filters = [["Contact", "email_id", "is", "set"]] - - if meta.get("fields", {"fieldname": "enabled", "fieldtype": "Check"}): - filters.append([doctype, "enabled", "=", 1]) - if meta.get("fields", {"fieldname": "disabled", "fieldtype": "Check"}): - filters.append([doctype, "disabled", "!=", 1]) - - or_filters = [] - search_fields = ["full_name", "email_id", "name"] - if txt: - for f in search_fields: - or_filters.append([doctype, f.strip(), "like", f"%{txt}%"]) - - results = frappe.get_list( - doctype, - filters=filters, - fields=search_fields, - or_filters=or_filters, - limit_start=0, - limit_page_length=20, - order_by='email_id, full_name, name', - ignore_permissions=False, - as_list=True, - strict=False, - ) - - return results \ No newline at end of file diff --git a/crm/api/demo.py b/crm/api/demo.py deleted file mode 100644 index 1ac27b7ff..000000000 --- a/crm/api/demo.py +++ /dev/null @@ -1,36 +0,0 @@ -import frappe -from frappe import _ -from frappe.auth import LoginManager - - -@frappe.whitelist(allow_guest=True) -def login(): - if not frappe.conf.demo_username or not frappe.conf.demo_password: - return - frappe.local.response["redirect_to"] = "/crm" - login_manager = LoginManager() - login_manager.authenticate(frappe.conf.demo_username, frappe.conf.demo_password) - login_manager.post_login() - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = frappe.local.response["redirect_to"] - - -def validate_reset_password(user): - if frappe.conf.demo_username and frappe.session.user == frappe.conf.demo_username: - frappe.throw( - _("Password cannot be reset by Demo User {}").format( - frappe.bold(frappe.conf.demo_username) - ), - frappe.PermissionError, - ) - - -def validate_user(doc, event): - if frappe.conf.demo_username and frappe.session.user == frappe.conf.demo_username and doc.new_password: - frappe.throw( - _("Password cannot be reset by Demo User {}").format( - frappe.bold(frappe.conf.demo_username) - ), - frappe.PermissionError, - ) - diff --git a/crm/api/doc.py b/crm/api/doc.py deleted file mode 100644 index 576127a1a..000000000 --- a/crm/api/doc.py +++ /dev/null @@ -1,702 +0,0 @@ -import frappe -import json -from frappe import _ -from frappe.model.document import get_controller -from frappe.model import no_value_fields -from pypika import Criterion -from frappe.utils import make_filter_tuple - -from crm.api.views import get_views -from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script - - -@frappe.whitelist() -def sort_options(doctype: str): - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldtype not in no_value_fields] - fields = [ - { - "label": _(field.label), - "value": field.fieldname, - } - for field in fields - if field.label and field.fieldname - ] - - standard_fields = [ - {"label": "Name", "value": "name"}, - {"label": "Created On", "value": "creation"}, - {"label": "Last Modified", "value": "modified"}, - {"label": "Modified By", "value": "modified_by"}, - {"label": "Owner", "value": "owner"}, - ] - - for field in standard_fields: - field["label"] = _(field["label"]) - fields.append(field) - - return fields - - -@frappe.whitelist() -def get_filterable_fields(doctype: str): - allowed_fieldtypes = [ - "Check", - "Data", - "Float", - "Int", - "Currency", - "Dynamic Link", - "Link", - "Long Text", - "Select", - "Small Text", - "Text Editor", - "Text", - "Duration", - "Date", - "Datetime", - ] - - c = get_controller(doctype) - restricted_fields = [] - if hasattr(c, "get_non_filterable_fields"): - restricted_fields = c.get_non_filterable_fields() - - res = [] - - # append DocFields - DocField = frappe.qb.DocType("DocField") - doc_fields = get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields) - res.extend(doc_fields) - - # append Custom Fields - CustomField = frappe.qb.DocType("Custom Field") - custom_fields = get_doctype_fields_meta(CustomField, doctype, allowed_fieldtypes, restricted_fields) - res.extend(custom_fields) - - # append standard fields (getting error when using frappe.model.std_fields) - standard_fields = [ - {"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype}, - { - "fieldname": "owner", - "fieldtype": "Link", - "label": "Created By", - "options": "User" - }, - { - "fieldname": "modified_by", - "fieldtype": "Link", - "label": "Last Updated By", - "options": "User", - }, - {"fieldname": "_user_tags", "fieldtype": "Data", "label": "Tags"}, - {"fieldname": "_liked_by", "fieldtype": "Data", "label": "Like"}, - {"fieldname": "_comments", "fieldtype": "Text", "label": "Comments"}, - {"fieldname": "_assign", "fieldtype": "Text", "label": "Assigned To"}, - {"fieldname": "creation", "fieldtype": "Datetime", "label": "Created On"}, - {"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"}, - ] - for field in standard_fields: - if ( - field.get("fieldname") not in restricted_fields and - field.get("fieldtype") in allowed_fieldtypes - ): - field["name"] = field.get("fieldname") - res.append(field) - - for field in res: - field["label"] = _(field.get("label")) - - return res - - -@frappe.whitelist() -def get_group_by_fields(doctype: str): - allowed_fieldtypes = [ - "Check", - "Data", - "Float", - "Int", - "Currency", - "Dynamic Link", - "Link", - "Select", - "Duration", - "Date", - "Datetime", - ] - - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldtype not in no_value_fields and field.fieldtype in allowed_fieldtypes] - fields = [ - { - "label": _(field.label), - "value": field.fieldname, - } - for field in fields - if field.label and field.fieldname - ] - - standard_fields = [ - {"label": "Name", "value": "name"}, - {"label": "Created On", "value": "creation"}, - {"label": "Last Modified", "value": "modified"}, - {"label": "Modified By", "value": "modified_by"}, - {"label": "Owner", "value": "owner"}, - {"label": "Liked By", "value": "_liked_by"}, - {"label": "Assigned To", "value": "_assign"}, - {"label": "Comments", "value": "_comments"}, - {"label": "Created On", "value": "creation"}, - {"label": "Modified On", "value": "modified"}, - ] - - for field in standard_fields: - field["label"] = _(field["label"]) - fields.append(field) - - return fields - - -def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields): - parent = "parent" if DocField._table_name == "tabDocField" else "dt" - return ( - frappe.qb.from_(DocField) - .select( - DocField.fieldname, - DocField.fieldtype, - DocField.label, - DocField.name, - DocField.options, - ) - .where(DocField[parent] == doctype) - .where(DocField.hidden == False) - .where(Criterion.any([DocField.fieldtype == i for i in allowed_fieldtypes])) - .where(Criterion.all([DocField.fieldname != i for i in restricted_fields])) - .run(as_dict=True) - ) - -@frappe.whitelist() -def get_quick_filters(doctype: str): - meta = frappe.get_meta(doctype) - fields = [field for field in meta.fields if field.in_standard_filter] - quick_filters = [] - - for field in fields: - - if field.fieldtype == "Select": - field.options = field.options.split("\n") - field.options = [{"label": option, "value": option} for option in field.options] - field.options.insert(0, {"label": "", "value": ""}) - quick_filters.append({ - "label": _(field.label), - "name": field.fieldname, - "type": field.fieldtype, - "options": field.options, - }) - - if doctype == "CRM Lead": - quick_filters = [filter for filter in quick_filters if filter.get("name") != "converted"] - - return quick_filters - -@frappe.whitelist() -def get_data( - doctype: str, - filters: dict, - order_by: str, - page_length=20, - page_length_count=20, - column_field=None, - title_field=None, - columns=[], - rows=[], - kanban_columns=[], - kanban_fields=[], - view=None, - default_filters=None, -): - custom_view = False - filters = frappe._dict(filters) - rows = frappe.parse_json(rows or "[]") - columns = frappe.parse_json(columns or "[]") - kanban_fields = frappe.parse_json(kanban_fields or "[]") - kanban_columns = frappe.parse_json(kanban_columns or "[]") - - custom_view_name = view.get('custom_view_name') if view else None - view_type = view.get('view_type') if view else None - group_by_field = view.get('group_by_field') if view else None - - for key in filters: - value = filters[key] - if isinstance(value, list): - if "@me" in value: - value[value.index("@me")] = frappe.session.user - elif "%@me%" in value: - index = [i for i, v in enumerate(value) if v == "%@me%"] - for i in index: - value[i] = "%" + frappe.session.user + "%" - elif value == "@me": - filters[key] = frappe.session.user - - if default_filters: - default_filters = frappe.parse_json(default_filters) - filters.update(default_filters) - - is_default = True - data = [] - _list = get_controller(doctype) - default_rows = [] - if hasattr(_list, "default_list_data"): - default_rows = _list.default_list_data().get("rows") - - if view_type != "kanban": - if columns or rows: - custom_view = True - is_default = False - columns = frappe.parse_json(columns) - rows = frappe.parse_json(rows) - - if not columns: - columns = [ - {"label": "Name", "type": "Data", "key": "name", "width": "16rem"}, - {"label": "Last Modified", "type": "Datetime", "key": "modified", "width": "8rem"}, - ] - - if not rows: - rows = ["name"] - - default_view_filters = { - "dt": doctype, - "type": view_type or 'list', - "is_default": 1, - "user": frappe.session.user, - } - - if not custom_view and frappe.db.exists("CRM View Settings", default_view_filters): - list_view_settings = frappe.get_doc("CRM View Settings", default_view_filters) - columns = frappe.parse_json(list_view_settings.columns) - rows = frappe.parse_json(list_view_settings.rows) - is_default = False - elif not custom_view or is_default and hasattr(_list, "default_list_data"): - rows = default_rows - columns = _list.default_list_data().get("columns") - - # check if rows has all keys from columns if not add them - for column in columns: - if column.get("key") not in rows: - rows.append(column.get("key")) - column["label"] = _(column.get("label")) - - if column.get("key") == "_liked_by" and column.get("width") == "10rem": - column["width"] = "50px" - - # check if rows has group_by_field if not add it - if group_by_field and group_by_field not in rows: - rows.append(group_by_field) - - data = frappe.get_list( - doctype, - fields=rows, - filters=filters, - order_by=order_by, - page_length=page_length, - ) or [] - - if view_type == "kanban": - if not rows: - rows = default_rows - - if not kanban_columns and column_field: - field_meta = frappe.get_meta(doctype).get_field(column_field) - if field_meta.fieldtype == "Link": - kanban_columns = frappe.get_all( - field_meta.options, - fields=["name"], - order_by="modified asc", - ) - elif field_meta.fieldtype == "Select": - kanban_columns = [{"name": option} for option in field_meta.options.split("\n")] - - if not title_field: - title_field = "name" - if hasattr(_list, "default_kanban_settings"): - title_field = _list.default_kanban_settings().get("title_field") - - if title_field not in rows: - rows.append(title_field) - - if not kanban_fields: - kanban_fields = ["name"] - if hasattr(_list, "default_kanban_settings"): - kanban_fields = json.loads(_list.default_kanban_settings().get("kanban_fields")) - - for field in kanban_fields: - if field not in rows: - rows.append(field) - - for kc in kanban_columns: - column_filters = { column_field: kc.get('name') } - order = kc.get("order") - if column_field in filters and filters.get(column_field) != kc.name or 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") - - if order: - column_data = get_records_based_on_order(doctype, rows, column_filters, page_length, order) - else: - column_data = frappe.get_list( - doctype, - fields=rows, - filters=convert_filter_to_tuple(doctype, column_filters), - order_by=order_by, - page_length=page_length, - ) - - new_filters = filters.copy() - new_filters.update({ column_field: kc.get('name') }) - - all_count = len(frappe.get_list(doctype, filters=convert_filter_to_tuple(doctype, new_filters))) - - 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, key=lambda x: order.index(x.get("name")) - if x.get("name") in order else len(order) - ) - - data.append({"column": kc, "fields": kanban_fields, "data": column_data}) - - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldtype not in no_value_fields] - fields = [ - { - "label": _(field.label), - "type": field.fieldtype, - "value": field.fieldname, - "options": field.options, - } - for field in fields - if field.label and field.fieldname - ] - - std_fields = [ - {"label": "Name", "type": "Data", "value": "name"}, - {"label": "Created On", "type": "Datetime", "value": "creation"}, - {"label": "Last Modified", "type": "Datetime", "value": "modified"}, - { - "label": "Modified By", - "type": "Link", - "value": "modified_by", - "options": "User", - }, - {"label": "Assigned To", "type": "Text", "value": "_assign"}, - {"label": "Owner", "type": "Link", "value": "owner", "options": "User"}, - {"label": "Like", "type": "Data", "value": "_liked_by"}, - ] - - for field in std_fields: - if field.get('value') not in rows: - rows.append(field.get('value')) - if field not in fields: - field["label"] = _(field["label"]) - fields.append(field) - - if not is_default and custom_view_name: - is_default = frappe.db.get_value("CRM View Settings", custom_view_name, "load_default_columns") - - if group_by_field and view_type == "group_by": - def get_options(type, options): - if type == "Select": - return [option for option in options.split("\n")] - else: - has_empty_values = any([not d.get(group_by_field) for d in data]) - options = list(set([d.get(group_by_field) for d in data])) - options = [u for u in options if u] - if has_empty_values: - options.append("") - - if order_by and group_by_field in order_by: - order_by_fields = order_by.split(",") - order_by_fields = [(field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields] - if (group_by_field, "asc") in order_by_fields: - options.sort() - elif (group_by_field, "desc") in order_by_fields: - options.sort(reverse=True) - else: - options.sort() - return options - - for field in fields: - if field.get("value") == group_by_field: - group_by_field = { - "label": field.get("label"), - "name": field.get("value"), - "type": field.get("type"), - "options": get_options(field.get("type"), field.get("options")), - } - - return { - "data": data, - "columns": columns, - "rows": rows, - "fields": fields, - "column_field": column_field, - "title_field": title_field, - "kanban_columns": kanban_columns, - "kanban_fields": kanban_fields, - "group_by_field": group_by_field, - "page_length": page_length, - "page_length_count": page_length_count, - "is_default": is_default, - "views": get_views(doctype), - "total_count": len(frappe.get_list(doctype, filters=filters)), - "row_count": len(data), - "form_script": get_form_script(doctype), - "list_script": get_form_script(doctype, "List"), - "view_type": view_type, - } - -def convert_filter_to_tuple(doctype, filters): - if isinstance(filters, dict): - filters_items = filters.items() - filters = [] - for key, value in filters_items: - filters.append(make_filter_tuple(doctype, key, value)) - return filters - - -def get_records_based_on_order(doctype, rows, filters, page_length, order): - records = [] - filters = convert_filter_to_tuple(doctype, filters) - in_filters = filters.copy() - in_filters.append([doctype, "name", "in", order[:page_length]]) - records = frappe.get_list( - doctype, - fields=rows, - filters=in_filters, - order_by="creation desc", - page_length=page_length, - ) - - if len(records) < page_length: - not_in_filters = filters.copy() - not_in_filters.append([doctype, "name", "not in", order]) - remaining_records = frappe.get_list( - doctype, - fields=rows, - filters=not_in_filters, - order_by="creation desc", - page_length=page_length - len(records), - ) - for record in remaining_records: - records.append(record) - - return records - -@frappe.whitelist() -def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False): - not_allowed_fieldtypes = [ - "Tab Break", - "Section Break", - "Column Break", - ] - - if restricted_fieldtypes: - restricted_fieldtypes = frappe.parse_json(restricted_fieldtypes) - not_allowed_fieldtypes += restricted_fieldtypes - - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] - - standard_fields = [ - {"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype}, - { - "fieldname": "owner", - "fieldtype": "Link", - "label": "Created By", - "options": "User" - }, - { - "fieldname": "modified_by", - "fieldtype": "Link", - "label": "Last Updated By", - "options": "User", - }, - {"fieldname": "_user_tags", "fieldtype": "Data", "label": "Tags"}, - {"fieldname": "_liked_by", "fieldtype": "Data", "label": "Like"}, - {"fieldname": "_comments", "fieldtype": "Text", "label": "Comments"}, - {"fieldname": "_assign", "fieldtype": "Text", "label": "Assigned To"}, - {"fieldname": "creation", "fieldtype": "Datetime", "label": "Created On"}, - {"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"}, - ] - - for field in standard_fields: - if not restricted_fieldtypes or field.get('fieldtype') not in restricted_fieldtypes: - fields.append(field) - - if as_array: - return fields - - fields_meta = {} - for field in fields: - fields_meta[field.get('fieldname')] = field - - return fields_meta - -@frappe.whitelist() -def get_sidebar_fields(doctype, name): - if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}): - return [] - layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}).layout - - if not layout: - return [] - - layout = json.loads(layout) - - not_allowed_fieldtypes = [ - "Tab Break", - "Section Break", - "Column Break", - ] - - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] - - doc = frappe.get_cached_doc(doctype, name) - has_high_permlevel_fields = any(df.permlevel > 0 for df in fields) - if has_high_permlevel_fields: - has_read_access_to_permlevels = doc.get_permlevel_access("read") - has_write_access_to_permlevels = doc.get_permlevel_access("write") - - for section in layout: - section["name"] = section.get("name") or section.get("label") - for field in section.get("fields") if section.get("fields") else []: - field_obj = next((f for f in fields if f.fieldname == field), None) - if field_obj: - if field_obj.permlevel > 0: - field_has_write_access = field_obj.permlevel in has_write_access_to_permlevels - field_has_read_access = field_obj.permlevel in has_read_access_to_permlevels - if not field_has_write_access and field_has_read_access: - field_obj.read_only = 1 - if not field_has_read_access and not field_has_write_access: - field_obj.hidden = 1 - section["fields"][section.get("fields").index(field)] = get_field_obj(field_obj) - - fields_meta = {} - for field in fields: - fields_meta[field.fieldname] = field - - return layout - -def get_field_obj(field): - obj = { - "label": field.label, - "type": get_type(field), - "name": field.fieldname, - "hidden": field.hidden, - "reqd": field.reqd, - "read_only": field.read_only, - "all_properties": field, - } - - obj["placeholder"] = field.get("placeholder") or "Add " + field.label + "..." - - if field.fieldtype == "Link": - obj["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." - obj["doctype"] = field.options - elif field.fieldtype == "Select" and field.options: - obj["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." - obj["options"] = [{"label": option, "value": option} for option in field.options.split("\n")] - - if field.read_only: - obj["tooltip"] = "This field is read only and cannot be edited." - - return obj - - -def get_type(field): - if field.fieldtype == "Data" and field.options == "Phone": - return "phone" - elif field.fieldtype == "Data" and field.options == "Email": - return "email" - elif field.fieldtype == "Check": - return "checkbox" - elif field.fieldtype == "Int": - return "number" - elif field.fieldtype in ["Small Text", "Text", "Long Text"]: - return "textarea" - elif field.read_only: - return "read_only" - return field.fieldtype.lower() - -def get_assigned_users(doctype, name, default_assigned_to=None): - assigned_users = frappe.get_all( - "ToDo", - fields=["allocated_to"], - filters={ - "reference_type": doctype, - "reference_name": name, - "status": ("!=", "Cancelled"), - }, - pluck="allocated_to", - ) - - users = list(set(assigned_users)) - - # if users is empty, add default_assigned_to - if not users and default_assigned_to: - users = [default_assigned_to] - return users - - -@frappe.whitelist() -def get_fields(doctype: str, allow_all_fieldtypes: bool = False): - not_allowed_fieldtypes = list(frappe.model.no_value_fields) + ["Read Only"] - if allow_all_fieldtypes: - not_allowed_fieldtypes = [] - fields = frappe.get_meta(doctype).fields - - _fields = [] - - for field in fields: - if ( - field.fieldtype not in not_allowed_fieldtypes - and field.fieldname - ): - _fields.append({ - "label": field.label, - "type": field.fieldtype, - "value": field.fieldname, - "options": field.options, - "mandatory": field.reqd, - "read_only": field.read_only, - "hidden": field.hidden, - "depends_on": field.depends_on, - "mandatory_depends_on": field.mandatory_depends_on, - "read_only_depends_on": field.read_only_depends_on, - "link_filters": field.get("link_filters"), - "placeholder": field.get("placeholder"), - }) - - return _fields - - -def getCounts(d, doctype): - d["_email_count"] = frappe.db.count("Communication", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "communication_type": "Communication"}) or 0 - d["_email_count"] = d["_email_count"] + frappe.db.count("Communication", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "communication_type": "Automated Message"}) - d["_comment_count"] = frappe.db.count("Comment", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "comment_type": "Comment"}) - d["_task_count"] = frappe.db.count("CRM Task", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}) - d["_note_count"] = frappe.db.count("FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}) - return d \ No newline at end of file diff --git a/crm/api/session.py b/crm/api/session.py deleted file mode 100644 index 2e2b93758..000000000 --- a/crm/api/session.py +++ /dev/null @@ -1,89 +0,0 @@ -import frappe - - -@frappe.whitelist() -def get_users(): - users = frappe.qb.get_query( - "User", - fields=["name", "email", "enabled", "user_image", "first_name", "last_name", "full_name", "user_type"], - order_by="full_name asc", - distinct=True, - ).run(as_dict=1) - - for user in 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" - ) - return users - -@frappe.whitelist() -def get_contacts(): - contacts = frappe.get_all( - "Contact", - fields=[ - "name", - "salutation", - "first_name", - "last_name", - "full_name", - "gender", - "address", - "designation", - "image", - "email_id", - "mobile_no", - "phone", - "company_name", - "modified" - ], - order_by="first_name asc", - distinct=True, - ) - - for contact in contacts: - contact["email_ids"] = frappe.get_all( - "Contact Email", - filters={"parenttype": "Contact", "parent": contact.name}, - fields=["name", "email_id", "is_primary"], - ) - - contact["phone_nos"] = frappe.get_all( - "Contact Phone", - filters={"parenttype": "Contact", "parent": contact.name}, - fields=["name", "phone", "is_primary_phone", "is_primary_mobile_no"], - ) - - return contacts - -@frappe.whitelist() -def get_lead_contacts(): - lead_contacts = frappe.get_all( - "CRM Lead", - fields=[ - "name", - "lead_name", - "mobile_no", - "phone", - "image", - "modified" - ], - filters={"converted": 0}, - order_by="lead_name asc", - distinct=True, - ) - - return lead_contacts - -@frappe.whitelist() -def get_organizations(): - organizations = frappe.qb.get_query( - "CRM Organization", - fields=['*'], - order_by="name asc", - distinct=True, - ).run(as_dict=1) - - return organizations diff --git a/crm/api/todo.py b/crm/api/todo.py deleted file mode 100644 index f30e19f41..000000000 --- a/crm/api/todo.py +++ /dev/null @@ -1,103 +0,0 @@ -import frappe -from frappe import _ -from crm.fcrm.doctype.crm_notification.crm_notification import notify_user - -def after_insert(doc, method): - if doc.reference_type in ["CRM Lead", "CRM Deal"] and doc.reference_name and doc.allocated_to: - fieldname = "lead_owner" if doc.reference_type == "CRM Lead" else "deal_owner" - lead_owner = frappe.db.get_value(doc.reference_type, doc.reference_name, fieldname) - if not lead_owner: - frappe.db.set_value(doc.reference_type, doc.reference_name, fieldname, doc.allocated_to) - - if doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"] and doc.reference_name and doc.allocated_to: - notify_assigned_user(doc) - -def on_update(doc, method): - if doc.has_value_changed("status") and doc.status == "Cancelled" and doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"] and doc.reference_name and doc.allocated_to: - notify_assigned_user(doc, is_cancelled=True) - -def notify_assigned_user(doc, is_cancelled=False): - _doc = frappe.get_doc(doc.reference_type, doc.reference_name) - owner = frappe.get_cached_value("User", frappe.session.user, "full_name") - notification_text = get_notification_text(owner, doc, _doc, is_cancelled) - - message = _("Your assignment on {0} {1} has been removed by {2}").format( - doc.reference_type, - doc.reference_name, - owner - ) if is_cancelled else _("{0} assigned a {1} {2} to you").format( - owner, - doc.reference_type, - doc.reference_name - ) - - redirect_to_doctype, redirect_to_name = get_redirect_to_doc(doc) - - notify_user({ - "owner": frappe.session.user, - "assigned_to": doc.allocated_to, - "notification_type": "Assignment", - "message": message, - "notification_text": notification_text, - "reference_doctype": doc.reference_type, - "reference_docname": doc.reference_name, - "redirect_to_doctype": redirect_to_doctype, - "redirect_to_docname": redirect_to_name, - }) - -def get_notification_text(owner, doc, reference_doc, is_cancelled=False): - name = doc.reference_name - doctype = doc.reference_type - - if doctype.startswith("CRM "): - doctype = doctype[4:].lower() - - if doctype in ["lead", "deal"]: - name = reference_doc.lead_name or name if doctype == "lead" else reference_doc.organization or reference_doc.lead_name or name - - if is_cancelled: - return f""" -
- { _('Your assignment on {0} {1} has been removed by {2}').format( - doctype, - f'{ name }', - f'{ owner }' - ) } -
- """ - - return f""" -
- { owner } - { _('assigned a {0} {1} to you').format( - doctype, - f'{ name }' - ) } -
- """ - - if doctype == "task": - if is_cancelled: - return f""" -
- { _('Your assignment on task {0} has been removed by {1}').format( - f'{ reference_doc.title }', - f'{ owner }' - ) } -
- """ - return f""" -
- { owner } - { _('assigned a new task {0} to you').format( - f'{ reference_doc.title }' - ) } -
- """ - -def get_redirect_to_doc(doc): - if doc.reference_type == "CRM Task": - reference_doc = frappe.get_doc(doc.reference_type, doc.reference_name) - return reference_doc.reference_doctype, reference_doc.reference_docname - - return doc.reference_type, doc.reference_name diff --git a/crm/api/views.py b/crm/api/views.py deleted file mode 100644 index f70246e1d..000000000 --- a/crm/api/views.py +++ /dev/null @@ -1,16 +0,0 @@ -import frappe -from pypika import Criterion - - -@frappe.whitelist() -def get_views(doctype): - View = frappe.qb.DocType("CRM View Settings") - query = ( - frappe.qb.from_(View) - .select("*") - .where(Criterion.any([View.user == '', View.user == frappe.session.user])) - ) - if doctype: - query = query.where(View.dt == doctype) - views = query.run(as_dict=True) - return views \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_call_log/crm_call_log.py b/crm/fcrm/doctype/crm_call_log/crm_call_log.py deleted file mode 100644 index 42752c6ac..000000000 --- a/crm/fcrm/doctype/crm_call_log/crm_call_log.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -class CRMCallLog(Document): - @staticmethod - def default_list_data(): - columns = [ - { - 'label': 'From', - 'type': 'Link', - 'key': 'caller', - 'options': 'User', - 'width': '9rem', - }, - { - 'label': 'To', - 'type': 'Link', - 'key': 'receiver', - 'options': 'User', - 'width': '9rem', - }, - { - 'label': 'Type', - 'type': 'Select', - 'key': 'type', - 'width': '9rem', - }, - { - 'label': 'Status', - 'type': 'Select', - 'key': 'status', - 'width': '9rem', - }, - { - 'label': 'Duration', - 'type': 'Duration', - 'key': 'duration', - 'width': '6rem', - }, - { - 'label': 'From (number)', - 'type': 'Data', - 'key': 'from', - 'width': '9rem', - }, - { - 'label': 'To (number)', - 'type': 'Data', - 'key': 'to', - 'width': '9rem', - }, - { - 'label': 'Created On', - 'type': 'Datetime', - 'key': 'creation', - 'width': '8rem', - }, - ] - rows = [ - "name", - "caller", - "receiver", - "type", - "status", - "duration", - "from", - "to", - "note", - "recording_url", - "reference_doctype", - "reference_docname", - "creation", - ] - return {'columns': columns, 'rows': rows} - -@frappe.whitelist() -def create_lead_from_call_log(call_log): - 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.save(ignore_permissions=True) - - frappe.db.set_value("CRM Call Log", call_log.get("name"), { - "reference_doctype": "CRM Lead", - "reference_docname": lead.name - }) - - if call_log.get("note"): - frappe.db.set_value("FCRM Note", call_log.get("note"), { - "reference_doctype": "CRM Lead", - "reference_docname": lead.name - }) - - return lead.name \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_deal/api.py b/crm/fcrm/doctype/crm_deal/api.py deleted file mode 100644 index 76a764aed..000000000 --- a/crm/fcrm/doctype/crm_deal/api.py +++ /dev/null @@ -1,66 +0,0 @@ -import frappe -from frappe import _ - -from crm.api.doc import get_fields_meta, get_assigned_users -from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script - -@frappe.whitelist() -def get_deal(name): - Deal = frappe.qb.DocType("CRM Deal") - - query = ( - frappe.qb.from_(Deal) - .select("*") - .where(Deal.name == name) - .limit(1) - ) - - deal = query.run(as_dict=True) - if not len(deal): - frappe.throw(_("Deal not found"), frappe.DoesNotExistError) - deal = deal.pop() - - - deal["contacts"] = frappe.get_all( - "CRM Contacts", - filters={"parenttype": "CRM Deal", "parent": deal.name}, - fields=["contact", "is_primary"], - ) - - deal["doctype"] = "CRM Deal" - 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, deal.owner) - return deal - -@frappe.whitelist() -def get_deal_contacts(name): - contacts = frappe.get_all( - "CRM Contacts", - filters={"parenttype": "CRM Deal", "parent": name}, - fields=["contact", "is_primary"], - ) - deal_contacts = [] - for contact in contacts: - 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), - "is_primary": is_primary, - } - deal_contacts.append(_contact) - return deal_contacts \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.js b/crm/fcrm/doctype/crm_deal/crm_deal.js deleted file mode 100644 index 944ac6d4e..000000000 --- a/crm/fcrm/doctype/crm_deal/crm_deal.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("CRM Deal", { - refresh(frm) { - frm.add_web_link(`/crm/deals/${frm.doc.name}`, __("Open in Portal")); - }, -}); diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.json b/crm/fcrm/doctype/crm_deal/crm_deal.json deleted file mode 100644 index 34a22e35a..000000000 --- a/crm/fcrm/doctype/crm_deal/crm_deal.json +++ /dev/null @@ -1,379 +0,0 @@ -{ - "actions": [], - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", - "creation": "2023-11-06 17:56:25.210449", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "organization_tab", - "naming_series", - "organization", - "next_step", - "probability", - "column_break_ijan", - "status", - "close_date", - "deal_owner", - "contacts_tab", - "contacts", - "contact", - "lead_details_tab", - "lead", - "source", - "column_break_wsde", - "lead_name", - "organization_details_section", - "organization_name", - "website", - "no_of_employees", - "job_title", - "column_break_xbyf", - "territory", - "currency", - "annual_revenue", - "industry", - "person_section", - "salutation", - "first_name", - "last_name", - "column_break_xjmy", - "email", - "mobile_no", - "phone", - "gender", - "sla_tab", - "sla", - "sla_creation", - "column_break_pfvq", - "sla_status", - "communication_status", - "response_details_section", - "response_by", - "column_break_hpvj", - "first_response_time", - "first_responded_on", - "log_tab", - "status_change_log" - ], - "fields": [ - { - "fieldname": "organization", - "fieldtype": "Link", - "label": "Organization", - "options": "CRM Organization" - }, - { - "fieldname": "probability", - "fieldtype": "Percent", - "label": "Probability" - }, - { - "fetch_from": ".annual_revenue", - "fieldname": "annual_revenue", - "fieldtype": "Currency", - "label": "Amount", - "options": "currency" - }, - { - "fetch_from": ".website", - "fieldname": "website", - "fieldtype": "Data", - "label": "Website" - }, - { - "fieldname": "close_date", - "fieldtype": "Date", - "label": "Close Date" - }, - { - "fieldname": "next_step", - "fieldtype": "Data", - "label": "Next Step" - }, - { - "fieldname": "lead", - "fieldtype": "Link", - "label": "Lead", - "options": "CRM Lead" - }, - { - "fieldname": "deal_owner", - "fieldtype": "Link", - "label": "Deal Owner", - "options": "User" - }, - { - "default": "CRM-DEAL-.YYYY.-", - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Naming Series", - "options": "CRM-DEAL-.YYYY.-" - }, - { - "fieldname": "contacts_tab", - "fieldtype": "Tab Break", - "label": "Contacts" - }, - { - "fieldname": "email", - "fieldtype": "Data", - "label": "Email", - "options": "Email" - }, - { - "fieldname": "mobile_no", - "fieldtype": "Data", - "label": "Mobile No", - "options": "Phone" - }, - { - "default": "Qualification", - "fieldname": "status", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Status", - "options": "CRM Deal Status", - "reqd": 1, - "search_index": 1 - }, - { - "fieldname": "contacts", - "fieldtype": "Table", - "label": "Contacts", - "options": "CRM Contacts" - }, - { - "fieldname": "organization_tab", - "fieldtype": "Tab Break", - "label": "Organization" - }, - { - "fieldname": "sla_tab", - "fieldtype": "Tab Break", - "label": "SLA", - "read_only": 1 - }, - { - "fieldname": "sla", - "fieldtype": "Link", - "label": "SLA", - "options": "CRM Service Level Agreement" - }, - { - "fieldname": "response_by", - "fieldtype": "Datetime", - "label": "Response By", - "read_only": 1 - }, - { - "fieldname": "column_break_pfvq", - "fieldtype": "Column Break" - }, - { - "fieldname": "sla_status", - "fieldtype": "Select", - "label": "SLA Status", - "options": "\nFirst Response Due\nFailed\nFulfilled", - "read_only": 1 - }, - { - "fieldname": "sla_creation", - "fieldtype": "Datetime", - "label": "SLA Creation", - "read_only": 1 - }, - { - "fieldname": "response_details_section", - "fieldtype": "Section Break", - "label": "Response Details" - }, - { - "fieldname": "column_break_hpvj", - "fieldtype": "Column Break" - }, - { - "fieldname": "first_response_time", - "fieldtype": "Duration", - "label": "First Response Time", - "read_only": 1 - }, - { - "fieldname": "first_responded_on", - "fieldtype": "Datetime", - "label": "First Responded On", - "read_only": 1 - }, - { - "default": "Open", - "fieldname": "communication_status", - "fieldtype": "Link", - "label": "Communication Status", - "options": "CRM Communication Status" - }, - { - "fetch_from": ".territory", - "fieldname": "territory", - "fieldtype": "Link", - "label": "Territory", - "options": "CRM Territory" - }, - { - "fieldname": "source", - "fieldtype": "Link", - "label": "Source", - "options": "CRM Lead Source" - }, - { - "fieldname": "no_of_employees", - "fieldtype": "Select", - "label": "No. of Employees", - "options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+" - }, - { - "fieldname": "job_title", - "fieldtype": "Data", - "label": "Job Title" - }, - { - "fieldname": "phone", - "fieldtype": "Data", - "label": "Phone", - "options": "Phone" - }, - { - "fieldname": "log_tab", - "fieldtype": "Tab Break", - "label": "Log", - "read_only": 1 - }, - { - "fieldname": "status_change_log", - "fieldtype": "Table", - "label": "Status Change Log", - "options": "CRM Status Change Log" - }, - { - "fieldname": "lead_name", - "fieldtype": "Data", - "label": "Lead Name" - }, - { - "fieldname": "column_break_ijan", - "fieldtype": "Column Break" - }, - { - "fieldname": "lead_details_tab", - "fieldtype": "Tab Break", - "label": "Lead Details" - }, - { - "fieldname": "column_break_wsde", - "fieldtype": "Column Break" - }, - { - "fieldname": "organization_details_section", - "fieldtype": "Section Break", - "label": "Organization Details" - }, - { - "fieldname": "organization_name", - "fieldtype": "Data", - "label": "Organization Name" - }, - { - "fieldname": "column_break_xbyf", - "fieldtype": "Column Break" - }, - { - "fieldname": "industry", - "fieldtype": "Link", - "label": "Industry", - "options": "CRM Industry" - }, - { - "fieldname": "person_section", - "fieldtype": "Section Break", - "label": "Person" - }, - { - "fieldname": "salutation", - "fieldtype": "Link", - "label": "Salutation", - "options": "Salutation" - }, - { - "fieldname": "first_name", - "fieldtype": "Data", - "label": "First Name" - }, - { - "fieldname": "last_name", - "fieldtype": "Data", - "label": "Last Name" - }, - { - "fieldname": "column_break_xjmy", - "fieldtype": "Column Break" - }, - { - "fieldname": "gender", - "fieldtype": "Link", - "label": "Gender", - "options": "Gender" - }, - { - "fieldname": "contact", - "fieldtype": "Link", - "label": "Contact", - "options": "Contact" - }, - { - "fieldname": "currency", - "fieldtype": "Link", - "label": "Currency", - "options": "Currency" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-09-17 18:34:15.873610", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Deal", - "naming_rule": "By \"Naming Series\" field", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "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 - } - ], - "show_title_field_in_link": 1, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "title_field": "organization", - "track_changes": 1 -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.py b/crm/fcrm/doctype/crm_deal/crm_deal.py deleted file mode 100644 index dc18c1331..000000000 --- a/crm/fcrm/doctype/crm_deal/crm_deal.py +++ /dev/null @@ -1,308 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt -import json - -import frappe -from frappe import _ -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 - - -class CRMDeal(Document): - def before_validate(self): - self.set_sla() - - def validate(self): - self.set_primary_contact() - self.set_primary_email_mobile_no() - if not self.is_new() and self.has_value_changed("deal_owner") and self.deal_owner: - self.share_with_agent(self.deal_owner) - self.assign_agent(self.deal_owner) - if self.has_value_changed("status"): - add_status_change_log(self) - - def after_insert(self): - if self.deal_owner: - self.assign_agent(self.deal_owner) - - def before_save(self): - self.apply_sla() - - def set_primary_contact(self, contact=None): - if not self.contacts: - return - - if not contact and len(self.contacts) == 1: - self.contacts[0].is_primary = 1 - elif contact: - for d in self.contacts: - if d.contact == contact: - d.is_primary = 1 - else: - d.is_primary = 0 - - def set_primary_email_mobile_no(self): - if not self.contacts: - self.email = "" - self.mobile_no = "" - self.phone = "" - return - - if len([contact for contact in self.contacts if contact.is_primary]) > 1: - frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold("Contact"))) - - primary_contact_exists = False - for d in self.contacts: - if d.is_primary == 1: - primary_contact_exists = True - self.email = d.email.strip() if d.email else "" - self.mobile_no = d.mobile_no.strip() if d.mobile_no else "" - self.phone = d.phone.strip() if d.phone else "" - break - - if not primary_contact_exists: - self.email = "" - self.mobile_no = "" - self.phone = "" - - def assign_agent(self, agent): - if not agent: - return - - assignees = self.get_assigned_users() - if assignees: - for assignee in assignees: - if agent == assignee: - # the agent is already set as an assignee - return - - assign({"assign_to": [agent], "doctype": "CRM Deal", "name": self.name}) - - def share_with_agent(self, agent): - if not agent: - return - - docshares = frappe.get_all( - "DocShare", - filters={"share_name": self.name, "share_doctype": self.doctype}, - fields=["name", "user"], - ) - - shared_with = [d.user for d in docshares] + [agent] - - for user in shared_with: - if user == agent and not frappe.db.exists("DocShare", {"user": agent, "share_name": self.name, "share_doctype": self.doctype}): - frappe.share.add_docshare( - self.doctype, self.name, agent, write=1, flags={"ignore_share_permission": True} - ) - elif user != agent: - frappe.share.remove(self.doctype, self.name, user) - - - def set_sla(self): - """ - Find an SLA to apply to the deal. - """ - if self.sla: return - - sla = get_sla(self) - if not sla: - self.first_responded_on = None - self.first_response_time = None - return - self.sla = sla.name - - def apply_sla(self): - """ - Apply SLA if set. - """ - if not self.sla: - return - sla = frappe.get_last_doc("CRM Service Level Agreement", {"name": self.sla}) - if sla: - sla.apply(self) - - @staticmethod - def default_list_data(): - columns = [ - { - 'label': 'Organization', - 'type': 'Link', - 'key': 'organization', - 'options': 'CRM Organization', - 'width': '11rem', - }, - { - 'label': 'Amount', - 'type': 'Currency', - 'key': 'annual_revenue', - 'width': '9rem', - }, - { - 'label': 'Status', - 'type': 'Select', - 'key': 'status', - 'width': '10rem', - }, - { - 'label': 'Email', - 'type': 'Data', - 'key': 'email', - 'width': '12rem', - }, - { - 'label': 'Mobile No', - 'type': 'Data', - 'key': 'mobile_no', - 'width': '11rem', - }, - { - 'label': 'Assigned To', - 'type': 'Text', - 'key': '_assign', - 'width': '10rem', - }, - { - 'label': 'Last Modified', - 'type': 'Datetime', - 'key': 'modified', - 'width': '8rem', - }, - ] - rows = [ - "name", - "organization", - "annual_revenue", - "status", - "email", - "currency", - "mobile_no", - "deal_owner", - "sla_status", - "response_by", - "first_response_time", - "first_responded_on", - "modified", - "_assign", - ] - return {'columns': columns, 'rows': rows} - - @staticmethod - def default_kanban_settings(): - return { - "column_field": "status", - "title_field": "organization", - "kanban_fields": '["annual_revenue", "email", "mobile_no", "_assign", "modified"]' - } - -@frappe.whitelist() -def add_contact(deal, contact): - if not frappe.has_permission("CRM Deal", "write", deal): - frappe.throw(_("Not allowed to add contact to Deal"), frappe.PermissionError) - - deal = frappe.get_cached_doc("CRM Deal", deal) - deal.append("contacts", {"contact": contact}) - deal.save() - return True - -@frappe.whitelist() -def remove_contact(deal, contact): - if not frappe.has_permission("CRM Deal", "write", deal): - frappe.throw(_("Not allowed to remove contact from Deal"), frappe.PermissionError) - - deal = frappe.get_cached_doc("CRM Deal", deal) - deal.contacts = [d for d in deal.contacts if d.contact != contact] - deal.save() - return True - -@frappe.whitelist() -def set_primary_contact(deal, contact): - if not frappe.has_permission("CRM Deal", "write", deal): - frappe.throw(_("Not allowed to set primary contact for Deal"), frappe.PermissionError) - - deal = frappe.get_cached_doc("CRM Deal", deal) - deal.set_primary_contact(contact) - deal.save() - return True - -def create_organization(doc): - if not doc.get("organization_name"): - return - - existing_organization = frappe.db.exists("CRM Organization", {"organization_name": doc.get("organization_name")}) - if existing_organization: - return existing_organization - - organization = frappe.new_doc("CRM Organization") - organization.update( - { - "organization_name": doc.get("organization_name"), - "website": doc.get("website"), - "territory": doc.get("territory"), - "industry": doc.get("industry"), - "annual_revenue": doc.get("annual_revenue"), - } - ) - organization.insert(ignore_permissions=True) - return organization.name - -def contact_exists(doc): - email_exist = frappe.db.exists("Contact Email", {"email_id": doc.get("email")}) - mobile_exist = frappe.db.exists("Contact Phone", {"phone": doc.get("mobile_no")}) - - doctype = "Contact Email" if email_exist else "Contact Phone" - name = email_exist or mobile_exist - - if name: - return frappe.db.get_value(doctype, name, "parent") - - return False - -def create_contact(doc): - existing_contact = contact_exists(doc) - if existing_contact: - return existing_contact - - contact = frappe.new_doc("Contact") - contact.update( - { - "first_name": doc.get("first_name"), - "last_name": doc.get("last_name"), - "salutation": doc.get("salutation"), - "company_name": doc.get("organization") or doc.get("organization_name"), - } - ) - - if doc.get("email"): - contact.append("email_ids", {"email_id": doc.get("email"), "is_primary": 1}) - - if doc.get("mobile_no"): - contact.append("phone_nos", {"phone": doc.get("mobile_no"), "is_primary_mobile_no": 1}) - - contact.insert(ignore_permissions=True) - contact.reload() # load changes by hooks on contact - - return contact.name - -@frappe.whitelist() -def create_deal(args): - deal = frappe.new_doc("CRM Deal") - - contact = args.get("contact") - if not contact and (args.get("first_name") or args.get("last_name") or args.get("email") or args.get("mobile_no")): - contact = create_contact(args) - - deal.update({ - "organization": args.get("organization") or create_organization(args), - "contacts": [{"contact": contact, "is_primary": 1}] if contact else [], - }) - - args.pop("organization", None) - - deal.update(args) - - deal.insert(ignore_permissions=True) - return deal.name diff --git a/crm/fcrm/doctype/crm_deal/test_crm_deal.py b/crm/fcrm/doctype/crm_deal/test_crm_deal.py deleted file mode 100644 index 85ef93310..000000000 --- a/crm/fcrm/doctype/crm_deal/test_crm_deal.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestCRMDeal(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py deleted file mode 100644 index 6926d0b2b..000000000 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import json -import frappe -from frappe import _ -from frappe.model.document import Document - - -class CRMFieldsLayout(Document): - pass - -@frappe.whitelist() -def get_fields_layout(doctype: str, type: str): - sections = [] - if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}): - layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type}) - else: - return [] - - if layout.layout: - sections = json.loads(layout.layout) - - allowed_fields = [] - for section in sections: - if not section.get("fields"): - continue - allowed_fields.extend(section.get("fields")) - - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldname in allowed_fields] - - for section in sections: - for field in section.get("fields") if section.get("fields") else []: - field = next((f for f in fields if f.fieldname == field), None) - if field: - if field.fieldtype == "Select" and field.options: - field.options = field.options.split("\n") - field.options = [{"label": _(option), "value": option} for option in field.options] - field.options.insert(0, {"label": "", "value": ""}) - field = { - "label": _(field.label), - "name": field.fieldname, - "type": field.fieldtype, - "options": field.options, - "mandatory": field.reqd, - "placeholder": field.get("placeholder"), - "filters": field.get("link_filters") - } - section["fields"][section.get("fields").index(field["name"])] = field - - return sections or [] - - -@frappe.whitelist() -def save_fields_layout(doctype: str, type: str, layout: str): - if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}): - doc = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type}) - else: - doc = frappe.new_doc("CRM Fields Layout") - - doc.update({ - "dt": doctype, - "type": type, - "layout": layout, - }) - doc.save(ignore_permissions=True) - - return doc.layout diff --git a/crm/fcrm/doctype/crm_form_script/crm_form_script.js b/crm/fcrm/doctype/crm_form_script/crm_form_script.js deleted file mode 100644 index 0fbea0bb7..000000000 --- a/crm/fcrm/doctype/crm_form_script/crm_form_script.js +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("CRM Form Script", { - refresh(frm) { - frm.set_query("dt", { - filters: { - istable: 0, - }, - }); - - if (frm.doc.is_standard && !frappe.boot.developer_mode) { - frm.disable_form(); - frappe.show_alert( - __( - "Standard Form Scripts can not be modified, duplicate the Form Script instead." - ) - ); - } - - if (!frappe.boot.developer_mode) { - frm.toggle_enable("is_standard", 0); - } - - frm.trigger("add_enable_button"); - }, - - add_enable_button(frm) { - frm.add_custom_button( - frm.doc.enabled ? __("Disable") : __("Enable"), - () => { - frm.set_value("enabled", !frm.doc.enabled); - frm.save(); - } - ); - }, - - view(frm) { - let has_form_boilerplate = frm.doc.script.includes( - "function setupForm(" - ); - let has_list_boilerplate = frm.doc.script.includes( - "function setupList(" - ); - - if (frm.doc.view == "Form" && !has_form_boilerplate) { - frm.doc.script = ` -function setupForm({ doc }) { - return { - actions: [], - statuses: [], - } -}`.trim(); - } - if (frm.doc.view == "List" && !has_list_boilerplate) { - frm.doc.script = ` -function setupList({ list }) { - return { - actions: [], - bulk_actions: [], - } -}`.trim(); - } - }, -}); diff --git a/crm/fcrm/doctype/crm_form_script/crm_form_script.py b/crm/fcrm/doctype/crm_form_script/crm_form_script.py deleted file mode 100644 index bb35c851c..000000000 --- a/crm/fcrm/doctype/crm_form_script/crm_form_script.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) 2023, 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 CRMFormScript(Document): - def validate(self): - in_user_env = not ( - frappe.flags.in_install - or frappe.flags.in_patch - or frappe.flags.in_test - or frappe.flags.in_fixtures - ) - if in_user_env and not self.is_new() and self.is_standard and not frappe.conf.developer_mode: - # only enabled can be changed for standard form scripts - if self.has_value_changed("enabled"): - enabled_value = self.enabled - self.reload() - self.enabled = enabled_value - else: - frappe.throw(_("You need to be in developer mode to edit a Standard Form Script")) - -def get_form_script(dt, view="Form"): - """Returns the form script for the given doctype""" - FormScript = frappe.qb.DocType("CRM Form Script") - query = ( - frappe.qb.from_(FormScript) - .select("script") - .where(FormScript.dt == dt) - .where(FormScript.view == view) - .where(FormScript.enabled == 1) - ) - - doc = query.run(as_dict=True) - if doc: - return [d.script for d in doc] if len(doc) > 1 else doc[0].script - else: - return None diff --git a/crm/fcrm/doctype/crm_holiday/crm_holiday.json b/crm/fcrm/doctype/crm_holiday/crm_holiday.json deleted file mode 100644 index fc0f24f39..000000000 --- a/crm/fcrm/doctype/crm_holiday/crm_holiday.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2023-12-14 11:16:15.476366", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "date", - "column_break_xzyo", - "weekly_off", - "section_break_zenz", - "description" - ], - "fields": [ - { - "fieldname": "date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "Date", - "reqd": 1 - }, - { - "fieldname": "column_break_xzyo", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "weekly_off", - "fieldtype": "Check", - "label": "Weekly Off" - }, - { - "fieldname": "section_break_zenz", - "fieldtype": "Section Break" - }, - { - "fieldname": "description", - "fieldtype": "Text Editor", - "in_list_view": 1, - "label": "Description", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2023-12-14 11:17:41.745419", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Holiday", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_holiday/crm_holiday.py b/crm/fcrm/doctype/crm_holiday/crm_holiday.py deleted file mode 100644 index d77b735dc..000000000 --- a/crm/fcrm/doctype/crm_holiday/crm_holiday.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class CRMHoliday(Document): - pass diff --git a/crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.js b/crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.js deleted file mode 100644 index 914d43de6..000000000 --- a/crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("CRM Holiday List", { -// refresh(frm) { - -// }, -// }); diff --git a/crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.json b/crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.json deleted file mode 100644 index 066f12eed..000000000 --- a/crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "field:holiday_list_name", - "creation": "2023-12-14 11:09:12.876640", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "holiday_list_name", - "from_date", - "to_date", - "column_break_qwqc", - "total_holidays", - "add_weekly_holidays_section", - "weekly_off", - "add_to_holidays", - "holidays_section", - "holidays", - "clear_table" - ], - "fields": [ - { - "fieldname": "holiday_list_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Holiday List Name", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "from_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "From Date", - "reqd": 1 - }, - { - "fieldname": "to_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "To Date", - "reqd": 1 - }, - { - "fieldname": "column_break_qwqc", - "fieldtype": "Column Break" - }, - { - "fieldname": "total_holidays", - "fieldtype": "Int", - "label": "Total Holidays" - }, - { - "fieldname": "add_weekly_holidays_section", - "fieldtype": "Section Break", - "label": "Add Weekly Holidays" - }, - { - "fieldname": "weekly_off", - "fieldtype": "Select", - "label": "Weekly Off", - "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday" - }, - { - "fieldname": "add_to_holidays", - "fieldtype": "Button", - "label": "Add to Holidays" - }, - { - "fieldname": "holidays_section", - "fieldtype": "Section Break", - "label": "Holidays" - }, - { - "fieldname": "clear_table", - "fieldtype": "Button", - "label": "Clear Table" - }, - { - "fieldname": "holidays", - "fieldtype": "Table", - "label": "Holidays", - "options": "CRM Holiday" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-01-19 21:54:54.809445", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Holiday List", - "naming_rule": "By fieldname", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "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 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.py b/crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.py deleted file mode 100644 index 45c8b1015..000000000 --- a/crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class CRMHolidayList(Document): - pass diff --git a/crm/fcrm/doctype/crm_holiday_list/test_crm_holiday_list.py b/crm/fcrm/doctype/crm_holiday_list/test_crm_holiday_list.py deleted file mode 100644 index ee90dc8a4..000000000 --- a/crm/fcrm/doctype/crm_holiday_list/test_crm_holiday_list.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestCRMHolidayList(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/crm_industry/crm_industry.js b/crm/fcrm/doctype/crm_industry/crm_industry.js deleted file mode 100644 index 1489970c6..000000000 --- a/crm/fcrm/doctype/crm_industry/crm_industry.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("CRM Industry", { -// refresh(frm) { - -// }, -// }); diff --git a/crm/fcrm/doctype/crm_industry/crm_industry.json b/crm/fcrm/doctype/crm_industry/crm_industry.json deleted file mode 100644 index d3d15a711..000000000 --- a/crm/fcrm/doctype/crm_industry/crm_industry.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "actions": [], - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:industry", - "creation": "2023-07-24 19:40:31.980882", - "default_view": "List", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "industry" - ], - "fields": [ - { - "fieldname": "industry", - "fieldtype": "Data", - "label": "Industry", - "unique": 1 - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-01-19 21:57:02.025918", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Industry", - "naming_rule": "By fieldname", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "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 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_industry/crm_industry.py b/crm/fcrm/doctype/crm_industry/crm_industry.py deleted file mode 100644 index cbafacd94..000000000 --- a/crm/fcrm/doctype/crm_industry/crm_industry.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class CRMIndustry(Document): - pass diff --git a/crm/fcrm/doctype/crm_industry/test_crm_industry.py b/crm/fcrm/doctype/crm_industry/test_crm_industry.py deleted file mode 100644 index 16d87b134..000000000 --- a/crm/fcrm/doctype/crm_industry/test_crm_industry.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestCRMIndustry(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/crm_invitation/crm_invitation.js b/crm/fcrm/doctype/crm_invitation/crm_invitation.js deleted file mode 100644 index 6e0485d84..000000000 --- a/crm/fcrm/doctype/crm_invitation/crm_invitation.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("CRM Invitation", { - refresh(frm) { - if (frm.doc.status != "Accepted") { - frm.add_custom_button(__("Accept Invitation"), () => { - return frm.call("accept_invitation"); - }); - } - }, -}); diff --git a/crm/fcrm/doctype/crm_invitation/crm_invitation.json b/crm/fcrm/doctype/crm_invitation/crm_invitation.json deleted file mode 100644 index f5902d6ea..000000000 --- a/crm/fcrm/doctype/crm_invitation/crm_invitation.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2024-09-03 12:19:18.933810", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "email", - "role", - "key", - "invited_by", - "column_break_dsuz", - "status", - "email_sent_at", - "accepted_at" - ], - "fields": [ - { - "fieldname": "email", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Email", - "reqd": 1 - }, - { - "fieldname": "role", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Role", - "options": "\nSales User\nSales Manager", - "reqd": 1 - }, - { - "fieldname": "key", - "fieldtype": "Data", - "label": "Key" - }, - { - "fieldname": "invited_by", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Invited By", - "options": "User" - }, - { - "fieldname": "column_break_dsuz", - "fieldtype": "Column Break" - }, - { - "fieldname": "status", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Status", - "options": "\nPending\nAccepted\nExpired" - }, - { - "fieldname": "email_sent_at", - "fieldtype": "Datetime", - "label": "Email Sent At" - }, - { - "fieldname": "accepted_at", - "fieldtype": "Datetime", - "label": "Accepted At" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-09-03 14:59:29.450018", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Invitation", - "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 - }, - { - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "share": 1 - } - ], - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_invitation/crm_invitation.py b/crm/fcrm/doctype/crm_invitation/crm_invitation.py deleted file mode 100644 index f4c9a8f9c..000000000 --- a/crm/fcrm/doctype/crm_invitation/crm_invitation.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -class CRMInvitation(Document): - def before_insert(self): - frappe.utils.validate_email_address(self.email, True) - - self.key = frappe.generate_hash(length=12) - self.invited_by = frappe.session.user - self.status = "Pending" - - def after_insert(self): - self.invite_via_email() - - def invite_via_email(self): - invite_link = frappe.utils.get_url(f"/api/method/crm.api.accept_invitation?key={self.key}") - if frappe.local.dev_server: - print(f"Invite link for {self.email}: {invite_link}") - - title = f"Frappe CRM" - template = "crm_invitation" - - frappe.sendmail( - recipients=self.email, - subject=f"You have been invited to join {title}", - template=template, - args={"title": title, "invite_link": invite_link}, - now=True, - ) - self.db_set("email_sent_at", frappe.utils.now()) - - @frappe.whitelist() - def accept_invitation(self): - frappe.only_for("System Manager") - self.accept() - - def accept(self): - if self.status == "Expired": - frappe.throw("Invalid or expired key") - - user = self.create_user_if_not_exists() - user.append_roles(self.role) - user.save(ignore_permissions=True) - - self.status = "Accepted" - self.accepted_at = frappe.utils.now() - self.save(ignore_permissions=True) - - def create_user_if_not_exists(self): - if not frappe.db.exists("User", self.email): - first_name = self.email.split("@")[0].title() - user = frappe.get_doc( - doctype="User", - user_type="System User", - email=self.email, - send_welcome_email=0, - first_name=first_name, - ).insert(ignore_permissions=True) - else: - user = frappe.get_doc("User", self.email) - return user - - -def expire_invitations(): - """expire invitations after 3 days""" - from frappe.utils import add_days, now - - days = 3 - invitations_to_expire = frappe.db.get_all( - "CRM Invitation", filters={"status": "Pending", "creation": ["<", add_days(now(), -days)]} - ) - for invitation in invitations_to_expire: - invitation = frappe.get_doc("CRM Invitation", invitation.name) - invitation.status = "Expired" - invitation.save(ignore_permissions=True) diff --git a/crm/fcrm/doctype/crm_invitation/test_crm_invitation.py b/crm/fcrm/doctype/crm_invitation/test_crm_invitation.py deleted file mode 100644 index ddee0e24f..000000000 --- a/crm/fcrm/doctype/crm_invitation/test_crm_invitation.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestCRMInvitation(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/crm_lead/api.py b/crm/fcrm/doctype/crm_lead/api.py deleted file mode 100644 index e1bb4a4f5..000000000 --- a/crm/fcrm/doctype/crm_lead/api.py +++ /dev/null @@ -1,22 +0,0 @@ -import frappe -from frappe import _ - -from crm.api.doc import get_fields_meta, get_assigned_users -from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script - -@frappe.whitelist() -def get_lead(name): - Lead = frappe.qb.DocType("CRM Lead") - - query = frappe.qb.from_(Lead).select("*").where(Lead.name == name).limit(1) - - lead = query.run(as_dict=True) - if not len(lead): - frappe.throw(_("Lead not found"), frappe.DoesNotExistError) - lead = lead.pop() - - lead["doctype"] = "CRM Lead" - 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, lead.owner) - return lead diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.js b/crm/fcrm/doctype/crm_lead/crm_lead.js deleted file mode 100644 index 0a9d57e1f..000000000 --- a/crm/fcrm/doctype/crm_lead/crm_lead.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("CRM Lead", { - refresh(frm) { - frm.add_web_link(`/crm/leads/${frm.doc.name}`, __("Open in Portal")); - }, -}); diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.json b/crm/fcrm/doctype/crm_lead/crm_lead.json deleted file mode 100644 index 786c03a53..000000000 --- a/crm/fcrm/doctype/crm_lead/crm_lead.json +++ /dev/null @@ -1,333 +0,0 @@ -{ - "actions": [], - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", - "creation": "2023-07-24 12:19:39.616298", - "default_view": "List", - "doctype": "DocType", - "editable_grid": 1, - "email_append_to": 1, - "engine": "InnoDB", - "field_order": [ - "details", - "organization", - "website", - "territory", - "industry", - "job_title", - "source", - "lead_owner", - "person_tab", - "salutation", - "first_name", - "last_name", - "email", - "mobile_no", - "organization_tab", - "section_break_uixv", - "naming_series", - "lead_name", - "middle_name", - "gender", - "phone", - "column_break_dbsv", - "status", - "no_of_employees", - "annual_revenue", - "image", - "converted", - "sla_tab", - "sla", - "sla_creation", - "column_break_ffnp", - "sla_status", - "communication_status", - "response_details_section", - "response_by", - "column_break_pweh", - "first_response_time", - "first_responded_on", - "log_tab", - "status_change_log" - ], - "fields": [ - { - "default": "CRM-LEAD-.YYYY.-", - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "options": "CRM-LEAD-.YYYY.-" - }, - { - "fieldname": "salutation", - "fieldtype": "Link", - "label": "Salutation", - "options": "Salutation" - }, - { - "fieldname": "first_name", - "fieldtype": "Data", - "label": "First Name", - "reqd": 1 - }, - { - "fieldname": "middle_name", - "fieldtype": "Data", - "label": "Middle Name" - }, - { - "fieldname": "last_name", - "fieldtype": "Data", - "label": "Last Name" - }, - { - "fieldname": "gender", - "fieldtype": "Link", - "label": "Gender", - "options": "Gender" - }, - { - "default": "New", - "fieldname": "status", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Status", - "options": "CRM Lead Status", - "reqd": 1, - "search_index": 1 - }, - { - "fieldname": "email", - "fieldtype": "Data", - "label": "Email", - "options": "Email", - "search_index": 1 - }, - { - "fieldname": "website", - "fieldtype": "Data", - "label": "Website" - }, - { - "fieldname": "mobile_no", - "fieldtype": "Data", - "label": "Mobile No", - "options": "Phone" - }, - { - "fieldname": "phone", - "fieldtype": "Data", - "label": "Phone", - "options": "Phone" - }, - { - "fieldname": "section_break_uixv", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_dbsv", - "fieldtype": "Column Break" - }, - { - "fieldname": "no_of_employees", - "fieldtype": "Select", - "label": "No. of Employees", - "options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+" - }, - { - "fieldname": "annual_revenue", - "fieldtype": "Currency", - "label": "Annual Revenue" - }, - { - "fieldname": "lead_owner", - "fieldtype": "Link", - "label": "Lead Owner", - "options": "User" - }, - { - "fieldname": "source", - "fieldtype": "Link", - "label": "Source", - "options": "CRM Lead Source" - }, - { - "fieldname": "industry", - "fieldtype": "Link", - "label": "Industry", - "options": "CRM Industry" - }, - { - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "label": "Image", - "print_hide": 1 - }, - { - "fieldname": "lead_name", - "fieldtype": "Data", - "label": "Full Name", - "search_index": 1 - }, - { - "fieldname": "job_title", - "fieldtype": "Data", - "label": "Job Title" - }, - { - "fieldname": "organization_tab", - "fieldtype": "Tab Break", - "label": "Others", - "read_only": 1 - }, - { - "fieldname": "organization", - "fieldtype": "Data", - "label": "Organization" - }, - { - "default": "0", - "fieldname": "converted", - "fieldtype": "Check", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Converted" - }, - { - "fieldname": "person_tab", - "fieldtype": "Tab Break", - "label": "Person" - }, - { - "fieldname": "details", - "fieldtype": "Tab Break", - "label": "Details" - }, - { - "fieldname": "sla_tab", - "fieldtype": "Tab Break", - "label": "SLA", - "read_only": 1 - }, - { - "fieldname": "sla", - "fieldtype": "Link", - "label": "SLA", - "options": "CRM Service Level Agreement" - }, - { - "fieldname": "sla_creation", - "fieldtype": "Datetime", - "label": "SLA Creation", - "read_only": 1 - }, - { - "fieldname": "column_break_ffnp", - "fieldtype": "Column Break" - }, - { - "fieldname": "sla_status", - "fieldtype": "Select", - "label": "SLA Status", - "options": "\nFirst Response Due\nFailed\nFulfilled", - "read_only": 1 - }, - { - "fieldname": "response_details_section", - "fieldtype": "Section Break", - "label": "Response Details" - }, - { - "fieldname": "response_by", - "fieldtype": "Datetime", - "label": "Response By", - "read_only": 1 - }, - { - "fieldname": "column_break_pweh", - "fieldtype": "Column Break" - }, - { - "fieldname": "first_response_time", - "fieldtype": "Duration", - "label": "First Response Time", - "read_only": 1 - }, - { - "fieldname": "first_responded_on", - "fieldtype": "Datetime", - "label": "First Responded On", - "read_only": 1 - }, - { - "default": "Open", - "fieldname": "communication_status", - "fieldtype": "Link", - "label": "Communication Status", - "options": "CRM Communication Status" - }, - { - "fieldname": "territory", - "fieldtype": "Link", - "label": "Territory", - "options": "CRM Territory" - }, - { - "fieldname": "log_tab", - "fieldtype": "Tab Break", - "label": "Log", - "read_only": 1 - }, - { - "fieldname": "status_change_log", - "fieldtype": "Table", - "label": "Status Change Log", - "options": "CRM Status Change Log" - } - ], - "image_field": "image", - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-09-17 18:36:57.289897", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Lead", - "naming_rule": "By \"Naming Series\" field", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "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 - } - ], - "sender_field": "email", - "sender_name_field": "first_name", - "show_title_field_in_link": 1, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "title_field": "lead_name", - "track_changes": 1 -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.py b/crm/fcrm/doctype/crm_lead/crm_lead.py deleted file mode 100644 index 632080a90..000000000 --- a/crm/fcrm/doctype/crm_lead/crm_lead.py +++ /dev/null @@ -1,352 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt -import json - -import frappe -from frappe import _ -from frappe.desk.form.assign_to import add as assign -from frappe.model.document import Document - -from frappe.utils import has_gravatar, validate_email_address -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 - - -class CRMLead(Document): - def before_validate(self): - self.set_sla() - - def validate(self): - self.set_full_name() - self.set_lead_name() - self.set_title() - self.validate_email() - if not self.is_new() and self.has_value_changed("lead_owner") and self.lead_owner: - self.share_with_agent(self.lead_owner) - self.assign_agent(self.lead_owner) - if self.has_value_changed("status"): - add_status_change_log(self) - - def after_insert(self): - if self.lead_owner: - self.assign_agent(self.lead_owner) - - def before_save(self): - self.apply_sla() - - def set_full_name(self): - if self.first_name: - self.lead_name = " ".join( - filter(None, [self.salutation, self.first_name, self.middle_name, self.last_name]) - ) - - def set_lead_name(self): - if not self.lead_name: - # Check for leads being created through data import - if not self.organization and not self.email and not self.flags.ignore_mandatory: - frappe.throw(_("A Lead requires either a person's name or an organization's name")) - elif self.organization: - self.lead_name = self.organization - elif self.email: - self.lead_name = self.email.split("@")[0] - else: - self.lead_name = "Unnamed Lead" - - def set_title(self): - self.title = self.organization or self.lead_name - - def validate_email(self): - if self.email: - if not self.flags.ignore_email_validation: - validate_email_address(self.email, throw=True) - - if self.email == self.lead_owner: - frappe.throw(_("Lead Owner cannot be same as the Lead Email Address")) - - if self.is_new() or not self.image: - self.image = has_gravatar(self.email) - - def assign_agent(self, agent): - if not agent: - return - - assignees = self.get_assigned_users() - if assignees: - for assignee in assignees: - if agent == assignee: - # the agent is already set as an assignee - return - - assign({"assign_to": [agent], "doctype": "CRM Lead", "name": self.name}) - - def share_with_agent(self, agent): - if not agent: - return - - docshares = frappe.get_all( - "DocShare", - filters={"share_name": self.name, "share_doctype": self.doctype}, - fields=["name", "user"], - ) - - shared_with = [d.user for d in docshares] + [agent] - - for user in shared_with: - if user == agent and not frappe.db.exists("DocShare", {"user": agent, "share_name": self.name, "share_doctype": self.doctype}): - frappe.share.add_docshare( - self.doctype, self.name, agent, write=1, flags={"ignore_share_permission": True} - ) - elif user != agent: - frappe.share.remove(self.doctype, self.name, user) - - def create_contact(self, throw=True): - if not self.lead_name: - self.set_full_name() - self.set_lead_name() - - existing_contact = self.contact_exists(throw) - if existing_contact: - return existing_contact - - contact = frappe.new_doc("Contact") - contact.update( - { - "first_name": self.first_name or self.lead_name, - "last_name": self.last_name, - "salutation": self.salutation, - "gender": self.gender, - "designation": self.job_title, - "company_name": self.organization, - "image": self.image or "", - } - ) - - if self.email: - contact.append("email_ids", {"email_id": self.email, "is_primary": 1}) - - if self.phone: - contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1}) - - if self.mobile_no: - contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1}) - - contact.insert(ignore_permissions=True) - contact.reload() # load changes by hooks on contact - - return contact.name - - def create_organization(self): - if not self.organization: - return - - existing_organization = frappe.db.exists("CRM Organization", {"organization_name": self.organization}) - if existing_organization: - return existing_organization - - organization = frappe.new_doc("CRM Organization") - organization.update( - { - "organization_name": self.organization, - "website": self.website, - "territory": self.territory, - "industry": self.industry, - "annual_revenue": self.annual_revenue, - } - ) - organization.insert(ignore_permissions=True) - return organization.name - - def contact_exists(self, throw=True): - email_exist = frappe.db.exists("Contact Email", {"email_id": self.email}) - phone_exist = frappe.db.exists("Contact Phone", {"phone": self.phone}) - mobile_exist = frappe.db.exists("Contact Phone", {"phone": self.mobile_no}) - - doctype = "Contact Email" if email_exist else "Contact Phone" - name = email_exist or phone_exist or mobile_exist - - if name: - text = "Email" if email_exist else "Phone" if phone_exist else "Mobile No" - data = self.email if email_exist else self.phone if phone_exist else self.mobile_no - - value = "{0}: {1}".format(text, data) - - contact = frappe.db.get_value(doctype, name, "parent") - - if throw: - frappe.throw( - _("Contact already exists with {0}").format(value), - title=_("Contact Already Exists"), - ) - return contact - - return False - - def create_deal(self, contact, organization): - deal = frappe.new_doc("CRM Deal") - - lead_deal_map = { - "lead_owner": "deal_owner", - } - - restricted_fieldtypes = ["Tab Break", "Section Break", "Column Break", "HTML", "Button", "Attach", "Table"] - restricted_map_fields = ["name", "naming_series", "creation", "owner", "modified", "modified_by", "idx", "docstatus", "status", "email", "mobile_no", "phone", "sla", "sla_status", "response_by", "first_response_time", "first_responded_on", "communication_status", "sla_creation"] - - for field in self.meta.fields: - if field.fieldtype in restricted_fieldtypes: - continue - if field.fieldname in restricted_map_fields: - continue - - fieldname = field.fieldname - if field.fieldname in lead_deal_map: - fieldname = lead_deal_map[field.fieldname] - - if hasattr(deal, fieldname): - if fieldname == "organization": - deal.update({fieldname: organization}) - else: - deal.update({fieldname: self.get(field.fieldname)}) - - deal.update( - { - "lead": self.name, - "contacts": [{"contact": contact}], - } - ) - - if self.first_responded_on: - deal.update( - { - "sla_creation": self.sla_creation, - "response_by": self.response_by, - "sla_status": self.sla_status, - "communication_status": self.communication_status, - "first_response_time": self.first_response_time, - "first_responded_on": self.first_responded_on - } - ) - - deal.insert(ignore_permissions=True) - return deal.name - - def set_sla(self): - """ - Find an SLA to apply to the lead. - """ - if self.sla: return - - sla = get_sla(self) - if not sla: - self.first_responded_on = None - self.first_response_time = None - return - self.sla = sla.name - - def apply_sla(self): - """ - Apply SLA if set. - """ - if not self.sla: - return - sla = frappe.get_last_doc("CRM Service Level Agreement", {"name": self.sla}) - if sla: - sla.apply(self) - - def convert_to_deal(self): - return convert_to_deal(lead=self.name, doc=self) - - @staticmethod - def get_non_filterable_fields(): - return ["converted"] - - @staticmethod - def default_list_data(): - columns = [ - { - 'label': 'Name', - 'type': 'Data', - 'key': 'lead_name', - 'width': '12rem', - }, - { - 'label': 'Organization', - 'type': 'Link', - 'key': 'organization', - 'options': 'CRM Organization', - 'width': '10rem', - }, - { - 'label': 'Status', - 'type': 'Select', - 'key': 'status', - 'width': '8rem', - }, - { - 'label': 'Email', - 'type': 'Data', - 'key': 'email', - 'width': '12rem', - }, - { - 'label': 'Mobile No', - 'type': 'Data', - 'key': 'mobile_no', - 'width': '11rem', - }, - { - 'label': 'Assigned To', - 'type': 'Text', - 'key': '_assign', - 'width': '10rem', - }, - { - 'label': 'Last Modified', - 'type': 'Datetime', - 'key': 'modified', - 'width': '8rem', - }, - ] - rows = [ - "name", - "lead_name", - "organization", - "status", - "email", - "mobile_no", - "lead_owner", - "first_name", - "sla_status", - "response_by", - "first_response_time", - "first_responded_on", - "modified", - "_assign", - "image", - ] - return {'columns': columns, 'rows': rows} - - @staticmethod - def default_kanban_settings(): - return { - "column_field": "status", - "title_field": "lead_name", - "kanban_fields": '["organization", "email", "mobile_no", "_assign", "modified"]' - } - - -@frappe.whitelist() -def convert_to_deal(lead, doc=None): - if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission("CRM Lead", "write", lead): - frappe.throw(_("Not allowed to convert Lead to Deal"), frappe.PermissionError) - - lead = frappe.get_cached_doc("CRM Lead", lead) - if frappe.db.exists("CRM Lead Status", "Qualified"): - lead.status = "Qualified" - lead.converted = 1 - if lead.sla and frappe.db.exists("CRM Communication Status", "Replied"): - lead.communication_status = "Replied" - lead.save(ignore_permissions=True) - contact = lead.create_contact(False) - organization = lead.create_organization() - deal = lead.create_deal(contact, organization) - return deal diff --git a/crm/fcrm/doctype/crm_lead/test_crm_lead.py b/crm/fcrm/doctype/crm_lead/test_crm_lead.py deleted file mode 100644 index e730792ea..000000000 --- a/crm/fcrm/doctype/crm_lead/test_crm_lead.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestCRMLead(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.js b/crm/fcrm/doctype/crm_lead_source/crm_lead_source.js deleted file mode 100644 index 2330b7835..000000000 --- a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("CRM Lead Source", { -// refresh(frm) { - -// }, -// }); diff --git a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json b/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json deleted file mode 100644 index 23aea6f62..000000000 --- a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "actions": [], - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:source_name", - "creation": "2023-07-24 19:47:01.063203", - "default_view": "List", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "source_name", - "details" - ], - "fields": [ - { - "fieldname": "source_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Source Name", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "details", - "fieldtype": "Text Editor", - "label": "Details" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-01-19 21:56:04.702254", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Lead Source", - "naming_rule": "By fieldname", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "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 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.py b/crm/fcrm/doctype/crm_lead_source/crm_lead_source.py deleted file mode 100644 index 48487b406..000000000 --- a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class CRMLeadSource(Document): - pass diff --git a/crm/fcrm/doctype/crm_lead_source/test_crm_lead_source.py b/crm/fcrm/doctype/crm_lead_source/test_crm_lead_source.py deleted file mode 100644 index 31d379179..000000000 --- a/crm/fcrm/doctype/crm_lead_source/test_crm_lead_source.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestCRMLeadSource(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/crm_notification/crm_notification.py b/crm/fcrm/doctype/crm_notification/crm_notification.py deleted file mode 100644 index 69aa127d3..000000000 --- a/crm/fcrm/doctype/crm_notification/crm_notification.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2024, 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 CRMNotification(Document): - def on_update(self): - frappe.publish_realtime("crm_notification") - -def notify_user(args): - """ - Notify the assigned user - """ - args = frappe._dict(args) - if args.owner == args.assigned_to: - return - - values = frappe._dict( - doctype="CRM Notification", - from_user=args.owner, - to_user=args.assigned_to, - type=args.notification_type, - message=args.message, - notification_text=args.notification_text, - notification_type_doctype=args.reference_doctype, - notification_type_doc=args.reference_docname, - reference_doctype=args.redirect_to_doctype, - reference_name=args.redirect_to_docname, - ) - - if frappe.db.exists("CRM Notification", values): - return - frappe.get_doc(values).insert(ignore_permissions=True) \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_organization/crm_organization.js b/crm/fcrm/doctype/crm_organization/crm_organization.js deleted file mode 100644 index 763952d1f..000000000 --- a/crm/fcrm/doctype/crm_organization/crm_organization.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("CRM Organization", { -// refresh(frm) { - -// }, -// }); diff --git a/crm/fcrm/doctype/crm_organization/crm_organization.json b/crm/fcrm/doctype/crm_organization/crm_organization.json deleted file mode 100644 index 34252d1c6..000000000 --- a/crm/fcrm/doctype/crm_organization/crm_organization.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "actions": [], - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:organization_name", - "creation": "2023-11-03 16:23:59.341751", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "organization_name", - "no_of_employees", - "currency", - "annual_revenue", - "organization_logo", - "column_break_pnpp", - "website", - "territory", - "industry", - "address" - ], - "fields": [ - { - "fieldname": "organization_name", - "fieldtype": "Data", - "label": "Organization Name", - "unique": 1 - }, - { - "fieldname": "website", - "fieldtype": "Data", - "label": "Website" - }, - { - "fieldname": "organization_logo", - "fieldtype": "Attach Image", - "label": "Organization Logo" - }, - { - "fieldname": "no_of_employees", - "fieldtype": "Select", - "label": "No. of Employees", - "options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+" - }, - { - "fieldname": "column_break_pnpp", - "fieldtype": "Column Break" - }, - { - "fieldname": "annual_revenue", - "fieldtype": "Currency", - "label": "Annual Revenue", - "options": "currency" - }, - { - "fieldname": "industry", - "fieldtype": "Link", - "label": "Industry", - "options": "CRM Industry" - }, - { - "fieldname": "territory", - "fieldtype": "Link", - "label": "Territory", - "options": "CRM Territory" - }, - { - "fieldname": "currency", - "fieldtype": "Link", - "label": "Currency", - "options": "Currency" - }, - { - "fieldname": "address", - "fieldtype": "Link", - "label": "Address", - "options": "Address" - } - ], - "image_field": "organization_logo", - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-09-17 18:37:10.341062", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Organization", - "naming_rule": "By fieldname", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "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 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_organization/crm_organization.py b/crm/fcrm/doctype/crm_organization/crm_organization.py deleted file mode 100644 index 471d0f512..000000000 --- a/crm/fcrm/doctype/crm_organization/crm_organization.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -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} diff --git a/crm/fcrm/doctype/crm_organization/test_crm_organization.py b/crm/fcrm/doctype/crm_organization/test_crm_organization.py deleted file mode 100644 index e80667548..000000000 --- a/crm/fcrm/doctype/crm_organization/test_crm_organization.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestCRMOrganization(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/crm_service_day/crm_service_day.json b/crm/fcrm/doctype/crm_service_day/crm_service_day.json deleted file mode 100644 index ddca98836..000000000 --- a/crm/fcrm/doctype/crm_service_day/crm_service_day.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2023-12-04 16:07:20.400084", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "workday", - "section_break_uegc", - "start_time", - "column_break_maie", - "end_time" - ], - "fields": [ - { - "fieldname": "workday", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Workday", - "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", - "reqd": 1 - }, - { - "fieldname": "section_break_uegc", - "fieldtype": "Section Break" - }, - { - "fieldname": "start_time", - "fieldtype": "Time", - "in_list_view": 1, - "label": "Start Time", - "reqd": 1 - }, - { - "fieldname": "column_break_maie", - "fieldtype": "Column Break" - }, - { - "fieldname": "end_time", - "fieldtype": "Time", - "in_list_view": 1, - "label": "End Time", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2023-12-04 16:09:22.928308", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Service Day", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_service_day/crm_service_day.py b/crm/fcrm/doctype/crm_service_day/crm_service_day.py deleted file mode 100644 index 724a720db..000000000 --- a/crm/fcrm/doctype/crm_service_day/crm_service_day.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class CRMServiceDay(Document): - pass diff --git a/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.js b/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.js deleted file mode 100644 index 4c08f7bf3..000000000 --- a/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.js +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("CRM Service Level Agreement", { - validate(frm) { - let default_priority_count = 0; - frm.doc.priorities.forEach(function (row) { - if (row.default_priority) { - default_priority_count++; - } - }); - if (default_priority_count > 1) { - frappe.throw( - __("There can only be one default priority in Priorities table") - ); - } - }, -}); diff --git a/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.py b/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.py deleted file mode 100644 index d8f3f6698..000000000 --- a/crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.py +++ /dev/null @@ -1,225 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from datetime import timedelta -from frappe.model.document import Document -from frappe.utils import ( - add_to_date, - get_datetime, - get_weekdays, - getdate, - now_datetime, - time_diff_in_seconds, -) -from crm.fcrm.doctype.crm_service_level_agreement.utils import get_context - - -class CRMServiceLevelAgreement(Document): - def validate(self): - self.validate_default() - self.validate_condition() - - def validate_default(self): - if self.default: - other_slas = frappe.get_all( - "CRM Service Level Agreement", - filters={"apply_on": self.apply_on, "default": True}, - fields=["name"], - ) - if other_slas: - frappe.throw( - _( - "Default Service Level Agreement already exists for {0}" - ).format(self.apply_on) - ) - - def validate_condition(self): - if not self.condition: - return - try: - temp_doc = frappe.new_doc(self.apply_on) - frappe.safe_eval(self.condition, None, get_context(temp_doc)) - except Exception as e: - frappe.throw( - _("The Condition '{0}' is invalid: {1}").format(self.condition, str(e)) - ) - - def apply(self, doc: Document): - self.handle_creation(doc) - self.handle_communication_status(doc) - self.handle_targets(doc) - self.handle_sla_status(doc) - - def handle_creation(self, doc: Document): - doc.sla_creation = doc.sla_creation or now_datetime() - - def handle_communication_status(self, doc: Document): - if doc.is_new() or not doc.has_value_changed("communication_status"): - return - self.set_first_responded_on(doc) - self.set_first_response_time(doc) - - def set_first_responded_on(self, doc: Document): - if doc.communication_status != self.get_default_priority(): - doc.first_responded_on = ( - doc.first_responded_on or now_datetime() - ) - - def set_first_response_time(self, doc: Document): - start_at = doc.sla_creation - end_at = doc.first_responded_on - if not start_at or not end_at: - return - doc.first_response_time = self.calc_elapsed_time(start_at, end_at) - - def handle_targets(self, doc: Document): - self.set_response_by(doc) - - def set_response_by(self, doc: Document): - start_time = doc.sla_creation - communication_status = doc.communication_status - - priorities = self.get_priorities() - priority = priorities.get(communication_status) - if not priority or doc.response_by: - return - - first_response_time = priority.get("first_response_time", 0) - end_time = self.calc_time(start_time, first_response_time) - if end_time: - doc.response_by = end_time - - def handle_sla_status(self, doc: Document): - is_failed = self.is_first_response_failed(doc) - options = { - "Fulfilled": True, - "First Response Due": not doc.first_responded_on, - "Failed": is_failed, - } - for status in options: - if options[status]: - doc.sla_status = status - - def is_first_response_failed(self, doc: Document): - if not doc.first_responded_on: - return get_datetime(doc.response_by) < now_datetime() - return get_datetime(doc.response_by) < get_datetime(doc.first_responded_on) - - def calc_time( - self, - start_at: str, - duration_seconds: int, - ): - res = get_datetime(start_at) - time_needed = duration_seconds - holidays = self.get_holidays() - weekdays = get_weekdays() - workdays = self.get_workdays() - while time_needed: - today = res - today_day = getdate(today) - today_weekday = weekdays[today.weekday()] - is_workday = today_weekday in workdays - is_holiday = today_day in holidays - if is_holiday or not is_workday: - res = add_to_date(res, days=1, as_datetime=True) - continue - today_workday = workdays[today_weekday] - now_in_seconds = time_diff_in_seconds(today, today_day) - start_time = max(today_workday.start_time.total_seconds(), now_in_seconds) - till_start_time = max(start_time - now_in_seconds, 0) - end_time = max(today_workday.end_time.total_seconds(), now_in_seconds) - time_left = max(end_time - start_time, 0) - if not time_left: - res = getdate(add_to_date(res, days=1, as_datetime=True)) - continue - time_taken = min(time_needed, time_left) - time_needed -= time_taken - time_required = till_start_time + time_taken - res = add_to_date(res, seconds=time_required, as_datetime=True) - return res - - def calc_elapsed_time(self, start_time, end_time) -> float: - """ - Get took from start to end, excluding non-working hours - - :param start_at: Date at which calculation starts - :param end_at: Date at which calculation ends - :return: Number of seconds - """ - start_time = get_datetime(start_time) - end_time = get_datetime(end_time) - holiday_list = [] - working_day_list = self.get_working_days() - working_hours = self.get_working_hours() - - total_seconds = 0 - current_time = start_time - - while current_time < end_time: - in_holiday_list = current_time.date() in holiday_list - not_in_working_day_list = get_weekdays()[current_time.weekday()] not in working_day_list - if in_holiday_list or not_in_working_day_list or not self.is_working_time(current_time, working_hours): - current_time += timedelta(seconds=1) - continue - total_seconds += 1 - current_time += timedelta(seconds=1) - - return total_seconds - - def get_priorities(self): - """ - Return priorities related info as a dict. With `priority` as key - """ - res = {} - for row in self.priorities: - res[row.priority] = row - return res - - def get_default_priority(self): - """ - Return default priority - """ - for row in self.priorities: - if row.default_priority: - return row.priority - - return self.priorities[0].priority - - def get_workdays(self) -> dict[str, dict]: - """ - Return workdays related info as a dict. With `workday` as key - """ - res = {} - for row in self.working_hours: - res[row.workday] = row - return res - - def get_working_days(self) -> dict[str, dict]: - workdays = [] - for row in self.working_hours: - workdays.append(row.workday) - return workdays - - def get_working_hours(self) -> dict[str, dict]: - res = {} - for row in self.working_hours: - res[row.workday] = (row.start_time, row.end_time) - return res - - def is_working_time(self, date_time, working_hours): - day_of_week = get_weekdays()[date_time.weekday()] - start_time, end_time = working_hours.get(day_of_week, (0, 0)) - date_time = timedelta(hours=date_time.hour, minutes=date_time.minute, seconds=date_time.second) - return start_time <= date_time < end_time - - def get_holidays(self): - res = [] - if not self.holiday_list: - return res - holiday_list = frappe.get_doc("CRM Holiday List", self.holiday_list) - for row in holiday_list.holidays: - res.append(row.date) - return res diff --git a/crm/fcrm/doctype/crm_service_level_agreement/utils.py b/crm/fcrm/doctype/crm_service_level_agreement/utils.py deleted file mode 100644 index 809dcafe6..000000000 --- a/crm/fcrm/doctype/crm_service_level_agreement/utils.py +++ /dev/null @@ -1,61 +0,0 @@ -import frappe -from frappe.model.document import Document -from frappe.query_builder import JoinType -from frappe.utils.safe_exec import get_safe_globals -from frappe.utils import now_datetime -from pypika import Criterion - -def get_sla(doc: Document) -> Document: - """ - Get Service Level Agreement for `doc` - - :param doc: Lead/Deal to use - :return: Applicable SLA - """ - SLA = frappe.qb.DocType("CRM Service Level Agreement") - Priority = frappe.qb.DocType("CRM Service Level Priority") - now = now_datetime() - priority = doc.communication_status - q = ( - frappe.qb.from_(SLA) - .select(SLA.name, SLA.condition) - .where(SLA.apply_on == doc.doctype) - .where(SLA.enabled == True) - .where(Criterion.any([SLA.start_date.isnull(), SLA.start_date <= now])) - .where(Criterion.any([SLA.end_date.isnull(), SLA.end_date >= now])) - ) - if priority: - q = ( - q.join(Priority, JoinType.inner) - .on(Priority.parent == SLA.name) - .where(Priority.priority == priority) - ) - sla_list = q.run(as_dict=True) - res = None - - # move default sla to the end of the list - for sla in sla_list: - if sla.get("default") == True: - sla_list.remove(sla) - sla_list.append(sla) - break - - for sla in sla_list: - cond = sla.get("condition") - if not cond or frappe.safe_eval(cond, None, get_context(doc)): - res = sla - break - return res - -def get_context(d: Document) -> dict: - """ - Get safe context for `safe_eval` - - :param doc: `Document` to add in context - :return: Context with `doc` and safe variables - """ - utils = get_safe_globals().get("frappe").get("utils") - return { - "doc": d.as_dict(), - "frappe": frappe._dict(utils=utils), - } \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.js b/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.js deleted file mode 100644 index ef808235f..000000000 --- a/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("CRM Service Level Priority", { -// refresh(frm) { - -// }, -// }); diff --git a/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.json b/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.json deleted file mode 100644 index ede5c8c0e..000000000 --- a/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2023-12-04 13:18:58.028384", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "default_priority", - "column_break_grod", - "priority", - "section_break_anyl", - "first_response_time", - "column_break_bwgs" - ], - "fields": [ - { - "default": "0", - "fieldname": "default_priority", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Default Priority" - }, - { - "fieldname": "priority", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Priority", - "options": "CRM Communication Status", - "reqd": 1 - }, - { - "fieldname": "first_response_time", - "fieldtype": "Duration", - "in_list_view": 1, - "label": "First Response TIme", - "reqd": 1 - }, - { - "fieldname": "column_break_grod", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_anyl", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_bwgs", - "fieldtype": "Column Break" - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2023-12-15 11:49:54.424029", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Service Level Priority", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.py b/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.py deleted file mode 100644 index a7210a827..000000000 --- a/crm/fcrm/doctype/crm_service_level_priority/crm_service_level_priority.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class CRMServiceLevelPriority(Document): - pass diff --git a/crm/fcrm/doctype/crm_service_level_priority/test_crm_service_level_priority.py b/crm/fcrm/doctype/crm_service_level_priority/test_crm_service_level_priority.py deleted file mode 100644 index 89f9c4e39..000000000 --- a/crm/fcrm/doctype/crm_service_level_priority/test_crm_service_level_priority.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestCRMServiceLevelPriority(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.py b/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.py deleted file mode 100644 index 2a3691e48..000000000 --- a/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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 -from frappe.model.document import Document - - -class CRMStatusChangeLog(Document): - pass - -def get_duration(from_date, to_date): - if not isinstance(from_date, datetime): - from_date = get_datetime(from_date) - if not isinstance(to_date, datetime): - to_date = get_datetime(to_date) - duration = to_date - from_date - return duration.total_seconds() - -def add_status_change_log(doc): - if not doc.is_new(): - previous_status = doc.get_doc_before_save().status if doc.get_doc_before_save() 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, - }) - last_status_change = doc.status_change_log[-1] - last_status_change.to = doc.status - 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, - }) \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_task/crm_task.json b/crm/fcrm/doctype/crm_task/crm_task.json deleted file mode 100644 index cdb95c616..000000000 --- a/crm/fcrm/doctype/crm_task/crm_task.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "actions": [], - "allow_import": 1, - "autoname": "autoincrement", - "creation": "2023-09-28 15:04:28.084159", - "default_view": "List", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "title", - "priority", - "start_date", - "reference_doctype", - "reference_docname", - "column_break_cqua", - "assigned_to", - "status", - "due_date", - "section_break_bzhd", - "description" - ], - "fields": [ - { - "fieldname": "title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Title", - "reqd": 1 - }, - { - "fieldname": "priority", - "fieldtype": "Select", - "label": "Priority", - "options": "Low\nMedium\nHigh" - }, - { - "fieldname": "start_date", - "fieldtype": "Date", - "label": "Start Date" - }, - { - "fieldname": "column_break_cqua", - "fieldtype": "Column Break" - }, - { - "fieldname": "assigned_to", - "fieldtype": "Link", - "label": "Assigned To", - "options": "User" - }, - { - "fieldname": "status", - "fieldtype": "Select", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Status", - "options": "Backlog\nTodo\nIn Progress\nDone\nCanceled" - }, - { - "fieldname": "due_date", - "fieldtype": "Datetime", - "label": "Due Date" - }, - { - "fieldname": "section_break_bzhd", - "fieldtype": "Section Break" - }, - { - "fieldname": "description", - "fieldtype": "Text Editor", - "label": "Description" - }, - { - "default": "CRM Lead", - "fieldname": "reference_doctype", - "fieldtype": "Link", - "label": "Reference Document Type", - "options": "DocType" - }, - { - "fieldname": "reference_docname", - "fieldtype": "Dynamic Link", - "label": "Reference Doc", - "options": "reference_doctype" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-02-08 12:04:00.955984", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Task", - "naming_rule": "Autoincrement", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "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 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_task/crm_task.py b/crm/fcrm/doctype/crm_task/crm_task.py deleted file mode 100644 index 06b50c4de..000000000 --- a/crm/fcrm/doctype/crm_task/crm_task.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.desk.form.assign_to import add as assign, remove as unassign -from crm.fcrm.doctype.crm_notification.crm_notification import notify_user - - -class CRMTask(Document): - def after_insert(self): - self.assign_to() - - def validate(self): - if self.is_new() or not self.assigned_to: - return - - if self.get_doc_before_save().assigned_to != self.assigned_to: - self.unassign_from_previous_user(self.get_doc_before_save().assigned_to) - self.assign_to() - - def unassign_from_previous_user(self, user): - unassign(self.doctype, self.name, user) - - def assign_to(self): - if self.assigned_to: - assign({ - "assign_to": [self.assigned_to], - "doctype": self.doctype, - "name": self.name, - "description": self.title or self.description, - }) - - - @staticmethod - def default_list_data(): - columns = [ - { - 'label': 'Title', - 'type': 'Data', - 'key': 'title', - 'width': '16rem', - }, - { - 'label': 'Status', - 'type': 'Select', - 'key': 'status', - 'width': '8rem', - }, - { - 'label': 'Priority', - 'type': 'Select', - 'key': 'priority', - 'width': '8rem', - }, - { - 'label': 'Due Date', - 'type': 'Date', - 'key': 'due_date', - 'width': '8rem', - }, - { - 'label': 'Assigned To', - 'type': 'Link', - 'key': 'assigned_to', - 'width': '10rem', - }, - { - 'label': 'Last Modified', - 'type': 'Datetime', - 'key': 'modified', - 'width': '8rem', - }, - ] - - rows = [ - "name", - "title", - "description", - "assigned_to", - "due_date", - "status", - "priority", - "reference_doctype", - "reference_docname", - "modified", - ] - return {'columns': columns, 'rows': rows} - - @staticmethod - def default_kanban_settings(): - return { - "column_field": "status", - "title_field": "title", - "kanban_fields": '["description", "priority", "creation"]' - } diff --git a/crm/fcrm/doctype/crm_task/test_crm_task.py b/crm/fcrm/doctype/crm_task/test_crm_task.py deleted file mode 100644 index d632cebe2..000000000 --- a/crm/fcrm/doctype/crm_task/test_crm_task.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestCRMTask(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/crm_territory/__init__.py b/crm/fcrm/doctype/crm_territory/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/fcrm/doctype/crm_territory/crm_territory.js b/crm/fcrm/doctype/crm_territory/crm_territory.js deleted file mode 100644 index 568e9c4c7..000000000 --- a/crm/fcrm/doctype/crm_territory/crm_territory.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("CRM Territory", { -// refresh(frm) { - -// }, -// }); diff --git a/crm/fcrm/doctype/crm_territory/crm_territory.json b/crm/fcrm/doctype/crm_territory/crm_territory.json deleted file mode 100644 index 91e3b09cb..000000000 --- a/crm/fcrm/doctype/crm_territory/crm_territory.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "actions": [], - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:territory_name", - "creation": "2024-01-04 18:52:58.872535", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "territory_name", - "column_break_mckp", - "territory_manager", - "section_break_qhaf", - "old_parent", - "parent_crm_territory", - "column_break_pypy", - "lft", - "rgt", - "is_group" - ], - "fields": [ - { - "fieldname": "territory_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Territory Name", - "reqd": 1, - "unique": 1 - }, - { - "default": "0", - "fieldname": "is_group", - "fieldtype": "Check", - "label": "Is Group" - }, - { - "fieldname": "column_break_pypy", - "fieldtype": "Column Break" - }, - { - "fieldname": "territory_manager", - "fieldtype": "Link", - "label": "Territory Manager", - "options": "User" - }, - { - "fieldname": "lft", - "fieldtype": "Int", - "hidden": 1, - "label": "Left", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "rgt", - "fieldtype": "Int", - "hidden": 1, - "label": "Right", - "no_copy": 1, - "read_only": 1 - }, - { - "default": "0", - "fieldname": "is_group", - "fieldtype": "Check", - "label": "Is Group" - }, - { - "fieldname": "old_parent", - "fieldtype": "Link", - "label": "Old Parent", - "options": "CRM Territory" - }, - { - "fieldname": "parent_crm_territory", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Parent CRM Territory", - "options": "CRM Territory" - }, - { - "fieldname": "column_break_mckp", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_qhaf", - "fieldtype": "Section Break" - } - ], - "index_web_pages_for_search": 1, - "is_tree": 1, - "links": [], - "modified": "2024-01-19 21:53:53.451891", - "modified_by": "Administrator", - "module": "FCRM", - "name": "CRM Territory", - "naming_rule": "By fieldname", - "nsm_parent_field": "parent_crm_territory", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "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 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/crm_territory/crm_territory.py b/crm/fcrm/doctype/crm_territory/crm_territory.py deleted file mode 100644 index d08f3bee4..000000000 --- a/crm/fcrm/doctype/crm_territory/crm_territory.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class CRMTerritory(Document): - pass diff --git a/crm/fcrm/doctype/crm_view_settings/__init__.py b/crm/fcrm/doctype/crm_view_settings/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py b/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py deleted file mode 100644 index e0cf53d5e..000000000 --- a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt -import json -import frappe -from frappe.model.document import Document, get_controller -from frappe.utils import parse_json - - -class CRMViewSettings(Document): - pass - -@frappe.whitelist() -def create(view): - view = frappe._dict(view) - - view.filters = parse_json(view.filters) or {} - view.columns = parse_json(view.columns or '[]') - view.rows = parse_json(view.rows or '[]') - view.kanban_columns = parse_json(view.kanban_columns or '[]') - view.kanban_fields = parse_json(view.kanban_fields or '[]') - - default_rows = sync_default_rows(view.doctype) - view.rows = view.rows + default_rows if default_rows else view.rows - view.rows = remove_duplicates(view.rows) - - if not view.kanban_columns and view.type == "kanban": - view.kanban_columns = sync_default_columns(view) - elif not view.columns: - view.columns = sync_default_columns(view) - - doc = frappe.new_doc("CRM View Settings") - doc.name = view.label - doc.label = view.label - doc.type = view.type or 'list' - doc.icon = view.icon - doc.dt = view.doctype - doc.user = frappe.session.user - doc.route_name = view.route_name or "" - doc.load_default_columns = view.load_default_columns or False - doc.filters = json.dumps(view.filters) - doc.order_by = view.order_by - doc.group_by_field = view.group_by_field - doc.column_field = view.column_field - doc.title_field = view.title_field - doc.kanban_columns = json.dumps(view.kanban_columns) - doc.kanban_fields = json.dumps(view.kanban_fields) - doc.columns = json.dumps(view.columns) - doc.rows = json.dumps(view.rows) - doc.insert() - return doc - -@frappe.whitelist() -def update(view): - view = frappe._dict(view) - - filters = parse_json(view.filters or {}) - columns = parse_json(view.columns or []) - rows = parse_json(view.rows or []) - kanban_columns = parse_json(view.kanban_columns or []) - kanban_fields = parse_json(view.kanban_fields or []) - - default_rows = sync_default_rows(view.doctype) - rows = rows + default_rows if default_rows else rows - rows = remove_duplicates(rows) - - doc = frappe.get_doc("CRM View Settings", view.name) - doc.label = view.label - doc.type = view.type or 'list' - doc.icon = view.icon - doc.route_name = view.route_name or "" - doc.load_default_columns = view.load_default_columns or False - doc.filters = json.dumps(filters) - doc.order_by = view.order_by - doc.group_by_field = view.group_by_field - doc.column_field = view.column_field - doc.title_field = view.title_field - doc.kanban_columns = json.dumps(kanban_columns) - doc.kanban_fields = json.dumps(kanban_fields) - doc.columns = json.dumps(columns) - doc.rows = json.dumps(rows) - doc.save() - return doc - -@frappe.whitelist() -def delete(name): - if frappe.db.exists("CRM View Settings", name): - frappe.delete_doc("CRM View Settings", name) - -@frappe.whitelist() -def public(name, value): - if frappe.session.user != "Administrator" and "Sales Manager" not in frappe.get_roles(): - frappe.throw("Not permitted", frappe.PermissionError) - - doc = frappe.get_doc("CRM View Settings", name) - if doc.pinned: - doc.pinned = False - doc.public = value - doc.user = "" if value else frappe.session.user - doc.save() - -@frappe.whitelist() -def pin(name, value): - doc = frappe.get_doc("CRM View Settings", name) - doc.pinned = value - doc.save() - -def remove_duplicates(l): - return list(dict.fromkeys(l)) - -def sync_default_rows(doctype, type="list"): - list = get_controller(doctype) - rows = [] - - if hasattr(list, "default_list_data"): - rows = list.default_list_data().get("rows") - - return rows - -def sync_default_columns(view): - list = get_controller(view.doctype) - columns = [] - - if view.type == "kanban" and view.column_field: - field_meta = frappe.get_meta(view.doctype).get_field(view.column_field) - if field_meta.fieldtype == "Link": - columns = frappe.get_all( - field_meta.options, - fields=["name"], - order_by="modified asc", - ) - elif field_meta.fieldtype == "Select": - columns = [{"name": option} for option in field_meta.options.split("\n")] - elif hasattr(list, "default_list_data"): - columns = list.default_list_data().get("columns") - - return columns - - -@frappe.whitelist() -def create_or_update_default_view(view): - view = frappe._dict(view) - - filters = parse_json(view.filters) or {} - columns = parse_json(view.columns or '[]') - rows = parse_json(view.rows or '[]') - kanban_columns = parse_json(view.kanban_columns or '[]') - kanban_fields = parse_json(view.kanban_fields or '[]') - - default_rows = sync_default_rows(view.doctype, view.type) - rows = rows + default_rows if default_rows else rows - rows = remove_duplicates(rows) - - if not kanban_columns and view.type == "kanban": - kanban_columns = sync_default_columns(view) - elif not columns: - columns = sync_default_columns(view) - - doc = frappe.db.exists( - "CRM View Settings", - { - "dt": view.doctype, - "type": view.type or 'list', - "is_default": True, - "user": frappe.session.user - }, - ) - if doc: - doc = frappe.get_doc("CRM View Settings", doc) - doc.label = view.label - doc.type = view.type or 'list' - doc.route_name = view.route_name or "" - doc.load_default_columns = view.load_default_columns or False - doc.filters = json.dumps(filters) - doc.order_by = view.order_by - doc.group_by_field = view.group_by_field - doc.column_field = view.column_field - doc.title_field = view.title_field - doc.kanban_columns = json.dumps(kanban_columns) - doc.kanban_fields = json.dumps(kanban_fields) - doc.columns = json.dumps(columns) - doc.rows = json.dumps(rows) - doc.save() - else: - doc = frappe.new_doc("CRM View Settings") - label = 'Group By View' if view.type == 'group_by' else 'List View' - doc.name = view.label or label - doc.label = view.label or label - doc.type = view.type or 'list' - doc.dt = view.doctype - doc.user = frappe.session.user - doc.route_name = view.route_name or "" - doc.load_default_columns = view.load_default_columns or False - doc.filters = json.dumps(filters) - doc.order_by = view.order_by - doc.group_by_field = view.group_by_field - doc.column_field = view.column_field - doc.title_field = view.title_field - doc.kanban_columns = json.dumps(kanban_columns) - doc.kanban_fields = json.dumps(kanban_fields) - doc.columns = json.dumps(columns) - doc.rows = json.dumps(rows) - doc.is_default = True - doc.insert() \ No newline at end of file diff --git a/crm/fcrm/doctype/erpnext_crm_settings/__init__.py b/crm/fcrm/doctype/erpnext_crm_settings/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.js b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.js deleted file mode 100644 index 535e83fbd..000000000 --- a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.js +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("ERPNext CRM Settings", { - refresh(frm) { - if (!frm.doc.enabled) return; - frm.add_custom_button(__("Reset ERPNext Form Script"), () => { - frappe.confirm( - __( - "Are you sure you want to reset 'Create Quotation from CRM Deal' Form Script?" - ), - () => frm.trigger("reset_erpnext_form_script") - ); - }); - }, - async reset_erpnext_form_script(frm) { - let script = await frm.call("reset_erpnext_form_script"); - script.message && - frappe.msgprint(__("Form Script updated successfully")); - }, -}); diff --git a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json deleted file mode 100644 index 9a6f0f75e..000000000 --- a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2024-07-02 15:23:17.022214", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "enabled", - "is_erpnext_in_different_site", - "column_break_vfru", - "erpnext_company", - "section_break_oubd", - "erpnext_site_url", - "column_break_fllx", - "api_key", - "api_secret", - "section_break_jnbn", - "create_customer_on_status_change", - "column_break_kbhw", - "deal_status" - ], - "fields": [ - { - "depends_on": "eval:doc.enabled && doc.is_erpnext_in_different_site", - "fieldname": "api_key", - "fieldtype": "Data", - "label": "API Key", - "mandatory_depends_on": "is_erpnext_in_different_site" - }, - { - "depends_on": "eval:doc.enabled && doc.is_erpnext_in_different_site", - "fieldname": "api_secret", - "fieldtype": "Password", - "label": "API Secret", - "mandatory_depends_on": "is_erpnext_in_different_site" - }, - { - "depends_on": "enabled", - "fieldname": "section_break_oubd", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_fllx", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:doc.enabled && doc.is_erpnext_in_different_site", - "fieldname": "erpnext_site_url", - "fieldtype": "Data", - "label": "ERPNext Site URL", - "mandatory_depends_on": "is_erpnext_in_different_site" - }, - { - "depends_on": "enabled", - "fieldname": "erpnext_company", - "fieldtype": "Data", - "label": "Company in ERPNext Site", - "mandatory_depends_on": "enabled" - }, - { - "fieldname": "column_break_vfru", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "enabled", - "fieldtype": "Check", - "label": "Enabled" - }, - { - "default": "0", - "depends_on": "enabled", - "fieldname": "is_erpnext_in_different_site", - "fieldtype": "Check", - "label": "Is ERPNext installed on a different site?" - }, - { - "fieldname": "section_break_jnbn", - "fieldtype": "Section Break" - }, - { - "default": "0", - "depends_on": "enabled", - "fieldname": "create_customer_on_status_change", - "fieldtype": "Check", - "label": "Create customer on status change" - }, - { - "fieldname": "column_break_kbhw", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:doc.enabled && doc.create_customer_on_status_change", - "fieldname": "deal_status", - "fieldtype": "Link", - "label": "Deal Status", - "mandatory_depends_on": "create_customer_on_status_change", - "options": "CRM Deal Status" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2024-09-17 19:21:11.060901", - "modified_by": "Administrator", - "module": "FCRM", - "name": "ERPNext CRM Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py b/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py deleted file mode 100644 index 2e15a0118..000000000 --- a/crm/fcrm/doctype/erpnext_crm_settings/erpnext_crm_settings.py +++ /dev/null @@ -1,272 +0,0 @@ -# Copyright (c) 2024, Frappe and contributors -# For license information, please see license.txt - -import frappe -from frappe import _ -from frappe.custom.doctype.property_setter.property_setter import make_property_setter -from frappe.model.document import Document -from frappe.frappeclient import FrappeClient -from frappe.utils import get_url_to_form, get_url_to_list -import json - -class ERPNextCRMSettings(Document): - def validate(self): - if self.enabled: - self.validate_if_erpnext_installed() - self.add_quotation_to_option() - self.create_custom_fields() - self.create_crm_form_script() - - def validate_if_erpnext_installed(self): - if not self.is_erpnext_in_different_site: - if "erpnext" not in frappe.get_installed_apps(): - frappe.throw(_("ERPNext is not installed in the current site")) - - def add_quotation_to_option(self): - if not self.is_erpnext_in_different_site: - if not frappe.db.exists("Property Setter", {"name": "Quotation-quotation_to-link_filters"}): - make_property_setter( - doctype="Quotation", - fieldname="quotation_to", - property="link_filters", - value='[["DocType","name","in", ["Customer", "Lead", "Prospect", "CRM Deal"]]]', - property_type="JSON", - validate_fields_for_doctype=False, - ) - - def create_custom_fields(self): - if not self.is_erpnext_in_different_site: - from erpnext.crm.frappe_crm_api import create_custom_fields_for_frappe_crm - create_custom_fields_for_frappe_crm() - else: - self.create_custom_fields_in_remote_site() - - def create_custom_fields_in_remote_site(self): - client = get_erpnext_site_client(self) - try: - client.post_api("erpnext.crm.frappe_crm_api.create_custom_fields_for_frappe_crm") - except Exception: - frappe.log_error( - frappe.get_traceback(), - f"Error while creating custom field in the remote erpnext site: {self.erpnext_site_url}" - ) - frappe.throw("Error while creating custom field in ERPNext, check error log for more details") - - def create_crm_form_script(self): - if not frappe.db.exists("CRM Form Script", "Create Quotation from CRM Deal"): - script = get_crm_form_script() - frappe.get_doc({ - "doctype": "CRM Form Script", - "name": "Create Quotation from CRM Deal", - "dt": "CRM Deal", - "view": "Form", - "script": script, - "enabled": 1, - "is_standard": 1 - }).insert() - - @frappe.whitelist() - def reset_erpnext_form_script(self): - try: - if frappe.db.exists("CRM Form Script", "Create Quotation from CRM Deal"): - script = get_crm_form_script() - frappe.db.set_value("CRM Form Script", "Create Quotation from CRM Deal", "script", script) - return True - return False - except Exception: - frappe.log_error(frappe.get_traceback(), "Error while resetting form script") - return False - -def get_erpnext_site_client(erpnext_crm_settings): - site_url = erpnext_crm_settings.erpnext_site_url - api_key = erpnext_crm_settings.api_key - api_secret = erpnext_crm_settings.get_password("api_secret", raise_exception=False) - - return FrappeClient( - site_url, api_key=api_key, api_secret=api_secret - ) - -@frappe.whitelist() -def get_customer_link(crm_deal): - erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings") - if not erpnext_crm_settings.enabled: - frappe.throw(_("ERPNext is not integrated with the CRM")) - - if not erpnext_crm_settings.is_erpnext_in_different_site: - customer = frappe.db.exists("Customer", {"crm_deal": crm_deal}) - return get_url_to_form("Customer", customer) if customer else "" - else: - client = get_erpnext_site_client(erpnext_crm_settings) - try: - customer = client.get_list("Customer", {"crm_deal": crm_deal}) - customer = customer[0].get("name") if len(customer) else None - if customer: - return f"{erpnext_crm_settings.erpnext_site_url}/app/customer/{customer}" - else: - return "" - except Exception: - frappe.log_error( - frappe.get_traceback(), - f"Error while fetching customer in remote site: {erpnext_crm_settings.erpnext_site_url}" - ) - frappe.throw(_("Error while fetching customer in ERPNext, check error log for more details")) - - -@frappe.whitelist() -def get_quotation_url(crm_deal, organization): - erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings") - if not erpnext_crm_settings.enabled: - frappe.throw(_("ERPNext is not integrated with the CRM")) - - if not erpnext_crm_settings.is_erpnext_in_different_site: - quotation_url = get_url_to_list("Quotation") - return f"{quotation_url}/new?quotation_to=CRM Deal&crm_deal={crm_deal}&party_name={crm_deal}&company={erpnext_crm_settings.erpnext_company}" - else: - site_url = erpnext_crm_settings.get("erpnext_site_url") - quotation_url = f"{site_url}/app/quotation" - - prospect = create_prospect_in_remote_site(crm_deal, erpnext_crm_settings) - return f"{quotation_url}/new?quotation_to=Prospect&crm_deal={crm_deal}&party_name={prospect}&company={erpnext_crm_settings.erpnext_company}" - -def create_prospect_in_remote_site(crm_deal, erpnext_crm_settings): - try: - client = get_erpnext_site_client(erpnext_crm_settings) - doc = frappe.get_doc("CRM Deal", crm_deal) - contacts = get_contacts(doc) - address = get_organization_address(doc.organization) - return client.post_api("erpnext.crm.frappe_crm_api.create_prospect_against_crm_deal", - { - "organization": doc.organization, - "lead_name": doc.lead_name, - "no_of_employees": doc.no_of_employees, - "deal_owner": doc.deal_owner, - "crm_deal": doc.name, - "territory": doc.territory, - "industry": doc.industry, - "website": doc.website, - "annual_revenue": doc.annual_revenue, - "contacts": json.dumps(contacts), - "erpnext_company": erpnext_crm_settings.erpnext_company, - "address": address.as_dict() if address else None - }, - ) - except Exception: - frappe.log_error( - frappe.get_traceback(), - f"Error while creating prospect in remote site: {erpnext_crm_settings.erpnext_site_url}" - ) - frappe.throw(_("Error while creating prospect in ERPNext, check error log for more details")) - -def get_contacts(doc): - contacts = [] - for c in doc.contacts: - contacts.append({ - "contact": c.contact, - "full_name": c.full_name, - "email": c.email, - "mobile_no": c.mobile_no, - "gender": c.gender, - "is_primary": c.is_primary, - }) - return contacts - -def get_organization_address(organization): - address = frappe.db.get_value("CRM Organization", organization, "address") - address = frappe.get_doc("Address", address) if address else None - if not address: - return None - return { - "name": address.name, - "address_title": address.address_title, - "address_type": address.address_type, - "address_line1": address.address_line1, - "address_line2": address.address_line2, - "city": address.city, - "county": address.county, - "state": address.state, - "country": address.country, - "pincode": address.pincode, - } - -def create_customer_in_erpnext(doc, method): - erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings") - if ( - not erpnext_crm_settings.enabled - or not erpnext_crm_settings.create_customer_on_status_change - or doc.status != erpnext_crm_settings.deal_status - ): - return - - contacts = get_contacts(doc) - address = get_organization_address(doc.organization) - customer = { - "customer_name": doc.organization, - "customer_group": "All Customer Groups", - "customer_type": "Company", - "territory": doc.territory, - "default_currency": doc.currency, - "industry": doc.industry, - "website": doc.website, - "crm_deal": doc.name, - "contacts": json.dumps(contacts), - "address": json.dumps(address) if address else None, - } - if not erpnext_crm_settings.is_erpnext_in_different_site: - from erpnext.crm.frappe_crm_api import create_customer - create_customer(customer) - else: - create_customer_in_remote_site(customer, erpnext_crm_settings) - - frappe.publish_realtime("crm_customer_created") - -def create_customer_in_remote_site(customer, erpnext_crm_settings): - client = get_erpnext_site_client(erpnext_crm_settings) - try: - client.post_api("erpnext.crm.frappe_crm_api.create_customer", customer) - except Exception: - frappe.log_error( - frappe.get_traceback(), - "Error while creating customer in remote site" - ) - frappe.throw(_("Error while creating customer in ERPNext, check error log for more details")) - -@frappe.whitelist() -def get_crm_form_script(): - return """ -async function setupForm({ doc, call, $dialog, updateField, createToast }) { - 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) { - actions.push({ - label: __("Create Quotation"), - onClick: async () => { - let quotation_url = await call( - "crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.get_quotation_url", - { - crm_deal: doc.name, - organization: doc.organization - } - ); - - if (quotation_url) { - window.open(quotation_url, '_blank'); - } - } - }) - } - if (is_erpnext_integration_enabled) { - let customer_url = await call("crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.get_customer_link", { - crm_deal: doc.name - }); - if (customer_url) { - actions.push({ - label: __("View Customer"), - onClick: () => window.open(customer_url, '_blank') - }); - } - } - return { - actions: actions, - }; -} -""" diff --git a/crm/fcrm/doctype/fcrm_note/__init__.py b/crm/fcrm/doctype/fcrm_note/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/fcrm/doctype/fcrm_note/fcrm_note.js b/crm/fcrm/doctype/fcrm_note/fcrm_note.js deleted file mode 100644 index 843e8f734..000000000 --- a/crm/fcrm/doctype/fcrm_note/fcrm_note.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("FCRM Note", { -// refresh(frm) { - -// }, -// }); diff --git a/crm/fcrm/doctype/fcrm_note/fcrm_note.py b/crm/fcrm/doctype/fcrm_note/fcrm_note.py deleted file mode 100644 index c81538d8c..000000000 --- a/crm/fcrm/doctype/fcrm_note/fcrm_note.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class FCRMNote(Document): - @staticmethod - def default_list_data(): - rows = [ - "name", - "title", - "content", - "reference_doctype", - "reference_docname", - "owner", - "modified", - ] - return {'columns': [], 'rows': rows} diff --git a/crm/fcrm/doctype/fcrm_settings/__init__.py b/crm/fcrm/doctype/fcrm_settings/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.js b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.js deleted file mode 100644 index 4264130fc..000000000 --- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.js +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("FCRM Settings", { - // refresh(frm) { - - // }, - restore_defaults: function (frm) { - let message = __( - "This will restore (if not exist) all the default statuses, custom fields and layouts. Delete & Restore will delete default layouts and then restore them." - ); - let d = new frappe.ui.Dialog({ - title: __("Restore Defaults"), - primary_action_label: __("Restore"), - primary_action: () => { - frm.call("restore_defaults", { force: false }, () => { - frappe.show_alert({ - message: __( - "Default statuses, custom fields and layouts restored successfully." - ), - indicator: "green", - }); - }); - d.hide(); - }, - secondary_action_label: __("Delete & Restore"), - secondary_action: () => { - frm.call("restore_defaults", { force: true }, () => { - frappe.show_alert({ - message: __( - "Default statuses, custom fields and layouts restored successfully." - ), - indicator: "green", - }); - }); - d.hide(); - }, - }); - d.show(); - d.set_message(message); - }, -}); diff --git a/crm/fcrm/doctype/fcrm_settings/test_fcrm_settings.py b/crm/fcrm/doctype/fcrm_settings/test_fcrm_settings.py deleted file mode 100644 index 8e626f3e5..000000000 --- a/crm/fcrm/doctype/fcrm_settings/test_fcrm_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import UnitTestCase - - -class TestFCRMSettings(UnitTestCase): - pass diff --git a/crm/fcrm/doctype/twilio_agents/__init__.py b/crm/fcrm/doctype/twilio_agents/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/fcrm/doctype/twilio_settings/__init__.py b/crm/fcrm/doctype/twilio_settings/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/fcrm/doctype/twilio_settings/twilio_settings.py b/crm/fcrm/doctype/twilio_settings/twilio_settings.py deleted file mode 100644 index 1d3a20b63..000000000 --- a/crm/fcrm/doctype/twilio_settings/twilio_settings.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document -from frappe import _ - -from twilio.rest import Client - -class TwilioSettings(Document): - friendly_resource_name = "Frappe CRM" # System creates TwiML app & API keys with this name. - - def validate(self): - self.validate_twilio_account() - - def on_update(self): - # Single doctype records are created in DB at time of installation and those field values are set as null. - # This condition make sure that we handle null. - if not self.account_sid: - return - - twilio = Client(self.account_sid, self.get_password("auth_token")) - self.set_api_credentials(twilio) - self.set_application_credentials(twilio) - self.reload() - - def validate_twilio_account(self): - try: - twilio = Client(self.account_sid, self.get_password("auth_token")) - twilio.api.accounts(self.account_sid).fetch() - return twilio - except Exception: - frappe.throw(_("Invalid Account SID or Auth Token.")) - - def set_api_credentials(self, twilio): - """Generate Twilio API credentials if not exist and update them. - """ - if self.api_key and self.api_secret: - return - new_key = self.create_api_key(twilio) - self.api_key = new_key.sid - self.api_secret = new_key.secret - frappe.db.set_value('Twilio Settings', 'Twilio Settings', { - 'api_key': self.api_key, - 'api_secret': self.api_secret - }) - - def set_application_credentials(self, twilio): - """Generate TwiML app credentials if not exist and update them. - """ - credentials = self.get_application(twilio) or self.create_application(twilio) - self.twiml_sid = credentials.sid - frappe.db.set_value('Twilio Settings', 'Twilio Settings', 'twiml_sid', self.twiml_sid) - - def create_api_key(self, twilio): - """Create API keys in twilio account. - """ - try: - return twilio.new_keys.create(friendly_name=self.friendly_resource_name) - except Exception: - frappe.log_error(title=_("Twilio API credential creation error.")) - frappe.throw(_("Twilio API credential creation error.")) - - def get_twilio_voice_url(self): - url_path = "/api/method/crm.integrations.twilio.api.voice" - return get_public_url(url_path) - - def get_application(self, twilio, friendly_name=None): - """Get TwiML App from twilio account if exists. - """ - friendly_name = friendly_name or self.friendly_resource_name - applications = twilio.applications.list(friendly_name) - return applications and applications[0] - - def create_application(self, twilio, friendly_name=None): - """Create TwilML App in twilio account. - """ - friendly_name = friendly_name or self.friendly_resource_name - application = twilio.applications.create( - voice_method='POST', - voice_url=self.get_twilio_voice_url(), - friendly_name=friendly_name - ) - return application - -def get_public_url(path: str=None): - from frappe.utils import get_url - return get_url().split(":8", 1)[0] + path \ No newline at end of file diff --git a/crm/hooks.py b/crm/hooks.py deleted file mode 100644 index dfc4c9d75..000000000 --- a/crm/hooks.py +++ /dev/null @@ -1,253 +0,0 @@ -app_name = "crm" -app_title = "Frappe CRM" -app_publisher = "Frappe Technologies Pvt. Ltd." -app_description = "Kick-ass Open Source CRM" -app_email = "shariq@frappe.io" -app_license = "AGPLv3" -app_icon_url = "/assets/crm/images/logo.svg" -app_icon_title = "CRM" -app_icon_route = "/crm" - -# Apps -# ------------------ - -# required_apps = [] -add_to_apps_screen = [ - { - "name": "crm", - "logo": "/assets/crm/images/logo.svg", - "title": "CRM", - "route": "/crm", - "has_permission": "crm.api.check_app_permission", - } -] - -# Includes in -# ------------------ - -# include js, css files in header of desk.html -# app_include_css = "/assets/crm/css/crm.css" -# app_include_js = "/assets/crm/js/crm.js" - -# include js, css files in header of web template -# web_include_css = "/assets/crm/css/crm.css" -# web_include_js = "/assets/crm/js/crm.js" - -# include custom scss in every website theme (without file extension ".scss") -# website_theme_scss = "crm/public/scss/website" - -# include js, css files in header of web form -# webform_include_js = {"doctype": "public/js/doctype.js"} -# webform_include_css = {"doctype": "public/css/doctype.css"} - -# include js in page -# page_js = {"page" : "public/js/file.js"} - -# include js in doctype views -# doctype_js = {"doctype" : "public/js/doctype.js"} -# doctype_list_js = {"doctype" : "public/js/doctype_list.js"} -# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} -# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} - -# Home Pages -# ---------- - -# application home page (will override Website Settings) -# home_page = "login" - -# website user home page (by Role) -# role_home_page = { -# "Role": "home_page" -# } - -website_route_rules = [ - {"from_route": "/crm/", "to_route": "crm"}, -] - -# Generators -# ---------- - -# automatically create page for each record of this doctype -# website_generators = ["Web Page"] - -# Jinja -# ---------- - -# add methods and filters to jinja environment -# jinja = { -# "methods": "crm.utils.jinja_methods", -# "filters": "crm.utils.jinja_filters" -# } - -# Installation -# ------------ - -before_install = "crm.install.before_install" -after_install = "crm.install.after_install" - -# Uninstallation -# ------------ - -before_uninstall = "crm.uninstall.before_uninstall" -# after_uninstall = "crm.uninstall.after_uninstall" - -# Integration Setup -# ------------------ -# To set up dependencies/integrations with other apps -# Name of the app being installed is passed as an argument - -# before_app_install = "crm.utils.before_app_install" -# after_app_install = "crm.utils.after_app_install" - -# Integration Cleanup -# ------------------- -# To clean up dependencies/integrations with other apps -# Name of the app being uninstalled is passed as an argument - -# before_app_uninstall = "crm.utils.before_app_uninstall" -# after_app_uninstall = "crm.utils.after_app_uninstall" - -# Desk Notifications -# ------------------ -# See frappe.core.notifications.get_notification_config - -# notification_config = "crm.notifications.get_notification_config" - -# Permissions -# ----------- -# Permissions evaluated in scripted ways - -# permission_query_conditions = { -# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", -# } -# -# has_permission = { -# "Event": "frappe.desk.doctype.event.event.has_permission", -# } - -# DocType Class -# --------------- -# Override standard doctype classes - -override_doctype_class = { - "Contact": "crm.overrides.contact.CustomContact", - "Email Template": "crm.overrides.email_template.CustomEmailTemplate", - "User": "crm.overrides.user.CustomUser", -} - -# Document Events -# --------------- -# Hook on document methods and events - -doc_events = { - "Contact": { - "validate": ["crm.api.contact.validate"], - }, - "ToDo": { - "after_insert": ["crm.api.todo.after_insert"], - "on_update": ["crm.api.todo.on_update"], - }, - "Comment": { - "on_update": ["crm.api.comment.on_update"], - }, - "WhatsApp Message": { - "validate": ["crm.api.whatsapp.validate"], - "on_update": ["crm.api.whatsapp.on_update"], - }, - "CRM Deal": { - "on_update": ["crm.fcrm.doctype.erpnext_crm_settings.erpnext_crm_settings.create_customer_in_erpnext"], - }, - "User": { - "before_validate": ["crm.api.demo.validate_user"], - } -} - -# Scheduled Tasks -# --------------- - -# scheduler_events = { -# "all": [ -# "crm.tasks.all" -# ], -# "daily": [ -# "crm.tasks.daily" -# ], -# "hourly": [ -# "crm.tasks.hourly" -# ], -# "weekly": [ -# "crm.tasks.weekly" -# ], -# "monthly": [ -# "crm.tasks.monthly" -# ], -# } - -# Testing -# ------- - -# before_tests = "crm.install.before_tests" - -# Overriding Methods -# ------------------------------ -# -# override_whitelisted_methods = { -# "frappe.desk.doctype.event.event.get_events": "crm.event.get_events" -# } -# -# each overriding function accepts a `data` argument; -# generated from the base implementation of the doctype dashboard, -# along with any modifications made in other Frappe apps -# override_doctype_dashboards = { -# "Task": "crm.task.get_dashboard_data" -# } - -# exempt linked doctypes from being automatically cancelled -# -# auto_cancel_exempted_doctypes = ["Auto Repeat"] - -# Ignore links to specified DocTypes when deleting documents -# ----------------------------------------------------------- - -# ignore_links_on_delete = ["Communication", "ToDo"] - -# Request Events -# ---------------- -# before_request = ["crm.utils.before_request"] -# after_request = ["crm.utils.after_request"] - -# Job Events -# ---------- -# before_job = ["crm.utils.before_job"] -# after_job = ["crm.utils.after_job"] - -# User Data Protection -# -------------------- - -# user_data_fields = [ -# { -# "doctype": "{doctype_1}", -# "filter_by": "{filter_by}", -# "redact_fields": ["{field_1}", "{field_2}"], -# "partial": 1, -# }, -# { -# "doctype": "{doctype_2}", -# "filter_by": "{filter_by}", -# "partial": 1, -# }, -# { -# "doctype": "{doctype_3}", -# "strict": False, -# }, -# { -# "doctype": "{doctype_4}" -# } -# ] - -# Authentication and authorization -# -------------------------------- - -# auth_hooks = [ -# "crm.auth.validate" -# ] diff --git a/crm/install.py b/crm/install.py deleted file mode 100644 index 594b1ea5f..000000000 --- a/crm/install.py +++ /dev/null @@ -1,242 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt -from __future__ import unicode_literals -import click -import frappe - -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - -def before_install(): - pass - -def after_install(force=False): - add_default_lead_statuses() - add_default_deal_statuses() - add_default_communication_statuses() - add_default_fields_layout(force) - add_property_setter() - add_email_template_custom_fields() - add_default_industries() - add_default_lead_sources() - frappe.db.commit() - -def add_default_lead_statuses(): - statuses = { - "New": { - "color": "gray", - "position": 1, - }, - "Contacted": { - "color": "orange", - "position": 2, - }, - "Nurture": { - "color": "blue", - "position": 3, - }, - "Qualified": { - "color": "green", - "position": 4, - }, - "Unqualified": { - "color": "red", - "position": 5, - }, - "Junk": { - "color": "purple", - "position": 6, - }, - } - - for status in statuses: - if frappe.db.exists("CRM Lead Status", status): - continue - - doc = frappe.new_doc("CRM Lead Status") - doc.lead_status = status - doc.color = statuses[status]["color"] - doc.position = statuses[status]["position"] - doc.insert() - -def add_default_deal_statuses(): - statuses = { - "Qualification": { - "color": "gray", - "position": 1, - }, - "Demo/Making": { - "color": "orange", - "position": 2, - }, - "Proposal/Quotation": { - "color": "blue", - "position": 3, - }, - "Negotiation": { - "color": "yellow", - "position": 4, - }, - "Ready to Close": { - "color": "purple", - "position": 5, - }, - "Won": { - "color": "green", - "position": 6, - }, - "Lost": { - "color": "red", - "position": 7, - }, - } - - for status in statuses: - if frappe.db.exists("CRM Deal Status", status): - continue - - doc = frappe.new_doc("CRM Deal Status") - doc.deal_status = status - doc.color = statuses[status]["color"] - doc.position = statuses[status]["position"] - doc.insert() - -def add_default_communication_statuses(): - statuses = ["Open", "Replied"] - - for status in statuses: - if frappe.db.exists("CRM Communication Status", status): - continue - - doc = frappe.new_doc("CRM Communication Status") - doc.status = status - doc.insert() - -def add_default_fields_layout(force=False): - quick_entry_layouts = { - "CRM Lead-Quick Entry": { - "doctype": "CRM Lead", - "layout": '[{"label":"Person","fields":["salutation","first_name","last_name","email","mobile_no", "gender"],"hideLabel":true},{"label":"Organization","fields":["organization","website","no_of_employees","territory","annual_revenue","industry"],"hideLabel":true,"hideBorder":false},{"label":"Other","columns":2,"fields":["status","lead_owner"],"hideLabel":true,"hideBorder":false}]' - }, - "CRM Deal-Quick Entry": { - "doctype": "CRM Deal", - "layout": '[{"label": "Select Organization", "fields": ["organization"], "hideLabel": true, "editable": true}, {"label": "Organization Details", "fields": ["organization_name", "website", "no_of_employees", "territory", "annual_revenue", "industry"], "hideLabel": true, "editable": true}, {"label": "Select Contact", "fields": ["contact"], "hideLabel": true, "editable": true}, {"label": "Contact Details", "fields": ["salutation", "first_name", "last_name", "email", "mobile_no", "gender"], "hideLabel": true, "editable": true}, {"label": "Other", "columns": 2, "fields": ["status", "deal_owner"], "hideLabel": true}]' - }, - "Contact-Quick Entry": { - "doctype": "Contact", - "layout": '[{"label":"Salutation","columns":1,"fields":["salutation"],"hideLabel":true},{"label":"Full Name","columns":2,"hideBorder":true,"fields":["first_name","last_name"],"hideLabel":true},{"label":"Email","columns":1,"hideBorder":true,"fields":["email_id"],"hideLabel":true},{"label":"Mobile No. & Gender","columns":2,"hideBorder":true,"fields":["mobile_no","gender"],"hideLabel":true},{"label":"Organization","columns":1,"hideBorder":true,"fields":["company_name"],"hideLabel":true},{"label":"Designation","columns":1,"hideBorder":true,"fields":["designation"],"hideLabel":true},{"label":"Address","columns":1,"hideBorder":true,"fields":["address"],"hideLabel":true}]' - }, - "CRM Organization-Quick Entry": { - "doctype": "CRM Organization", - "layout": '[{"label":"Organization Name","columns":1,"fields":["organization_name"],"hideLabel":true},{"label":"Website & Revenue","columns":2,"hideBorder":true,"fields":["website","annual_revenue"],"hideLabel":true},{"label":"Territory","columns":1,"hideBorder":true,"fields":["territory"],"hideLabel":true},{"label":"No of Employees & Industry","columns":2,"hideBorder":true,"fields":["no_of_employees","industry"],"hideLabel":true},{"label":"Address","columns":1,"hideBorder":true,"fields":["address"],"hideLabel":true}]' - }, - "Address-Quick Entry": { - "doctype": "Address", - "layout": '[{"label":"Address","columns":1,"fields":["address_title","address_type","address_line1","address_line2","city","state","country","pincode"],"hideLabel":true}]' - }, - } - - sidebar_fields_layouts = { - "CRM Lead-Side Panel": { - "doctype": "CRM Lead", - "layout": '[{"label": "Details", "name": "details", "opened": true, "fields": ["organization", "website", "territory", "industry", "job_title", "source", "lead_owner"]}, {"label": "Person", "name": "person_tab", "opened": true, "fields": ["salutation", "first_name", "last_name", "email", "mobile_no"]}]' - }, - "CRM Deal-Side Panel": { - "doctype": "CRM Deal", - "layout": '[{"label":"Contacts","name":"contacts_section","opened":true,"editable":false,"contacts":[]},{"label":"Organization Details","name":"organization_tab","opened":true,"fields":["organization","website","territory","annual_revenue","close_date","probability","next_step","deal_owner"]}]' - }, - "Contact-Side Panel": { - "doctype": "Contact", - "layout": '[{"label":"Details","name":"details","opened":true,"fields":["salutation","first_name","last_name","email_id","mobile_no","gender","company_name","designation","address"]}]' - }, - "CRM Organization-Side Panel": { - "doctype": "CRM Organization", - "layout": '[{"label":"Details","name":"details","opened":true,"fields":["organization_name","website","territory","industry","no_of_employees","address"]}]' - }, - } - - for layout in quick_entry_layouts: - if frappe.db.exists("CRM Fields Layout", layout): - if force: - frappe.delete_doc("CRM Fields Layout", layout) - else: - continue - - doc = frappe.new_doc("CRM Fields Layout") - doc.type = "Quick Entry" - doc.dt = quick_entry_layouts[layout]["doctype"] - doc.layout = quick_entry_layouts[layout]["layout"] - doc.insert() - - for layout in sidebar_fields_layouts: - if frappe.db.exists("CRM Fields Layout", layout): - if force: - frappe.delete_doc("CRM Fields Layout", layout) - else: - continue - - doc = frappe.new_doc("CRM Fields Layout") - doc.type = "Side Panel" - doc.dt = sidebar_fields_layouts[layout]["doctype"] - doc.layout = sidebar_fields_layouts[layout]["layout"] - doc.insert() - -def add_property_setter(): - if not frappe.db.exists("Property Setter", {"name": "Contact-main-search_fields"}): - doc = frappe.new_doc("Property Setter") - doc.doctype_or_field = "DocType" - doc.doc_type = "Contact" - doc.property = "search_fields" - doc.property_type = "Data" - doc.value = "email_id" - doc.insert() - -def add_email_template_custom_fields(): - if not frappe.get_meta("Email Template").has_field("enabled"): - click.secho("* Installing Custom Fields in Email Template") - - create_custom_fields( - { - "Email Template": [ - { - "default": "0", - "fieldname": "enabled", - "fieldtype": "Check", - "label": "Enabled", - "insert_after": "", - }, - { - "fieldname": "reference_doctype", - "fieldtype": "Link", - "label": "Doctype", - "options": "DocType", - "insert_after": "enabled", - }, - ] - } - ) - - frappe.clear_cache(doctype="Email Template") - - -def add_default_industries(): - industries = ["Accounting", "Advertising", "Aerospace", "Agriculture", "Airline", "Apparel & Accessories", "Automotive", "Banking", "Biotechnology", "Broadcasting", "Brokerage", "Chemical", "Computer", "Consulting", "Consumer Products", "Cosmetics", "Defense", "Department Stores", "Education", "Electronics", "Energy", "Entertainment & Leisure, Executive Search", "Financial Services", "Food", "Beverage & Tobacco", "Grocery", "Health Care", "Internet Publishing", "Investment Banking", "Legal", "Manufacturing", "Motion Picture & Video", "Music", "Newspaper Publishers", "Online Auctions", "Pension Funds", "Pharmaceuticals", "Private Equity", "Publishing", "Real Estate", "Retail & Wholesale", "Securities & Commodity Exchanges", "Service", "Soap & Detergent", "Software", "Sports", "Technology", "Telecommunications", "Television", "Transportation", "Venture Capital"] - - for industry in industries: - if frappe.db.exists("CRM Industry", industry): - continue - - doc = frappe.new_doc("CRM Industry") - doc.industry = industry - doc.insert() - - -def add_default_lead_sources(): - lead_sources = ["Existing Customer", "Reference", "Advertisement", "Cold Calling", "Exhibition", "Supplier Reference", "Mass Mailing", "Customer's Vendor", "Campaign", "Walk In"] - - for source in lead_sources: - if frappe.db.exists("CRM Lead Source", source): - continue - - doc = frappe.new_doc("CRM Lead Source") - doc.source_name = source - doc.insert() diff --git a/crm/integrations/twilio/api.py b/crm/integrations/twilio/api.py deleted file mode 100644 index 1ea4d67dc..000000000 --- a/crm/integrations/twilio/api.py +++ /dev/null @@ -1,182 +0,0 @@ -from werkzeug.wrappers import Response -import json - -import frappe -from frappe import _ -from .twilio_handler import Twilio, IncomingCall, TwilioCallDetails -from .utils import parse_mobile_no - -@frappe.whitelist() -def is_enabled(): - return frappe.db.get_single_value("Twilio Settings", "enabled") - -@frappe.whitelist() -def generate_access_token(): - """Returns access token that is required to authenticate Twilio Client SDK. - """ - twilio = Twilio.connect() - if not twilio: - return {} - - from_number = frappe.db.get_value('Twilio Agents', frappe.session.user, 'twilio_number') - if not from_number: - return { - "ok": False, - "error": "caller_phone_identity_missing", - "detail": "Phone number is not mapped to the caller" - } - - token=twilio.generate_voice_access_token(identity=frappe.session.user) - return { - 'token': frappe.safe_decode(token) - } - -@frappe.whitelist(allow_guest=True) -def voice(**kwargs): - """This is a webhook called by twilio to get instructions when the voice call request comes to twilio server. - """ - def _get_caller_number(caller): - identity = caller.replace('client:', '').strip() - user = Twilio.emailid_from_identity(identity) - return frappe.db.get_value('Twilio Agents', user, 'twilio_number') - - args = frappe._dict(kwargs) - twilio = Twilio.connect() - if not twilio: - return - - assert args.AccountSid == twilio.account_sid - assert args.ApplicationSid == twilio.application_sid - - # Generate TwiML instructions to make a call - from_number = _get_caller_number(args.Caller) - resp = twilio.generate_twilio_dial_response(from_number, args.To) - - call_details = TwilioCallDetails(args, call_from=from_number) - create_call_log(call_details) - return Response(resp.to_xml(), mimetype='text/xml') - -@frappe.whitelist(allow_guest=True) -def twilio_incoming_call_handler(**kwargs): - args = frappe._dict(kwargs) - call_details = TwilioCallDetails(args) - create_call_log(call_details) - - resp = IncomingCall(args.From, args.To).process() - return Response(resp.to_xml(), mimetype='text/xml') - -def create_call_log(call_details: TwilioCallDetails): - call_log = frappe.get_doc({**call_details.to_dict(), - 'doctype': 'CRM Call Log', - 'medium': 'Twilio' - }) - call_log.reference_docname, call_log.reference_doctype = get_lead_or_deal_from_number(call_log) - call_log.flags.ignore_permissions = True - call_log.save() - frappe.db.commit() - -def update_call_log(call_sid, status=None): - """Update call log status. - """ - twilio = Twilio.connect() - if not (twilio and frappe.db.exists("CRM Call Log", call_sid)): return - - call_details = twilio.get_call_info(call_sid) - call_log = frappe.get_doc("CRM Call Log", call_sid) - call_log.status = TwilioCallDetails.get_call_status(status or call_details.status) - call_log.duration = call_details.duration - call_log.start_time = get_datetime_from_timestamp(call_details.start_time) - call_log.end_time = get_datetime_from_timestamp(call_details.end_time) - if call_log.note and call_log.reference_docname: - frappe.db.set_value("FCRM Note", call_log.note, "reference_doctype", call_log.reference_doctype) - frappe.db.set_value("FCRM Note", call_log.note, "reference_docname", call_log.reference_docname) - call_log.flags.ignore_permissions = True - call_log.save() - frappe.db.commit() - -@frappe.whitelist(allow_guest=True) -def update_recording_info(**kwargs): - try: - args = frappe._dict(kwargs) - recording_url = args.RecordingUrl - call_sid = args.CallSid - update_call_log(call_sid) - frappe.db.set_value("CRM Call Log", call_sid, "recording_url", recording_url) - except: - frappe.log_error(title=_("Failed to capture Twilio recording")) - -@frappe.whitelist(allow_guest=True) -def update_call_status_info(**kwargs): - try: - args = frappe._dict(kwargs) - parent_call_sid = args.ParentCallSid - update_call_log(parent_call_sid, status=args.CallStatus) - - call_info = { - 'ParentCallSid': args.ParentCallSid, - 'CallSid': args.CallSid, - 'CallStatus': args.CallStatus, - 'CallDuration': args.CallDuration, - 'From': args.From, - 'To': args.To, - } - - client = Twilio.get_twilio_client() - client.calls(args.ParentCallSid).user_defined_messages.create( - content=json.dumps(call_info) - ) - except: - frappe.log_error(title=_("Failed to update Twilio call status")) - -def get_datetime_from_timestamp(timestamp): - from datetime import datetime - from pytz import timezone - - if not timestamp: return None - - datetime_utc_tz_str = timestamp.strftime('%Y-%m-%d %H:%M:%S%z') - datetime_utc_tz = datetime.strptime(datetime_utc_tz_str, '%Y-%m-%d %H:%M:%S%z') - system_timezone = frappe.utils.get_system_timezone() - converted_datetime = datetime_utc_tz.astimezone(timezone(system_timezone)) - return frappe.utils.format_datetime(converted_datetime, 'yyyy-MM-dd HH:mm:ss') - -@frappe.whitelist() -def add_note_to_call_log(call_sid, note): - """Add note to call log. based on child call sid. - """ - twilio = Twilio.connect() - if not twilio: return - - call_details = twilio.get_call_info(call_sid) - sid = call_sid if call_details.direction == 'inbound' else call_details.parent_call_sid - - frappe.db.set_value("CRM Call Log", sid, "note", note) - frappe.db.commit() - -def get_lead_or_deal_from_number(call): - """Get lead/deal from the given number. - """ - - def find_record(doctype, mobile_no, where=''): - mobile_no = parse_mobile_no(mobile_no) - - query = f""" - SELECT name, mobile_no - FROM `tab{doctype}` - WHERE CONCAT('+', REGEXP_REPLACE(mobile_no, '[^0-9]', '')) = {mobile_no} - """ - - data = frappe.db.sql(query + where, as_dict=True) - return data[0].name if data else None - - doctype = "CRM Deal" - number = call.get('to') if call.type == 'Outgoing' else call.get('from') - - doc = find_record(doctype, number) or None - if not doc: - doctype = "CRM Lead" - doc = find_record(doctype, number, 'AND converted is not True') - if not doc: - doc = find_record(doctype, number) - - return doc, doctype \ No newline at end of file diff --git a/crm/integrations/twilio/twilio_handler.py b/crm/integrations/twilio/twilio_handler.py deleted file mode 100644 index 1ece92966..000000000 --- a/crm/integrations/twilio/twilio_handler.py +++ /dev/null @@ -1,267 +0,0 @@ -from twilio.rest import Client as TwilioClient -from twilio.jwt.access_token import AccessToken -from twilio.jwt.access_token.grants import VoiceGrant -from twilio.twiml.voice_response import VoiceResponse, Dial -from .utils import get_public_url, merge_dicts - -import frappe -from frappe import _ -from frappe.utils.password import get_decrypted_password - -class Twilio: - """Twilio connector over TwilioClient. - """ - def __init__(self, settings): - """ - :param settings: `Twilio Settings` doctype - """ - self.settings = settings - self.account_sid = settings.account_sid - self.application_sid = settings.twiml_sid - self.api_key = settings.api_key - self.api_secret = settings.get_password("api_secret") - self.twilio_client = self.get_twilio_client() - - @classmethod - def connect(self): - """Make a twilio connection. - """ - settings = frappe.get_doc("Twilio Settings") - if not (settings and settings.enabled): - return - return Twilio(settings=settings) - - def get_phone_numbers(self): - """Get account's twilio phone numbers. - """ - numbers = self.twilio_client.incoming_phone_numbers.list() - return [n.phone_number for n in numbers] - - def generate_voice_access_token(self, identity: str, ttl=60*60): - """Generates a token required to make voice calls from the browser. - """ - # identity is used by twilio to identify the user uniqueness at browser(or any endpoints). - identity = self.safe_identity(identity) - - # Create access token with credentials - token = AccessToken(self.account_sid, self.api_key, self.api_secret, identity=identity, ttl=ttl) - - # Create a Voice grant and add to token - voice_grant = VoiceGrant( - outgoing_application_sid=self.application_sid, - incoming_allow=True, # Allow incoming calls - ) - token.add_grant(voice_grant) - return token.to_jwt() - - @classmethod - def safe_identity(cls, identity: str): - """Create a safe identity by replacing unsupported special charaters `@` with (at)). - Twilio Client JS fails to make a call connection if identity has special characters like @, [, / etc) - https://www.twilio.com/docs/voice/client/errors (#31105) - """ - return identity.replace('@', '(at)') - - @classmethod - def emailid_from_identity(cls, identity: str): - """Convert safe identity string into emailID. - """ - return identity.replace('(at)', '@') - - def get_recording_status_callback_url(self): - url_path = "/api/method/crm.integrations.twilio.api.update_recording_info" - return get_public_url(url_path) - - def get_update_call_status_callback_url(self): - url_path = "/api/method/crm.integrations.twilio.api.update_call_status_info" - return get_public_url(url_path) - - def generate_twilio_dial_response(self, from_number: str, to_number: str): - """Generates voice call instructions to forward the call to agents Phone. - """ - resp = VoiceResponse() - dial = Dial( - caller_id=from_number, - record=self.settings.record_calls, - recording_status_callback=self.get_recording_status_callback_url(), - recording_status_callback_event='completed' - ) - dial.number( - to_number, - status_callback_event='initiated ringing answered completed', - status_callback=self.get_update_call_status_callback_url(), - status_callback_method='POST' - ) - resp.append(dial) - return resp - - def get_call_info(self, call_sid): - return self.twilio_client.calls(call_sid).fetch() - - def generate_twilio_client_response(self, client, ring_tone='at'): - """Generates voice call instructions to forward the call to agents computer. - """ - resp = VoiceResponse() - dial = Dial( - ring_tone=ring_tone, - record=self.settings.record_calls, - recording_status_callback=self.get_recording_status_callback_url(), - recording_status_callback_event='completed' - ) - dial.client( - client, - status_callback_event='initiated ringing answered completed', - status_callback=self.get_update_call_status_callback_url(), - status_callback_method='POST' - ) - resp.append(dial) - return resp - - @classmethod - def get_twilio_client(self): - twilio_settings = frappe.get_doc("Twilio Settings") - if not twilio_settings.enabled: - frappe.throw(_("Please enable twilio settings before making a call.")) - - auth_token = get_decrypted_password("Twilio Settings", "Twilio Settings", 'auth_token') - client = TwilioClient(twilio_settings.account_sid, auth_token) - - return client - -class IncomingCall: - def __init__(self, from_number, to_number, meta=None): - self.from_number = from_number - self.to_number = to_number - self.meta = meta - - def process(self): - """Process the incoming call - * Figure out who is going to pick the call (call attender) - * Check call attender settings and forward the call to Phone - """ - twilio = Twilio.connect() - owners = get_twilio_number_owners(self.to_number) - attender = get_the_call_attender(owners, self.from_number) - - if not attender: - resp = VoiceResponse() - resp.say(_('Agent is unavailable to take the call, please call after some time.')) - return resp - - if attender['call_receiving_device'] == 'Phone': - return twilio.generate_twilio_dial_response(self.from_number, attender['mobile_no']) - else: - return twilio.generate_twilio_client_response(twilio.safe_identity(attender['name'])) - -def get_twilio_number_owners(phone_number): - """Get list of users who is using the phone_number. - >>> get_twilio_number_owners('+11234567890') - { - 'owner1': {'name': '..', 'mobile_no': '..', 'call_receiving_device': '...'}, - 'owner2': {....} - } - """ - # remove special characters from phone number and get only digits also remove white spaces - # keep + sign in the number at start of the number - phone_number = ''.join([c for c in phone_number if c.isdigit() or c == '+']) - user_voice_settings = frappe.get_all( - 'Twilio Agents', - filters={'twilio_number': phone_number}, - fields=["name", "call_receiving_device"] - ) - user_wise_voice_settings = {user['name']: user for user in user_voice_settings} - - user_general_settings = frappe.get_all( - 'User', - filters = [['name', 'IN', user_wise_voice_settings.keys()]], - fields = ['name', 'mobile_no'] - ) - user_wise_general_settings = {user['name']: user for user in user_general_settings} - - return merge_dicts(user_wise_general_settings, user_wise_voice_settings) - -def get_active_loggedin_users(users): - """Filter the current loggedin users from the given users list - """ - rows = frappe.db.sql(""" - SELECT `user` - FROM `tabSessions` - WHERE `user` IN %(users)s - """, {'users': users}) - return [row[0] for row in set(rows)] - -def get_the_call_attender(owners, caller=None): - """Get attender details from list of owners - """ - if not owners: return - current_loggedin_users = get_active_loggedin_users(list(owners.keys())) - - if len(current_loggedin_users) > 1 and caller: - deal_owner = frappe.db.get_value('CRM Deal', {'mobile_no': caller}, 'deal_owner') - if not deal_owner: - deal_owner = frappe.db.get_value('CRM Lead', {'mobile_no': caller, 'converted': False}, 'lead_owner') - for user in current_loggedin_users: - if user == deal_owner: - current_loggedin_users = [user] - - for name, details in owners.items(): - if ((details['call_receiving_device'] == 'Phone' and details['mobile_no']) or - (details['call_receiving_device'] == 'Computer' and name in current_loggedin_users)): - return details - - -class TwilioCallDetails: - def __init__(self, call_info, call_from = None, call_to = None): - self.call_info = call_info - self.account_sid = call_info.get('AccountSid') - self.application_sid = call_info.get('ApplicationSid') - self.call_sid = call_info.get('CallSid') - self.call_status = self.get_call_status(call_info.get('CallStatus')) - self._call_from = call_from or call_info.get('From') - self._call_to = call_to or call_info.get('To') - - def get_direction(self): - if self.call_info.get('Caller').lower().startswith('client'): - return 'Outgoing' - return 'Incoming' - - def get_from_number(self): - return self._call_from or self.call_info.get('From') - - def get_to_number(self): - return self._call_to or self.call_info.get('To') - - @classmethod - def get_call_status(cls, twilio_status): - """Convert Twilio given status into system status. - """ - twilio_status = twilio_status or '' - return ' '.join(twilio_status.split('-')).title() - - def to_dict(self): - """Convert call details into dict. - """ - direction = self.get_direction() - from_number = self.get_from_number() - to_number = self.get_to_number() - caller = '' - receiver = '' - - if direction == 'Outgoing': - caller = self.call_info.get('Caller') - identity = caller.replace('client:', '').strip() - caller = Twilio.emailid_from_identity(identity) if identity else '' - else: - owners = get_twilio_number_owners(to_number) - attender = get_the_call_attender(owners, from_number) - receiver = attender['name'] if attender else '' - - return { - 'type': direction, - 'status': self.call_status, - 'id': self.call_sid, - 'from': from_number, - 'to': to_number, - 'receiver': receiver, - 'caller': caller, - } \ No newline at end of file diff --git a/crm/integrations/twilio/utils.py b/crm/integrations/twilio/utils.py deleted file mode 100644 index 2f0376842..000000000 --- a/crm/integrations/twilio/utils.py +++ /dev/null @@ -1,23 +0,0 @@ -from frappe.utils import get_url - - -def get_public_url(path: str=None): - return get_url().split(":8", 1)[0] + path - - -def merge_dicts(d1: dict, d2: dict): - """Merge dicts of dictionaries. - >>> merge_dicts( - {'name1': {'age': 20}, 'name2': {'age': 30}}, - {'name1': {'phone': '+xxx'}, 'name2': {'phone': '+yyy'}, 'name3': {'phone': '+zzz'}} - ) - ... {'name1': {'age': 20, 'phone': '+xxx'}, 'name2': {'age': 30, 'phone': '+yyy'}} - """ - return {k:{**v, **d2.get(k, {})} for k, v in d1.items()} - -def parse_mobile_no(mobile_no: str): - """Parse mobile number to remove spaces, brackets, etc. - >>> parse_mobile_no('+91 (766) 667 6666') - ... '+917666676666' - """ - return ''.join([c for c in mobile_no if c.isdigit() or c == '+']) \ No newline at end of file diff --git a/crm/modules.txt b/crm/modules.txt deleted file mode 100644 index e23619140..000000000 --- a/crm/modules.txt +++ /dev/null @@ -1 +0,0 @@ -FCRM \ No newline at end of file diff --git a/crm/overrides/contact.py b/crm/overrides/contact.py deleted file mode 100644 index 9845c3982..000000000 --- a/crm/overrides/contact.py +++ /dev/null @@ -1,50 +0,0 @@ -# import frappe -from frappe import _ -from frappe.contacts.doctype.contact.contact import Contact - - -class CustomContact(Contact): - @staticmethod - def default_list_data(): - columns = [ - { - 'label': 'Name', - 'type': 'Data', - 'key': 'full_name', - 'width': '17rem', - }, - { - 'label': 'Email', - 'type': 'Data', - 'key': 'email_id', - 'width': '12rem', - }, - { - 'label': 'Phone', - 'type': 'Data', - 'key': 'mobile_no', - 'width': '12rem', - }, - { - 'label': 'Organization', - 'type': 'Data', - 'key': 'company_name', - 'width': '12rem', - }, - { - 'label': 'Last Modified', - 'type': 'Datetime', - 'key': 'modified', - 'width': '8rem', - }, - ] - rows = [ - "name", - "full_name", - "company_name", - "email_id", - "mobile_no", - "modified", - "image", - ] - return {'columns': columns, 'rows': rows} diff --git a/crm/overrides/email_template.py b/crm/overrides/email_template.py deleted file mode 100644 index 3a4e30f7c..000000000 --- a/crm/overrides/email_template.py +++ /dev/null @@ -1,51 +0,0 @@ -# import frappe -from frappe import _ -from frappe.email.doctype.email_template.email_template import EmailTemplate - - -class CustomEmailTemplate(EmailTemplate): - @staticmethod - def default_list_data(): - columns = [ - { - 'label': 'Name', - 'type': 'Data', - 'key': 'name', - 'width': '17rem', - }, - { - 'label': 'Subject', - 'type': 'Data', - 'key': 'subject', - 'width': '12rem', - }, - { - 'label': 'Enabled', - 'type': 'Check', - 'key': 'enabled', - 'width': '6rem', - }, - { - 'label': 'Doctype', - 'type': 'Link', - 'key': 'reference_doctype', - 'width': '12rem', - }, - { - 'label': 'Last Modified', - 'type': 'Datetime', - 'key': 'modified', - 'width': '8rem', - }, - ] - rows = [ - "name", - "enabled", - "use_html", - "reference_doctype", - "subject", - "response", - "response_html", - "modified", - ] - return {'columns': columns, 'rows': rows} diff --git a/crm/overrides/user.py b/crm/overrides/user.py deleted file mode 100644 index d938825c2..000000000 --- a/crm/overrides/user.py +++ /dev/null @@ -1,10 +0,0 @@ -# import frappe -from frappe import _ -from frappe.core.doctype.user.user import User -from crm.api.demo import validate_reset_password - - -class CustomUser(User): - def validate_reset_password(self): - # restrict demo user to reset password - validate_reset_password(self) diff --git a/crm/patches.txt b/crm/patches.txt deleted file mode 100644 index b14faf132..000000000 --- a/crm/patches.txt +++ /dev/null @@ -1,11 +0,0 @@ -[pre_model_sync] -# Patches added in this section will be executed before doctypes are migrated -# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations -crm.patches.v1_0.move_crm_note_data_to_fcrm_note - -[post_model_sync] -# Patches added in this section will be executed after doctypes are migrated -crm.patches.v1_0.create_email_template_custom_fields -crm.patches.v1_0.create_default_fields_layout #31/10/2024 -crm.patches.v1_0.create_default_sidebar_fields_layout -crm.patches.v1_0.update_deal_quick_entry_layout \ No newline at end of file diff --git a/crm/patches/v1_0/__init__.py b/crm/patches/v1_0/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/patches/v1_0/create_default_fields_layout.py b/crm/patches/v1_0/create_default_fields_layout.py deleted file mode 100644 index 383e7ac23..000000000 --- a/crm/patches/v1_0/create_default_fields_layout.py +++ /dev/null @@ -1,5 +0,0 @@ - -from crm.install import add_default_fields_layout - -def execute(): - add_default_fields_layout() \ No newline at end of file diff --git a/crm/patches/v1_0/create_default_sidebar_fields_layout.py b/crm/patches/v1_0/create_default_sidebar_fields_layout.py deleted file mode 100644 index 04b65feee..000000000 --- a/crm/patches/v1_0/create_default_sidebar_fields_layout.py +++ /dev/null @@ -1,63 +0,0 @@ -import json -import frappe - -def execute(): - if not frappe.db.exists("CRM Fields Layout", {"dt": "CRM Lead", "type": "Side Panel"}): - create_doctype_fields_layout("CRM Lead") - - if not frappe.db.exists("CRM Fields Layout", {"dt": "CRM Deal", "type": "Side Panel"}): - create_doctype_fields_layout("CRM Deal") - -def create_doctype_fields_layout(doctype): - not_allowed_fieldtypes = [ - "Section Break", - "Column Break", - ] - - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] - - sections = {} - section_fields = [] - last_section = None - - for field in fields: - if field.fieldtype == "Tab Break" and last_section: - sections[last_section]["fields"] = section_fields - last_section = None - if field.read_only: - section_fields = [] - continue - if field.fieldtype == "Tab Break": - if field.read_only: - section_fields = [] - continue - section_fields = [] - last_section = field.fieldname - sections[field.fieldname] = { - "label": field.label, - "name": field.fieldname, - "opened": True, - "fields": [], - } - if field.fieldname == "contacts_tab": - sections[field.fieldname]["editable"] = False - sections[field.fieldname]["contacts"] = [] - else: - section_fields.append(field.fieldname) - - section_fields = [] - for section in sections: - if section == "contacts_tab": - sections[section]["name"] = "contacts_section" - sections[section].pop("fields", None) - section_fields.append(sections[section]) - - frappe.get_doc({ - "doctype": "CRM Fields Layout", - "dt": doctype, - "type": "Side Panel", - "layout": json.dumps(section_fields), - }).insert(ignore_permissions=True) - - return section_fields \ No newline at end of file diff --git a/crm/patches/v1_0/create_email_template_custom_fields.py b/crm/patches/v1_0/create_email_template_custom_fields.py deleted file mode 100644 index ea6665d79..000000000 --- a/crm/patches/v1_0/create_email_template_custom_fields.py +++ /dev/null @@ -1,5 +0,0 @@ - -from crm.install import add_email_template_custom_fields - -def execute(): - add_email_template_custom_fields() \ No newline at end of file diff --git a/crm/patches/v1_0/move_crm_note_data_to_fcrm_note.py b/crm/patches/v1_0/move_crm_note_data_to_fcrm_note.py deleted file mode 100644 index 2f1b319eb..000000000 --- a/crm/patches/v1_0/move_crm_note_data_to_fcrm_note.py +++ /dev/null @@ -1,31 +0,0 @@ -import frappe -from frappe.model.rename_doc import rename_doc - - -def execute(): - - if not frappe.db.exists("DocType", "FCRM Note"): - frappe.flags.ignore_route_conflict_validation = True - rename_doc("DocType", "CRM Note", "FCRM Note") - frappe.flags.ignore_route_conflict_validation = False - - frappe.reload_doctype("FCRM Note", force=True) - - if frappe.db.exists("DocType", "FCRM Note") and frappe.db.count("FCRM Note") > 0: - return - - notes = frappe.db.sql("SELECT * FROM `tabCRM Note`", as_dict=True) - if notes: - for note in notes: - doc = frappe.get_doc({ - "doctype": "FCRM Note", - "creation": note.get("creation"), - "modified": note.get("modified"), - "modified_by": note.get("modified_by"), - "owner": note.get("owner"), - "title": note.get("title"), - "content": note.get("content"), - "reference_doctype": note.get("reference_doctype"), - "reference_docname": note.get("reference_docname"), - }) - doc.db_insert() \ No newline at end of file diff --git a/crm/patches/v1_0/update_deal_quick_entry_layout.py b/crm/patches/v1_0/update_deal_quick_entry_layout.py deleted file mode 100644 index fc52e2acc..000000000 --- a/crm/patches/v1_0/update_deal_quick_entry_layout.py +++ /dev/null @@ -1,15 +0,0 @@ -import json -import frappe - -def execute(): - if not frappe.db.exists("CRM Fields Layout", "CRM Deal-Quick Entry"): - return - - deal = frappe.db.get_value("CRM Fields Layout", "CRM Deal-Quick Entry", "layout") - - layout = json.loads(deal) - for section in layout: - if section.get("label") in ["Select Organization", "Organization Details", "Select Contact", "Contact Details"]: - section["editable"] = False - - frappe.db.set_value("CRM Fields Layout", "CRM Deal-Quick Entry", "layout", json.dumps(layout)) \ No newline at end of file diff --git a/crm/public/images/logo.png b/crm/public/images/logo.png deleted file mode 100644 index 5881304f4..000000000 Binary files a/crm/public/images/logo.png and /dev/null differ diff --git a/crm/public/images/logo.svg b/crm/public/images/logo.svg deleted file mode 100644 index e203a0f83..000000000 --- a/crm/public/images/logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/crm/public/manifest/apple-icon-180.png b/crm/public/manifest/apple-icon-180.png deleted file mode 100644 index f1fde19d6..000000000 Binary files a/crm/public/manifest/apple-icon-180.png and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1125-2436.jpg b/crm/public/manifest/apple-splash-1125-2436.jpg deleted file mode 100644 index 1440fa05a..000000000 Binary files a/crm/public/manifest/apple-splash-1125-2436.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1136-640.jpg b/crm/public/manifest/apple-splash-1136-640.jpg deleted file mode 100644 index 8eb7bf6e5..000000000 Binary files a/crm/public/manifest/apple-splash-1136-640.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1170-2532.jpg b/crm/public/manifest/apple-splash-1170-2532.jpg deleted file mode 100644 index 8b1b82a7c..000000000 Binary files a/crm/public/manifest/apple-splash-1170-2532.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1179-2556.jpg b/crm/public/manifest/apple-splash-1179-2556.jpg deleted file mode 100644 index c0c6833bb..000000000 Binary files a/crm/public/manifest/apple-splash-1179-2556.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1242-2208.jpg b/crm/public/manifest/apple-splash-1242-2208.jpg deleted file mode 100644 index f084d3524..000000000 Binary files a/crm/public/manifest/apple-splash-1242-2208.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1242-2688.jpg b/crm/public/manifest/apple-splash-1242-2688.jpg deleted file mode 100644 index bdeff56e9..000000000 Binary files a/crm/public/manifest/apple-splash-1242-2688.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1284-2778.jpg b/crm/public/manifest/apple-splash-1284-2778.jpg deleted file mode 100644 index 29a25be4f..000000000 Binary files a/crm/public/manifest/apple-splash-1284-2778.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1290-2796.jpg b/crm/public/manifest/apple-splash-1290-2796.jpg deleted file mode 100644 index 0fca8c137..000000000 Binary files a/crm/public/manifest/apple-splash-1290-2796.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1334-750.jpg b/crm/public/manifest/apple-splash-1334-750.jpg deleted file mode 100644 index ec3ea1b84..000000000 Binary files a/crm/public/manifest/apple-splash-1334-750.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1488-2266.jpg b/crm/public/manifest/apple-splash-1488-2266.jpg deleted file mode 100644 index 1d7cb4809..000000000 Binary files a/crm/public/manifest/apple-splash-1488-2266.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1536-2048.jpg b/crm/public/manifest/apple-splash-1536-2048.jpg deleted file mode 100644 index de0157417..000000000 Binary files a/crm/public/manifest/apple-splash-1536-2048.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1620-2160.jpg b/crm/public/manifest/apple-splash-1620-2160.jpg deleted file mode 100644 index cde84c9b4..000000000 Binary files a/crm/public/manifest/apple-splash-1620-2160.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1640-2360.jpg b/crm/public/manifest/apple-splash-1640-2360.jpg deleted file mode 100644 index a33bfd031..000000000 Binary files a/crm/public/manifest/apple-splash-1640-2360.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1668-2224.jpg b/crm/public/manifest/apple-splash-1668-2224.jpg deleted file mode 100644 index d8ccacbf2..000000000 Binary files a/crm/public/manifest/apple-splash-1668-2224.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1668-2388.jpg b/crm/public/manifest/apple-splash-1668-2388.jpg deleted file mode 100644 index bb2ebc8d5..000000000 Binary files a/crm/public/manifest/apple-splash-1668-2388.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-1792-828.jpg b/crm/public/manifest/apple-splash-1792-828.jpg deleted file mode 100644 index f085722df..000000000 Binary files a/crm/public/manifest/apple-splash-1792-828.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2048-1536.jpg b/crm/public/manifest/apple-splash-2048-1536.jpg deleted file mode 100644 index f4213add2..000000000 Binary files a/crm/public/manifest/apple-splash-2048-1536.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2048-2732.jpg b/crm/public/manifest/apple-splash-2048-2732.jpg deleted file mode 100644 index 70b1e47c2..000000000 Binary files a/crm/public/manifest/apple-splash-2048-2732.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2160-1620.jpg b/crm/public/manifest/apple-splash-2160-1620.jpg deleted file mode 100644 index 7398911f1..000000000 Binary files a/crm/public/manifest/apple-splash-2160-1620.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2208-1242.jpg b/crm/public/manifest/apple-splash-2208-1242.jpg deleted file mode 100644 index 0da15d616..000000000 Binary files a/crm/public/manifest/apple-splash-2208-1242.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2266-1488.jpg b/crm/public/manifest/apple-splash-2266-1488.jpg deleted file mode 100644 index 5b8d7f16f..000000000 Binary files a/crm/public/manifest/apple-splash-2266-1488.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2360-1640.jpg b/crm/public/manifest/apple-splash-2360-1640.jpg deleted file mode 100644 index 6b112de41..000000000 Binary files a/crm/public/manifest/apple-splash-2360-1640.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2388-1668.jpg b/crm/public/manifest/apple-splash-2388-1668.jpg deleted file mode 100644 index 62b35cdf8..000000000 Binary files a/crm/public/manifest/apple-splash-2388-1668.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2436-1125.jpg b/crm/public/manifest/apple-splash-2436-1125.jpg deleted file mode 100644 index 30fe3e975..000000000 Binary files a/crm/public/manifest/apple-splash-2436-1125.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2532-1170.jpg b/crm/public/manifest/apple-splash-2532-1170.jpg deleted file mode 100644 index 420ecd299..000000000 Binary files a/crm/public/manifest/apple-splash-2532-1170.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2556-1179.jpg b/crm/public/manifest/apple-splash-2556-1179.jpg deleted file mode 100644 index a1d4841b0..000000000 Binary files a/crm/public/manifest/apple-splash-2556-1179.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2688-1242.jpg b/crm/public/manifest/apple-splash-2688-1242.jpg deleted file mode 100644 index 6c445c960..000000000 Binary files a/crm/public/manifest/apple-splash-2688-1242.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2732-2048.jpg b/crm/public/manifest/apple-splash-2732-2048.jpg deleted file mode 100644 index f4a7fe2bc..000000000 Binary files a/crm/public/manifest/apple-splash-2732-2048.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2778-1284.jpg b/crm/public/manifest/apple-splash-2778-1284.jpg deleted file mode 100644 index bef50099a..000000000 Binary files a/crm/public/manifest/apple-splash-2778-1284.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-2796-1290.jpg b/crm/public/manifest/apple-splash-2796-1290.jpg deleted file mode 100644 index e55a267f2..000000000 Binary files a/crm/public/manifest/apple-splash-2796-1290.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-640-1136.jpg b/crm/public/manifest/apple-splash-640-1136.jpg deleted file mode 100644 index d8bb053cc..000000000 Binary files a/crm/public/manifest/apple-splash-640-1136.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-750-1334.jpg b/crm/public/manifest/apple-splash-750-1334.jpg deleted file mode 100644 index 4e3713f04..000000000 Binary files a/crm/public/manifest/apple-splash-750-1334.jpg and /dev/null differ diff --git a/crm/public/manifest/apple-splash-828-1792.jpg b/crm/public/manifest/apple-splash-828-1792.jpg deleted file mode 100644 index ae35bb0bf..000000000 Binary files a/crm/public/manifest/apple-splash-828-1792.jpg and /dev/null differ diff --git a/crm/public/manifest/manifest-icon-192.maskable.png b/crm/public/manifest/manifest-icon-192.maskable.png deleted file mode 100644 index a600fecbd..000000000 Binary files a/crm/public/manifest/manifest-icon-192.maskable.png and /dev/null differ diff --git a/crm/public/manifest/manifest-icon-512.maskable.png b/crm/public/manifest/manifest-icon-512.maskable.png deleted file mode 100644 index de352b1ef..000000000 Binary files a/crm/public/manifest/manifest-icon-512.maskable.png and /dev/null differ diff --git a/crm/templates/__init__.py b/crm/templates/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/templates/emails/crm_invitation.html b/crm/templates/emails/crm_invitation.html deleted file mode 100644 index 3e74b1d1d..000000000 --- a/crm/templates/emails/crm_invitation.html +++ /dev/null @@ -1,4 +0,0 @@ -

You have been invited to join Frappe CRM

-

- Accept Invitation -

diff --git a/crm/templates/pages/__init__.py b/crm/templates/pages/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/crm/uninstall.py b/crm/uninstall.py deleted file mode 100644 index 34622d0b8..000000000 --- a/crm/uninstall.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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") - - fieldnames = ( - "enabled", - "reference_doctype", - ) - - for fieldname in fieldnames: - frappe.db.delete("Custom Field", {"name": "Email Template-" + fieldname}) - - frappe.clear_cache(doctype="Email Template") \ No newline at end of file diff --git a/crm/www/__init__.py b/crm/www/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 75e2d142b..97051cd69 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,5 @@ version: "3.7" -name: crm +name: next_crm services: mariadb: image: mariadb:10.8 diff --git a/docker/init.sh b/docker/init.sh index d9b89d811..4cb8361c5 100644 --- a/docker/init.sh +++ b/docker/init.sh @@ -30,10 +30,10 @@ bench new-site crm.localhost \ --admin-password admin \ --no-mariadb-socket -bench --site crm.localhost install-app crm +bench --site crm.localhost install-app next_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 use crm.localhost -bench start \ No newline at end of file +bench start diff --git a/frontend/index.html b/frontend/index.html index b9a44e485..fdaa97d0b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,9 +6,9 @@ name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover maximum-scale=1.0, user-scalable=no" /> - Frappe CRM + Next CRM - + @@ -16,181 +16,181 @@ rel="icon" type="image/png" sizes="196x196" - href="/assets/crm/manifest/apple-icon-180.png" + href="/assets/next_crm/manifest/apple-icon-180.png" /> diff --git a/frontend/package.json b/frontend/package.json index 566839632..f794b8d09 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,8 +4,8 @@ "version": "0.0.0", "scripts": { "dev": "vite", - "build": "vite build --base=/assets/crm/frontend/ && yarn copy-html-entry", - "copy-html-entry": "cp ../crm/public/frontend/index.html ../crm/www/crm.html", + "build": "vite build --base=/assets/next_crm/frontend/ && yarn copy-html-entry", + "copy-html-entry": "cp ../next_crm/public/frontend/index.html ../next_crm/www/next-crm/index.html", "serve": "vite preview" }, "dependencies": { diff --git a/frontend/src/components/Activities/Activities.vue b/frontend/src/components/Activities/Activities.vue index 2a3f3dd67..c3623dc10 100644 --- a/frontend/src/components/Activities/Activities.vue +++ b/frontend/src/components/Activities/Activities.vue @@ -63,8 +63,8 @@ -
- +
+
@@ -393,9 +393,9 @@ @click="emailBox.showComment = true" />
@@ -42,21 +40,9 @@ />
diff --git a/frontend/src/components/Activities/NoteArea.vue b/frontend/src/components/Activities/NoteArea.vue index 64b7293af..d93169770 100644 --- a/frontend/src/components/Activities/NoteArea.vue +++ b/frontend/src/components/Activities/NoteArea.vue @@ -65,7 +65,7 @@ const { getUser } = usersStore() async function deleteNote(name) { await call('frappe.client.delete', { - doctype: 'FCRM Note', + doctype: 'NCRM Note', name, }) notes.reload() diff --git a/frontend/src/components/Activities/TaskArea.vue b/frontend/src/components/Activities/ToDoArea.vue similarity index 69% rename from frontend/src/components/Activities/TaskArea.vue rename to frontend/src/components/Activities/ToDoArea.vue index 931c0007f..2d489d30e 100644 --- a/frontend/src/components/Activities/TaskArea.vue +++ b/frontend/src/components/Activities/ToDoArea.vue @@ -1,29 +1,29 @@ diff --git a/frontend/src/components/ColumnSettings.vue b/frontend/src/components/ColumnSettings.vue index 817a70c31..b3deb507d 100644 --- a/frontend/src/components/ColumnSettings.vue +++ b/frontend/src/components/ColumnSettings.vue @@ -11,9 +11,7 @@
- + diff --git a/frontend/src/components/Icons/DealsIcon.vue b/frontend/src/components/Icons/OpportunitiesIcon.vue similarity index 100% rename from frontend/src/components/Icons/DealsIcon.vue rename to frontend/src/components/Icons/OpportunitiesIcon.vue diff --git a/frontend/src/components/Icons/TaskIcon.vue b/frontend/src/components/Icons/ToDoIcon.vue similarity index 100% rename from frontend/src/components/Icons/TaskIcon.vue rename to frontend/src/components/Icons/ToDoIcon.vue diff --git a/frontend/src/components/Icons/TaskPriorityIcon.vue b/frontend/src/components/Icons/ToDoPriorityIcon.vue similarity index 53% rename from frontend/src/components/Icons/TaskPriorityIcon.vue rename to frontend/src/components/Icons/ToDoPriorityIcon.vue index dc2fdf6c2..a05a4d0d9 100644 --- a/frontend/src/components/Icons/TaskPriorityIcon.vue +++ b/frontend/src/components/Icons/ToDoPriorityIcon.vue @@ -2,17 +2,20 @@
diff --git a/frontend/src/components/ListViews/OrganizationsListView.vue b/frontend/src/components/ListViews/CustomersListView.vue similarity index 81% rename from frontend/src/components/ListViews/OrganizationsListView.vue rename to frontend/src/components/ListViews/CustomersListView.vue index 6099ba8d0..01c1fbcf7 100644 --- a/frontend/src/components/ListViews/OrganizationsListView.vue +++ b/frontend/src/components/ListViews/CustomersListView.vue @@ -4,8 +4,8 @@ :rows="rows" :options="{ getRowRoute: (row) => ({ - name: 'Organization', - params: { organizationId: row.name }, + name: 'Customer', + params: { customerId: row.name }, query: { view: route.query.view, viewType: route.params.viewType }, }), selectable: options.selectable, @@ -33,22 +33,11 @@ - +