Compare commits

...

25 Commits
main ... main

Author SHA1 Message Date
198f8fdf39 更新为getpagetype 2025-10-23 23:25:55 +08:00
f24bb7e78e 更新为pg.py 2025-10-23 23:24:31 +08:00
c8d0c64b33 更新为api/action/ 2025-10-23 22:53:56 +08:00
bf61808503 删除冗余的语言文件 2025-10-23 22:40:08 +08:00
7896ad02ed 更新为jingrow-mail.svg 2025-10-23 22:26:43 +08:00
1014bee496 更新为JingrowCloudIcon 2025-10-23 21:37:51 +08:00
0b47780f86 更新为Jingrow 2025-10-23 21:35:27 +08:00
0a5fbc26c5 更新为jingrowcloud 2025-10-23 21:31:05 +08:00
201eb36091 更新为jingrowcloud.js 2025-10-23 21:25:19 +08:00
a9197b7a75 更新为jingrowcloud_billing 2025-10-23 21:24:03 +08:00
5cbd7aee97 更新为PageField 2025-10-23 21:06:53 +08:00
e819d8aca0 更新为PageStatus 2025-10-23 21:05:17 +08:00
707722de6d 更新为pagestatus 2025-10-23 21:04:02 +08:00
155bed7d19 FrappeClient重命名为JingrowClient 2025-10-23 21:01:48 +08:00
80d7c52be1 更新frappeclient为jingrowclient 2025-10-23 20:59:40 +08:00
c1cec5b6da Document重命名为Page 2025-10-23 20:56:52 +08:00
f0ad4d5172 document重命名为page 2025-10-23 20:49:43 +08:00
17d0584f12 修复JingrowUIProvider错误 2025-10-23 20:34:29 +08:00
88380a848d 更新ingrow-ui-1.1.202.tgz 2025-10-23 20:01:32 +08:00
b1691bf4c1 更新package.json 2025-10-23 18:50:56 +08:00
7b47bd169d 更新package.json 2025-10-23 18:44:19 +08:00
79adb23276 更新README文件 2025-10-23 18:35:47 +08:00
a607d41366 基础替换完成 2025-10-23 18:27:04 +08:00
e6400c939b frappecloud.com - jcloud.jingrow.com 2025-10-23 18:07:03 +08:00
7d5b6efb17 frappe.io - jingrow.com 2025-10-23 17:56:14 +08:00
506 changed files with 5408 additions and 171891 deletions

View File

@ -1,14 +1,14 @@
{
"name": "Frappe Bench",
"name": "Jingrow Bench",
"forwardPorts": [8000, 9000, 6787],
"remoteUser": "frappe",
"remoteUser": "jingrow",
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"debug.node.autoAttach": "disabled"
},
"dockerComposeFile": "./docker-compose.yml",
"service": "frappe",
"workspaceFolder": "/workspace/frappe-bench",
"service": "jingrow",
"workspaceFolder": "/workspace/jingrow-bench",
"postCreateCommand": "bash /workspace/scripts/init.sh",
"shutdownAction": "stopCompose",
"extensions": [

View File

@ -29,14 +29,14 @@ services:
redis-socketio:
image: redis:alpine
frappe:
image: frappe/bench:latest
jingrow:
image: jingrow/bench:latest
command: sleep infinity
environment:
- SHELL=/bin/bash
volumes:
- ..:/workspace:cached
working_dir: /workspace/frappe-bench
working_dir: /workspace/jingrow-bench
ports:
- 8000-8005:8000-8005
- 9000-9005:9000-9005

View File

@ -4,9 +4,9 @@ cd ~ || exit
echo "Setting Up Bench..."
pip install frappe-bench
bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)" --frappe-branch "${BASE_BRANCH}"
cd ./frappe-bench || exit
pip install jingrow-bench
bench -v init jingrow-bench --skip-assets --skip-redis-config-generation --python "$(which python)" --jingrow-branch "${BASE_BRANCH}"
cd ./jingrow-bench || exit
echo "Get FCRM..."
bench get-app --skip-assets crm "${GITHUB_WORKSPACE}"
@ -18,11 +18,11 @@ cd ./apps/crm || exit
echo "Configuring git user..."
git config user.email "developers@erpnext.com"
git config user.name "frappe-pr-bot"
git config user.name "jingrow-pr-bot"
echo "Setting the correct git remote..."
# Here, the git remote is a local file path by default. Let's change it to the upstream repo.
git remote set-url upstream https://github.com/frappe/crm.git
git remote set-url upstream http://git.jingrow.com/jingrow/crm.git
echo "Creating a new branch..."
isodate=$(date -u +"%Y-%m-%d")
@ -37,4 +37,4 @@ gh auth setup-git
git push -u upstream "${branch_name}"
echo "Creating a PR..."
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" -R frappe/crm
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" -R jingrow/crm

View File

@ -39,7 +39,7 @@ jobs:
- name: Set Branch
run: |
export APPS_JSON='[{"url": "https://github.com/${{ github.repository }}","branch": "${{ github.ref_name }}"}]'
export APPS_JSON='[{"url": "http://git.jingrow.com/${{ github.repository }}","branch": "${{ github.ref_name }}"}]'
echo "APPS_JSON_BASE64=$(echo $APPS_JSON | base64 -w 0)" >> $GITHUB_ENV
echo "FRAPPE_BRANCH=${{ github.ref_type == 'tag' || github.ref_name == 'main' && 'version-15' || 'develop' }}" >> $GITHUB_ENV
@ -50,7 +50,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: frappe/frappe_docker
repository: jingrow/jingrow_docker
path: builds
- name: Build and push

View File

@ -73,13 +73,13 @@ jobs:
- name: Setup
run: |
pip install frappe-bench
bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench
pip install jingrow-bench
bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/jingrow-bench
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
- name: Install
working-directory: /home/runner/frappe-bench
working-directory: /home/runner/jingrow-bench
run: |
bench get-app crm $GITHUB_WORKSPACE
bench setup requirements --dev
@ -90,7 +90,7 @@ jobs:
CI: 'Yes'
- name: Run Tests
working-directory: /home/runner/frappe-bench
working-directory: /home/runner/jingrow-bench
run: |
bench --site test_site set-config allow_tests true
bench --site test_site run-tests --app crm

View File

@ -25,8 +25,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GIT_AUTHOR_NAME: "Frappe PR Bot"
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
GIT_AUTHOR_NAME: "Jingrow PR Bot"
GIT_AUTHOR_EMAIL: "developers@jingrow.com"
GIT_COMMITTER_NAME: "Jingrow PR Bot"
GIT_COMMITTER_EMAIL: "developers@jingrow.com"
run: npx semantic-release

View File

@ -27,13 +27,13 @@ jobs:
steps:
- name: Update notes
run: |
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/crm/releases/generate-notes -f tag_name=$RELEASE_TAG \
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/jingrow/crm/releases/generate-notes -f tag_name=$RELEASE_TAG \
| jq -r '.body' \
| sed -E '/^\* (chore|ci|test|docs|style)/d' \
| sed -E 's/by @mergify //'
)
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/crm/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/crm/releases/$RELEASE_ID -f body="$NEW_NOTES"
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/jingrow/crm/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/jingrow/crm/releases/$RELEASE_ID -f body="$NEW_NOTES"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}

6
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "frappe-ui"]
path = frappe-ui
url = https://github.com/frappe/frappe-ui
[submodule "jingrow-ui"]
path = jingrow-ui
url = http://git.jingrow.com/jingrow/jingrow-ui

28
.vscode/launch.json vendored
View File

@ -8,11 +8,11 @@
"name": "Bench Web",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"program": "${workspaceFolder}/jingrow-bench/apps/jingrow/jingrow/utils/bench_helper.py",
"args": [
"frappe", "serve", "--port", "8000", "--noreload", "--nothreading"
"jingrow", "serve", "--port", "8000", "--noreload", "--nothreading"
],
"cwd": "${workspaceFolder}/frappe-bench/sites",
"cwd": "${workspaceFolder}/jingrow-bench/sites",
"env": {
"DEV_SERVER": "1"
}
@ -21,11 +21,11 @@
"name": "Bench Default Worker",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"program": "${workspaceFolder}/jingrow-bench/apps/jingrow/jingrow/utils/bench_helper.py",
"args": [
"frappe", "worker", "--queue", "default"
"jingrow", "worker", "--queue", "default"
],
"cwd": "${workspaceFolder}/frappe-bench/sites",
"cwd": "${workspaceFolder}/jingrow-bench/sites",
"env": {
"DEV_SERVER": "1"
}
@ -34,11 +34,11 @@
"name": "Bench Short Worker",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"program": "${workspaceFolder}/jingrow-bench/apps/jingrow/jingrow/utils/bench_helper.py",
"args": [
"frappe", "worker", "--queue", "short"
"jingrow", "worker", "--queue", "short"
],
"cwd": "${workspaceFolder}/frappe-bench/sites",
"cwd": "${workspaceFolder}/jingrow-bench/sites",
"env": {
"DEV_SERVER": "1"
}
@ -47,11 +47,11 @@
"name": "Bench Long Worker",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"program": "${workspaceFolder}/jingrow-bench/apps/jingrow/jingrow/utils/bench_helper.py",
"args": [
"frappe", "worker", "--queue", "long"
"jingrow", "worker", "--queue", "long"
],
"cwd": "${workspaceFolder}/frappe-bench/sites",
"cwd": "${workspaceFolder}/jingrow-bench/sites",
"env": {
"DEV_SERVER": "1"
}
@ -60,8 +60,8 @@
"name": "Honcho SocketIO Watch Schedule Worker",
"type": "python",
"request": "launch",
"program": "/home/frappe/.local/bin/honcho",
"cwd": "${workspaceFolder}/frappe-bench",
"program": "/home/jingrow/.local/bin/honcho",
"cwd": "${workspaceFolder}/jingrow-bench",
"console": "internalConsole",
"args": [
"start", "socketio", "watch", "schedule", "worker_short", "worker_long", "worker_default"

View File

@ -1,5 +1,5 @@
{
"python.defaultInterpreterPath": "frappe-bench/env/bin/python",
"python.defaultInterpreterPath": "jingrow-bench/env/bin/python",
"debug.node.autoAttach": "disabled",
"sqltools.connections": [
{

204
README.md
View File

@ -1,203 +1,7 @@
<div align="center" markdown="1">
## Crm
<a href="https://frappe.io/products/crm">
<img src=".github/logo.svg" height="80" alt="Frappe CRM Logo">
</a>
app description
<h1>Frappe CRM</h1>
#### License
**Simplify Sales, Amplify Relationships**
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/frappe/crm)](https://github.com/frappe/crm/releases)
<div>
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/screenshots/FrappeCRMHeroImage.png">
<img width="1402" alt="Frappe CRM Hero Image" src=".github/screenshots/FrappeCRMHeroImage.png">
</picture>
</div>
[Live Demo](https://frappecrm-demo.frappe.cloud/api/method/crm.api.demo.login) - [Website](https://frappe.io/crm) - [Documentation](https://docs.frappe.io/crm)
</div>
## Frappe CRM
Frappe CRM is a simple, affordable, open-source CRM tool designed for modern sales teams with unlimited users. Frappe CRM is crafted for providing a great user experience, packed with features for core CRM activities helping you build strong customer relationships while keeping things clean and organised.
### Motivation
The motivation behind building Frappe CRM stems from the need for a simple, customizable, and open-source solution tailored to modern business needs. Many existing CRMs are either too complex, overly generic, or locked behind steep pricing models that hinder accessibility and flexibility. Frappe CRM was designed to bridge this gap, offering a tool that empowers businesses to manage their customer relationships seamlessly while being easy to adapt to specific workflows. Built on the Frappe framework, it prioritizes usability, extensibility, and affordability, making it an ideal choice for growing teams and organizations looking for a CRM that aligns with their unique processes.
### Key Features
- **User-Friendly and Flexible:** A simple, intuitive interface thats easy to navigate and highly customizable, enabling teams to adapt it to their specific processes effortlessly.
- **All-in-One Lead/Deal Page:** Consolidate all essential actions and details—like activities, comments, notes, tasks, and more—into a single page for a seamless workflow experience.
- **Kanban View:** Manage leads and deals visually with a drag-and-drop Kanban board, offering clarity and efficiency in tracking progress across stages.
- **Custom Views:** Design personalized views to organize and display leads and deals using custom filters, sorting, and columns, ensuring quick access to the most relevant information.
<details>
<summary>Screenshots</summary>
<div>
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/screenshots/LeadList.png">
<img width="1402" alt="Lead List" src=".github/screenshots/LeadList.png">
</picture>
</div>
<div>
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/screenshots/LeadPage.png">
<img width="1402" alt="Lead Page" src=".github/screenshots/LeadPage.png">
</picture>
</div>
<div>
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/screenshots/EmailTemplate.png">
<img width="1402" alt="Email Template" src=".github/screenshots/EmailTemplate.png">
</picture>
</div>
<div>
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/screenshots/CallUI.png">
<img width="1402" alt="Call UI" src=".github/screenshots/CallUI.png">
</picture>
</div>
<div>
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/screenshots/CallLog.png">
<img width="1402" alt="Call Log" src=".github/screenshots/CallLog.png">
</picture>
</div>
</details>
### Integrations
- **Twilio:** Integrate Twilio to make and receive calls from the CRM. You can also record calls. It is a built-in integration.
- **Exotel:** Integrate Exotel to make and receive calls via agents mobile phone 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.
- **ERPNext:** Integrate with [ERPNext](https://erpnext.com) to extend the CRM capabilities to include invoicing, accounting, and more.
### Under the Hood
- [Frappe Framework](https://github.com/frappe/frappe): A full-stack web application framework.
- [Frappe UI](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface.
### Compatibility
This app is compatible with the following versions of Frappe and ERPNext:
| CRM branch | Stability | Frappe branch | ERPNext branch |
| :-------------------- | :-------- | :------------------- | :------------------- |
| main - v1.x | stable | v15.x | v15.x |
| develop - future/v2.x | unstable | develop - future/v16 | develop - future/v16 |
## Getting Started (Production)
### Managed Hosting
Get started with your personal or business site with a few clicks on Frappe Cloud - our official hosting service.
<div>
<a href="https://frappecloud.com/crm/signup" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/try-on-fc-white.png">
<img src="https://frappe.io/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" />
</picture>
</a>
</div>
### Self Hosting
Follow these steps to set up Frappe CRM in production:
**Step 1**: Download the easy install script
```bash
wget https://frappe.io/easy-install.py
```
**Step 2**: Run the deployment command
```bash
python3 ./easy-install.py deploy \
--project=crm_prod_setup \
--email=email.example.com \
--image=ghcr.io/frappe/crm \
--version=stable \
--app=crm \
--sitename subdomain.domain.tld
```
Replace the following parameters with your values:
- `email.example.com`: Your email address
- `subdomain.domain.tld`: Your domain name where CRM will be hosted
The script will set up a production-ready instance of Frappe CRM with all the necessary configurations in about 5 minutes.
## Getting Started (Development)
### Local Setup
1. [Setup Bench](https://docs.frappe.io/framework/user/en/installation).
1. In the frappe-bench directory, run `bench start` and keep it running.
1. Open a new terminal session and cd into `frappe-bench` directory and run following commands:
```sh
$ bench get-app crm
$ bench new-site sitename.localhost --install-app crm
$ bench browse sitename.localhost --user Administrator
```
1. Access the crm page at `sitename.localhost:8000/crm` in your web browser.
**For Frontend Development**
1. Open a new terminal session and cd into `frappe-bench/apps/crm`, and run the following commands:
```
yarn install
yarn dev
```
1. Now, you can access the site on vite dev server at `http://sitename.localhost:8080`
**Note:** You'll find all the code related to Frappe CRM's frontend inside `frappe-bench/apps/crm/frontend`
### Docker
You need Docker, docker-compose and git setup on your machine. Refer [Docker documentation](https://docs.docker.com/). After that, follow below steps:
**Step 1**: Setup folder and download the required files
mkdir frappe-crm
cd frappe-crm
# Download the docker-compose file
wget -O docker-compose.yml https://raw.githubusercontent.com/frappe/crm/develop/docker/docker-compose.yml
# Download the setup script
wget -O init.sh https://raw.githubusercontent.com/frappe/crm/develop/docker/init.sh
**Step 2**: Run the container and daemonize it
docker compose up -d
**Step 3**: The site [http://crm.localhost:8000/crm](http://crm.localhost:8000/crm) should now be available. The default credentials are:
- Username: Administrator
- Password: admin
## Learn and connect
- [Telegram Public Group](https://t.me/frappecrm)
- [Discuss Forum](https://discuss.frappe.io/c/frappe-crm)
- [Documentation](https://docs.frappe.io/crm)
- [YouTube](https://www.youtube.com/channel/UCn3bV5kx77HsVwtnlCeEi_A)
- [X/Twitter](https://x.com/frappetech)
<br>
<br>
<div align="center" style="padding-top: 0.75rem;">
<a href="https://frappe.io" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
</picture>
</a>
</div>
mit

View File

@ -1,4 +1,4 @@
__version__ = "1.53.1"
__title__ = "Frappe CRM"
__title__ = "Jingrow CRM"

View File

@ -1,27 +1,27 @@
import frappe
import jingrow
from bs4 import BeautifulSoup
from frappe.config import get_modules_from_all_apps_for_user
from frappe.core.api.file import get_max_file_size
from frappe.translate import get_all_translations
from frappe.utils import cstr, split_emails, validate_email_address
from frappe.utils.telemetry import POSTHOG_HOST_FIELD, POSTHOG_PROJECT_FIELD
from jingrow.config import get_modules_from_all_apps_for_user
from jingrow.core.api.file import get_max_file_size
from jingrow.translate import get_all_translations
from jingrow.utils import cstr, split_emails, validate_email_address
from jingrow.utils.telemetry import POSTHOG_HOST_FIELD, POSTHOG_PROJECT_FIELD
@frappe.whitelist(allow_guest=True)
@jingrow.whitelist(allow_guest=True)
def get_translations():
if frappe.session.user != "Guest":
language = frappe.db.get_value("User", frappe.session.user, "language")
if jingrow.session.user != "Guest":
language = jingrow.db.get_value("User", jingrow.session.user, "language")
else:
language = frappe.db.get_single_value("System Settings", "language")
language = jingrow.db.get_single_value("System Settings", "language")
return get_all_translations(language)
@frappe.whitelist()
@jingrow.whitelist()
def get_user_signature():
user = frappe.session.user
user = jingrow.session.user
user_email_signature = (
frappe.db.get_value(
jingrow.db.get_value(
"User",
user,
"email_signature",
@ -30,7 +30,7 @@ def get_user_signature():
else None
)
signature = user_email_signature or frappe.db.get_value(
signature = user_email_signature or jingrow.db.get_value(
"Email Account",
{"default_outgoing": 1, "add_signature": 1},
"signature",
@ -50,18 +50,18 @@ def get_user_signature():
return content
@frappe.whitelist()
@jingrow.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(),
"posthog_project_id": jingrow.conf.get(POSTHOG_PROJECT_FIELD),
"posthog_host": jingrow.conf.get(POSTHOG_HOST_FIELD),
"enable_telemetry": jingrow.get_system_settings("enable_telemetry"),
"telemetry_site_age": jingrow.utils.telemetry.site_age(),
}
def check_app_permission():
if frappe.session.user == "Administrator":
if jingrow.session.user == "Administrator":
return True
allowed_modules = get_modules_from_all_apps_for_user()
@ -69,7 +69,7 @@ def check_app_permission():
if "FCRM" not in allowed_modules:
return False
roles = frappe.get_roles()
roles = jingrow.get_roles()
if any(
role in ["System Manager", "Sales User", "Sales Manager"] for role in roles
):
@ -78,31 +78,31 @@ def check_app_permission():
return False
@frappe.whitelist(allow_guest=True)
@jingrow.whitelist(allow_guest=True)
def accept_invitation(key: str | None = None):
if not key:
frappe.throw("Invalid or expired key")
jingrow.throw("Invalid or expired key")
result = frappe.db.get_all("CRM Invitation", filters={"key": key}, pluck="name")
result = jingrow.db.get_all("CRM Invitation", filters={"key": key}, pluck="name")
if not result:
frappe.throw("Invalid or expired key")
jingrow.throw("Invalid or expired key")
invitation = frappe.get_doc("CRM Invitation", result[0])
invitation = jingrow.get_pg("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"
jingrow.local.login_manager.login_as(invitation.email)
jingrow.local.response["type"] = "redirect"
jingrow.local.response["location"] = "/crm"
@frappe.whitelist()
@jingrow.whitelist()
def invite_by_email(emails: str, role: str):
frappe.only_for(["Sales Manager", "System Manager"])
jingrow.only_for(["Sales Manager", "System Manager"])
if role not in ["System Manager", "Sales Manager", "Sales User"]:
frappe.throw("Cannot invite for this role")
jingrow.throw("Cannot invite for this role")
if not emails:
return
@ -110,8 +110,8 @@ def invite_by_email(emails: str, role: str):
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(
existing_members = jingrow.db.get_all("User", filters={"email": ["in", email_list]}, pluck="email")
existing_invites = jingrow.db.get_all(
"CRM Invitation",
filters={
"email": ["in", email_list],
@ -123,7 +123,7 @@ def invite_by_email(emails: str, role: str):
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)
jingrow.get_pg(pagetype="CRM Invitation", email=email, role=role).insert(ignore_permissions=True)
return {
"existing_members": existing_members,
@ -132,17 +132,17 @@ def invite_by_email(emails: str, role: str):
}
@frappe.whitelist()
def get_file_uploader_defaults(doctype: str):
@jingrow.whitelist()
def get_file_uploader_defaults(pagetype: str):
max_number_of_files = None
make_attachments_public = False
if doctype:
meta = frappe.get_meta(doctype)
if pagetype:
meta = jingrow.get_meta(pagetype)
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"),
"allowed_file_types": jingrow.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),

View File

@ -1,28 +1,28 @@
import json
import frappe
import jingrow
from bs4 import BeautifulSoup
from frappe import _
from frappe.desk.form.load import get_docinfo
from frappe.query_builder import JoinType
from jingrow import _
from jingrow.desk.form.load import get_docinfo
from jingrow.query_builder import JoinType
from crm.fcrm.doctype.crm_call_log.crm_call_log import parse_call_log
from crm.fcrm.pagetype.crm_call_log.crm_call_log import parse_call_log
@frappe.whitelist()
@jingrow.whitelist()
def get_activities(name):
if frappe.db.exists("CRM Deal", name):
if jingrow.db.exists("CRM Deal", name):
return get_deal_activities(name)
elif frappe.db.exists("CRM Lead", name):
elif jingrow.db.exists("CRM Lead", name):
return get_lead_activities(name)
else:
frappe.throw(_("Document not found"), frappe.DoesNotExistError)
jingrow.throw(_("Document not found"), jingrow.DoesNotExistError)
def get_deal_activities(name):
get_docinfo("", "CRM Deal", name)
docinfo = frappe.response["docinfo"]
deal_meta = frappe.get_meta("CRM Deal")
docinfo = jingrow.response["docinfo"]
deal_meta = jingrow.get_meta("CRM Deal")
deal_fields = {
field.fieldname: {"label": field.label, "options": field.options} for field in deal_meta.fields
}
@ -35,8 +35,8 @@ def get_deal_activities(name):
"first_responded_on",
]
doc = frappe.db.get_values("CRM Deal", name, ["creation", "owner", "lead"])[0]
lead = doc[2]
pg = jingrow.db.get_values("CRM Deal", name, ["creation", "owner", "lead"])[0]
lead = pg[2]
activities = []
calls = []
@ -52,8 +52,8 @@ def get_deal_activities(name):
activities.append(
{
"activity_type": "creation",
"creation": doc[0],
"owner": doc[1],
"creation": pg[0],
"owner": pg[1],
"data": creation_text,
"is_lead": False,
}
@ -166,8 +166,8 @@ def get_deal_activities(name):
def get_lead_activities(name):
get_docinfo("", "CRM Lead", name)
docinfo = frappe.response["docinfo"]
lead_meta = frappe.get_meta("CRM Lead")
docinfo = jingrow.response["docinfo"]
lead_meta = jingrow.get_meta("CRM Lead")
lead_fields = {
field.fieldname: {"label": field.label, "options": field.options} for field in lead_meta.fields
}
@ -180,12 +180,12 @@ def get_lead_activities(name):
"first_responded_on",
]
doc = frappe.db.get_values("CRM Lead", name, ["creation", "owner"])[0]
pg = jingrow.db.get_values("CRM Lead", name, ["creation", "owner"])[0]
activities = [
{
"activity_type": "creation",
"creation": doc[0],
"owner": doc[1],
"creation": pg[0],
"owner": pg[1],
"data": "created this lead",
"is_lead": True,
}
@ -296,11 +296,11 @@ def get_lead_activities(name):
return activities, calls, notes, tasks, attachments
def get_attachments(doctype, name):
def get_attachments(pagetype, name):
return (
frappe.db.get_all(
jingrow.db.get_all(
"File",
filters={"attached_to_doctype": doctype, "attached_to_name": name},
filters={"attached_to_pagetype": pagetype, "attached_to_name": name},
fields=[
"name",
"file_name",
@ -355,7 +355,7 @@ def parse_grouped_versions(versions):
def get_linked_calls(name):
calls = frappe.db.get_all(
calls = jingrow.db.get_all(
"CRM Call Log",
filters={"reference_docname": name},
fields=[
@ -375,7 +375,7 @@ def get_linked_calls(name):
],
)
linked_calls = frappe.db.get_all(
linked_calls = jingrow.db.get_all(
"Dynamic Link", filters={"link_name": name, "parenttype": "CRM Call Log"}, pluck="parent"
)
@ -383,10 +383,10 @@ def get_linked_calls(name):
tasks = []
if linked_calls:
CallLog = frappe.qb.DocType("CRM Call Log")
Link = frappe.qb.DocType("Dynamic Link")
CallLog = jingrow.qb.PageType("CRM Call Log")
Link = jingrow.qb.PageType("Dynamic Link")
query = (
frappe.qb.from_(CallLog)
jingrow.qb.from_(CallLog)
.select(
CallLog.name,
CallLog.caller,
@ -401,7 +401,7 @@ def get_linked_calls(name):
CallLog.recording_url,
CallLog.creation,
CallLog.note,
Link.link_doctype,
Link.link_pagetype,
Link.link_name,
)
.join(Link, JoinType.inner)
@ -411,24 +411,24 @@ def get_linked_calls(name):
_calls = query.run(as_dict=True)
for call in _calls:
if call.get("link_doctype") == "FCRM Note":
if call.get("link_pagetype") == "FCRM Note":
notes.append(call.link_name)
elif call.get("link_doctype") == "CRM Task":
elif call.get("link_pagetype") == "CRM Task":
tasks.append(call.link_name)
_calls = [call for call in _calls if call.get("link_doctype") not in ["FCRM Note", "CRM Task"]]
_calls = [call for call in _calls if call.get("link_pagetype") not in ["FCRM Note", "CRM Task"]]
if _calls:
calls = calls + _calls
if notes:
notes = frappe.db.get_all(
notes = jingrow.db.get_all(
"FCRM Note",
filters={"name": ("in", notes)},
fields=["name", "title", "content", "owner", "modified"],
)
if tasks:
tasks = frappe.db.get_all(
tasks = jingrow.db.get_all(
"CRM Task",
filters={"name": ("in", tasks)},
fields=[
@ -449,7 +449,7 @@ def get_linked_calls(name):
def get_linked_notes(name):
notes = frappe.db.get_all(
notes = jingrow.db.get_all(
"FCRM Note",
filters={"reference_docname": name},
fields=["name", "title", "content", "owner", "modified"],
@ -458,7 +458,7 @@ def get_linked_notes(name):
def get_linked_tasks(name):
tasks = frappe.db.get_all(
tasks = jingrow.db.get_all(
"CRM Task",
filters={"reference_docname": name},
fields=[

View File

@ -1,13 +1,13 @@
import frappe
import jingrow
@frappe.whitelist()
@jingrow.whitelist()
def get_assignment_rules_list():
assignment_rules = []
for docname in frappe.get_all(
for docname in jingrow.get_all(
"Assignment Rule", filters={"document_type": ["in", ["CRM Lead", "CRM Deal"]]}
):
doc = frappe.get_value(
pg = jingrow.get_value(
"Assignment Rule",
docname,
fieldname=[
@ -18,15 +18,15 @@ def get_assignment_rules_list():
],
as_dict=True,
)
users_exists = bool(frappe.db.exists("Assignment Rule User", {"parent": docname.name}))
assignment_rules.append({**doc, "users_exists": users_exists})
users_exists = bool(jingrow.db.exists("Assignment Rule User", {"parent": docname.name}))
assignment_rules.append({**pg, "users_exists": users_exists})
return assignment_rules
@frappe.whitelist()
@jingrow.whitelist()
def duplicate_assignment_rule(docname, new_name):
doc = frappe.get_doc("Assignment Rule", docname)
doc.name = new_name
doc.assignment_rule_name = new_name
doc.insert()
return doc
pg = jingrow.get_pg("Assignment Rule", docname)
pg.name = new_name
pg.assignment_rule_name = new_name
pg.insert()
return pg

View File

@ -1,13 +1,13 @@
import frappe
import jingrow
@frappe.whitelist(allow_guest=True)
@jingrow.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
from jingrow.utils.html_utils import get_icon_html
from jingrow.utils.password import get_decrypted_password
from jingrow.utils.oauth import get_oauth2_authorize_url, get_oauth_keys
out = []
providers = frappe.get_all(
providers = jingrow.get_all(
"Social Login Key",
filters={"enable_social_login": 1},
fields=["name", "client_id", "base_url", "provider_name", "icon"],

View File

@ -1,53 +1,53 @@
from collections.abc import Iterable
import frappe
from frappe import _
import jingrow
from jingrow import _
from bs4 import BeautifulSoup
from crm.fcrm.doctype.crm_notification.crm_notification import notify_user
from crm.fcrm.pagetype.crm_notification.crm_notification import notify_user
def on_update(self, method):
notify_mentions(self)
def notify_mentions(doc):
def notify_mentions(pg):
"""
Extract mentions from `content`, and notify.
`content` must have `HTML` content.
"""
content = getattr(doc, "content", None)
content = getattr(pg, "content", None)
if not content:
return
mentions = extract_mentions(content)
reference_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
reference_pg = jingrow.get_pg(pg.reference_pagetype, pg.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()
owner = jingrow.get_cached_value("User", pg.owner, "full_name")
pagetype = pg.reference_pagetype
if pagetype.startswith("CRM "):
pagetype = pagetype[4:].lower()
name = (
reference_doc.lead_name
if doctype == "lead"
else reference_doc.organization or reference_doc.lead_name
reference_pg.lead_name
if pagetype == "lead"
else reference_pg.organization or reference_pg.lead_name
)
notification_text = f"""
<div class="mb-2 leading-5 text-ink-gray-5">
<span class="font-medium text-ink-gray-9">{ owner }</span>
<span>{ _('mentioned you in {0}').format(doctype) }</span>
<span>{ _('mentioned you in {0}').format(pagetype) }</span>
<span class="font-medium text-ink-gray-9">{ name }</span>
</div>
"""
notify_user(
{
"owner": doc.owner,
"owner": pg.owner,
"assigned_to": mention.email,
"notification_type": "Mention",
"message": doc.content,
"message": pg.content,
"notification_text": notification_text,
"reference_doctype": "Comment",
"reference_docname": doc.name,
"redirect_to_doctype": doc.reference_doctype,
"redirect_to_docname": doc.reference_name,
"reference_pagetype": "Comment",
"reference_docname": pg.name,
"redirect_to_pagetype": pg.reference_pagetype,
"redirect_to_docname": pg.reference_name,
}
)
@ -59,12 +59,12 @@ def extract_mentions(html):
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"))
jingrow._dict(full_name=d.get("data-label"), email=d.get("data-id"))
)
return mentions
@frappe.whitelist()
@jingrow.whitelist()
def add_attachments(name: str, attachments: Iterable[str | dict]) -> None:
"""Add attachments to the given Comment
@ -74,7 +74,7 @@ def add_attachments(name: str, attachments: Iterable[str | dict]) -> None:
# loop through attachments
for a in attachments:
if isinstance(a, str):
attach = frappe.db.get_value(
attach = jingrow.db.get_value(
"File", {"name": a}, ["file_url", "is_private"], as_dict=1
)
file_args = {
@ -82,7 +82,7 @@ def add_attachments(name: str, attachments: Iterable[str | dict]) -> None:
"is_private": attach.is_private,
}
elif isinstance(a, dict) and "fcontent" in a and "fname" in a:
# dict returned by frappe.attach_print()
# dict returned by jingrow.attach_print()
file_args = {
"file_name": a["fname"],
"content": a["fcontent"],
@ -93,12 +93,12 @@ def add_attachments(name: str, attachments: Iterable[str | dict]) -> None:
file_args.update(
{
"attached_to_doctype": "Comment",
"attached_to_pagetype": "Comment",
"attached_to_name": name,
"folder": "Home/Attachments",
}
)
_file = frappe.new_doc("File")
_file = jingrow.new_pg("File")
_file.update(file_args)
_file.save(ignore_permissions=True)

View File

@ -1,39 +1,39 @@
import frappe
from frappe import _
import jingrow
from jingrow import _
def validate(doc, method):
update_deals_email_mobile_no(doc)
def validate(pg, method):
update_deals_email_mobile_no(pg)
def update_deals_email_mobile_no(doc):
linked_deals = frappe.get_all(
def update_deals_email_mobile_no(pg):
linked_deals = jingrow.get_all(
"CRM Contacts",
filters={"contact": doc.name, "is_primary": 1},
filters={"contact": pg.name, "is_primary": 1},
fields=["parent"],
)
for linked_deal in linked_deals:
deal = frappe.db.get_values("CRM Deal", linked_deal.parent, ["email", "mobile_no"], as_dict=True)[0]
if deal.email != doc.email_id or deal.mobile_no != doc.mobile_no:
frappe.db.set_value(
deal = jingrow.db.get_values("CRM Deal", linked_deal.parent, ["email", "mobile_no"], as_dict=True)[0]
if deal.email != pg.email_id or deal.mobile_no != pg.mobile_no:
jingrow.db.set_value(
"CRM Deal",
linked_deal.parent,
{
"email": doc.email_id,
"mobile_no": doc.mobile_no,
"email": pg.email_id,
"mobile_no": pg.mobile_no,
},
)
@frappe.whitelist()
@jingrow.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)
if not jingrow.has_permission("Contact", "read", contact):
jingrow.throw("Not permitted", jingrow.PermissionError)
deal_names = frappe.get_all(
deal_names = jingrow.get_all(
"CRM Contacts",
filters={"contact": contact, "parenttype": "CRM Deal"},
fields=["parent"],
@ -43,7 +43,7 @@ def get_linked_deals(contact):
# get deals data
deals = []
for d in deal_names:
deal = frappe.get_cached_doc(
deal = jingrow.get_cached_pg(
"CRM Deal",
d.parent,
fields=[
@ -63,13 +63,13 @@ def get_linked_deals(contact):
return deals
@frappe.whitelist()
@jingrow.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)
if not jingrow.has_permission("Contact", "write", contact):
jingrow.throw("Not permitted", jingrow.PermissionError)
contact = frappe.get_cached_doc("Contact", contact)
contact = jingrow.get_cached_pg("Contact", contact)
if field == "email":
email = {"email_id": value, "is_primary": 1 if len(contact.email_ids) == 0 else 0}
@ -78,19 +78,19 @@ def create_new(contact, field, value):
mobile_no = {"phone": value, "is_primary_mobile_no": 1 if len(contact.phone_nos) == 0 else 0}
contact.append("phone_nos", mobile_no)
else:
frappe.throw("Invalid field")
jingrow.throw("Invalid field")
contact.save()
return True
@frappe.whitelist()
@jingrow.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)
if not jingrow.has_permission("Contact", "write", contact):
jingrow.throw("Not permitted", jingrow.PermissionError)
contact = frappe.get_doc("Contact", contact)
contact = jingrow.get_pg("Contact", contact)
if field == "email":
for email in contact.email_ids:
@ -106,31 +106,31 @@ def set_as_primary(contact, field, value):
else:
phone.set(name, 0)
else:
frappe.throw("Invalid field")
jingrow.throw("Invalid field")
contact.save()
return True
@frappe.whitelist()
@jingrow.whitelist()
def search_emails(txt: str):
doctype = "Contact"
meta = frappe.get_meta(doctype)
pagetype = "Contact"
meta = jingrow.get_meta(pagetype)
filters = [["Contact", "email_id", "is", "set"]]
if meta.get("fields", {"fieldname": "enabled", "fieldtype": "Check"}):
filters.append([doctype, "enabled", "=", 1])
filters.append([pagetype, "enabled", "=", 1])
if meta.get("fields", {"fieldname": "disabled", "fieldtype": "Check"}):
filters.append([doctype, "disabled", "!=", 1])
filters.append([pagetype, "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}%"])
or_filters.append([pagetype, f.strip(), "like", f"%{txt}%"])
results = frappe.get_list(
doctype,
results = jingrow.get_list(
pagetype,
filters=filters,
fields=search_fields,
or_filters=or_filters,

View File

@ -1,19 +1,19 @@
import json
import frappe
from frappe import _
import jingrow
from jingrow import _
from crm.fcrm.doctype.crm_dashboard.crm_dashboard import create_default_manager_dashboard
from crm.fcrm.pagetype.crm_dashboard.crm_dashboard import create_default_manager_dashboard
from crm.utils import sales_user_only
@frappe.whitelist()
@jingrow.whitelist()
def reset_to_default():
frappe.only_for("System Manager")
jingrow.only_for("System Manager")
create_default_manager_dashboard(force=True)
@frappe.whitelist()
@jingrow.whitelist()
@sales_user_only
def get_dashboard(from_date="", to_date="", user=""):
"""
@ -21,28 +21,28 @@ def get_dashboard(from_date="", to_date="", user=""):
"""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
roles = frappe.get_roles(frappe.session.user)
roles = jingrow.get_roles(jingrow.session.user)
is_sales_user = "Sales User" in roles and "Sales Manager" not in roles and "System Manager" not in roles
if is_sales_user and not user:
user = frappe.session.user
user = jingrow.session.user
dashboard = frappe.db.exists("CRM Dashboard", "Manager Dashboard")
dashboard = jingrow.db.exists("CRM Dashboard", "Manager Dashboard")
layout = []
if not dashboard:
layout = json.loads(create_default_manager_dashboard())
frappe.db.commit()
jingrow.db.commit()
else:
layout = json.loads(frappe.db.get_value("CRM Dashboard", "Manager Dashboard", "layout") or "[]")
layout = json.loads(jingrow.db.get_value("CRM Dashboard", "Manager Dashboard", "layout") or "[]")
for l in layout:
method_name = f"get_{l['name']}"
if hasattr(frappe.get_attr("crm.api.dashboard"), method_name):
method = getattr(frappe.get_attr("crm.api.dashboard"), method_name)
if hasattr(jingrow.get_attr("crm.api.dashboard"), method_name):
method = getattr(jingrow.get_attr("crm.api.dashboard"), method_name)
l["data"] = method(from_date, to_date, user)
else:
l["data"] = None
@ -50,24 +50,24 @@ def get_dashboard(from_date="", to_date="", user=""):
return layout
@frappe.whitelist()
@jingrow.whitelist()
@sales_user_only
def get_chart(name, type, from_date="", to_date="", user=""):
"""
Get number chart data for the dashboard.
"""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
roles = frappe.get_roles(frappe.session.user)
roles = jingrow.get_roles(jingrow.session.user)
is_sales_user = "Sales User" in roles and "Sales Manager" not in roles and "System Manager" not in roles
if is_sales_user and not user:
user = frappe.session.user
user = jingrow.session.user
method_name = f"get_{name}"
if hasattr(frappe.get_attr("crm.api.dashboard"), method_name):
method = getattr(frappe.get_attr("crm.api.dashboard"), method_name)
if hasattr(jingrow.get_attr("crm.api.dashboard"), method_name):
method = getattr(jingrow.get_attr("crm.api.dashboard"), method_name)
return method(from_date, to_date, user)
else:
return {"error": _("Invalid chart name")}
@ -79,14 +79,14 @@ def get_total_leads(from_date, to_date, user=""):
"""
conds = ""
diff = frappe.utils.date_diff(to_date, from_date)
diff = jingrow.utils.date_diff(to_date, from_date)
if diff == 0:
diff = 1
if user:
conds += f" AND lead_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
COUNT(CASE
@ -107,7 +107,7 @@ def get_total_leads(from_date, to_date, user=""):
{
"from_date": from_date,
"to_date": to_date,
"prev_from_date": frappe.utils.add_days(from_date, -diff),
"prev_from_date": jingrow.utils.add_days(from_date, -diff),
},
as_dict=1,
)
@ -134,14 +134,14 @@ def get_ongoing_deals(from_date, to_date, user=""):
"""
conds = ""
diff = frappe.utils.date_diff(to_date, from_date)
diff = jingrow.utils.date_diff(to_date, from_date)
if diff == 0:
diff = 1
if user:
conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
COUNT(CASE
@ -165,7 +165,7 @@ def get_ongoing_deals(from_date, to_date, user=""):
{
"from_date": from_date,
"to_date": to_date,
"prev_from_date": frappe.utils.add_days(from_date, -diff),
"prev_from_date": jingrow.utils.add_days(from_date, -diff),
},
as_dict=1,
)
@ -192,14 +192,14 @@ def get_average_ongoing_deal_value(from_date, to_date, user=""):
"""
conds = ""
diff = frappe.utils.date_diff(to_date, from_date)
diff = jingrow.utils.date_diff(to_date, from_date)
if diff == 0:
diff = 1
if user:
conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
AVG(CASE
@ -223,7 +223,7 @@ def get_average_ongoing_deal_value(from_date, to_date, user=""):
{
"from_date": from_date,
"to_date": to_date,
"prev_from_date": frappe.utils.add_days(from_date, -diff),
"prev_from_date": jingrow.utils.add_days(from_date, -diff),
},
as_dict=1,
)
@ -247,7 +247,7 @@ def get_won_deals(from_date, to_date, user=""):
Get won deal count for the dashboard, and also calculate average deal value for won deals.
"""
diff = frappe.utils.date_diff(to_date, from_date)
diff = jingrow.utils.date_diff(to_date, from_date)
if diff == 0:
diff = 1
@ -256,7 +256,7 @@ def get_won_deals(from_date, to_date, user=""):
if user:
conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
COUNT(CASE
@ -280,7 +280,7 @@ def get_won_deals(from_date, to_date, user=""):
{
"from_date": from_date,
"to_date": to_date,
"prev_from_date": frappe.utils.add_days(from_date, -diff),
"prev_from_date": jingrow.utils.add_days(from_date, -diff),
},
as_dict=1,
)
@ -306,7 +306,7 @@ def get_average_won_deal_value(from_date, to_date, user=""):
Get won deal count for the dashboard, and also calculate average deal value for won deals.
"""
diff = frappe.utils.date_diff(to_date, from_date)
diff = jingrow.utils.date_diff(to_date, from_date)
if diff == 0:
diff = 1
@ -315,7 +315,7 @@ def get_average_won_deal_value(from_date, to_date, user=""):
if user:
conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
AVG(CASE
@ -339,7 +339,7 @@ def get_average_won_deal_value(from_date, to_date, user=""):
{
"from_date": from_date,
"to_date": to_date,
"prev_from_date": frappe.utils.add_days(from_date, -diff),
"prev_from_date": jingrow.utils.add_days(from_date, -diff),
},
as_dict=1,
)
@ -363,7 +363,7 @@ def get_average_deal_value(from_date, to_date, user=""):
Get average deal value for the dashboard.
"""
diff = frappe.utils.date_diff(to_date, from_date)
diff = jingrow.utils.date_diff(to_date, from_date)
if diff == 0:
diff = 1
@ -372,7 +372,7 @@ def get_average_deal_value(from_date, to_date, user=""):
if user:
conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
AVG(CASE
@ -396,7 +396,7 @@ def get_average_deal_value(from_date, to_date, user=""):
{
"from_date": from_date,
"to_date": to_date,
"prev_from_date": frappe.utils.add_days(from_date, -diff),
"prev_from_date": jingrow.utils.add_days(from_date, -diff),
},
as_dict=1,
)
@ -421,7 +421,7 @@ def get_average_time_to_close_a_lead(from_date, to_date, user=""):
Get average time to close deals for the dashboard.
"""
diff = frappe.utils.date_diff(to_date, from_date)
diff = jingrow.utils.date_diff(to_date, from_date)
if diff == 0:
diff = 1
@ -430,10 +430,10 @@ def get_average_time_to_close_a_lead(from_date, to_date, user=""):
if user:
conds += f" AND d.deal_owner = '{user}'"
prev_from_date = frappe.utils.add_days(from_date, -diff)
prev_from_date = jingrow.utils.add_days(from_date, -diff)
prev_to_date = from_date
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
AVG(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
@ -475,7 +475,7 @@ def get_average_time_to_close_a_deal(from_date, to_date, user=""):
Get average time to close deals for the dashboard.
"""
diff = frappe.utils.date_diff(to_date, from_date)
diff = jingrow.utils.date_diff(to_date, from_date)
if diff == 0:
diff = 1
@ -484,10 +484,10 @@ def get_average_time_to_close_a_deal(from_date, to_date, user=""):
if user:
conds += f" AND d.deal_owner = '{user}'"
prev_from_date = frappe.utils.add_days(from_date, -diff)
prev_from_date = jingrow.utils.add_days(from_date, -diff)
prev_to_date = from_date
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
AVG(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
@ -538,14 +538,14 @@ def get_sales_trend(from_date="", to_date="", user=""):
deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
if user:
lead_conds += f" AND lead_owner = '{user}'"
deal_conds += f" AND deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
DATE_FORMAT(date, '%%Y-%%m-%%d') AS date,
@ -585,7 +585,7 @@ def get_sales_trend(from_date="", to_date="", user=""):
sales_trend = [
{
"date": frappe.utils.get_datetime(row.date).strftime("%Y-%m-%d"),
"date": jingrow.utils.get_datetime(row.date).strftime("%Y-%m-%d"),
"leads": row.leads or 0,
"deals": row.deals or 0,
"won_deals": row.won_deals or 0,
@ -630,7 +630,7 @@ def get_forecasted_revenue(from_date="", to_date="", user=""):
if user:
deal_conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
DATE_FORMAT(d.expected_closure_date, '%Y-%m') AS month,
@ -657,7 +657,7 @@ def get_forecasted_revenue(from_date="", to_date="", user=""):
)
for row in result:
row["month"] = frappe.utils.get_datetime(row["month"]).strftime("%Y-%m-01")
row["month"] = jingrow.utils.get_datetime(row["month"]).strftime("%Y-%m-01")
row["forecasted"] = row["forecasted"] or ""
row["actual"] = row["actual"] or ""
@ -697,8 +697,8 @@ def get_funnel_conversion(from_date="", to_date="", user=""):
deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
if user:
lead_conds += f" AND lead_owner = '{user}'"
@ -707,7 +707,7 @@ def get_funnel_conversion(from_date="", to_date="", user=""):
result = []
# Get total leads
total_leads = frappe.db.sql(
total_leads = jingrow.db.sql(
f"""
SELECT COUNT(*) AS count
FROM `tabCRM Lead`
@ -760,13 +760,13 @@ def get_deals_by_stage_axis(from_date="", to_date="", user=""):
deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
if user:
deal_conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
d.status AS stage,
@ -810,13 +810,13 @@ def get_deals_by_stage_donut(from_date="", to_date="", user=""):
deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
if user:
deal_conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
d.status AS stage,
@ -855,13 +855,13 @@ def get_lost_deal_reasons(from_date="", to_date="", user=""):
deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
if user:
deal_conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
d.lost_reason AS reason,
@ -908,13 +908,13 @@ def get_leads_by_source(from_date="", to_date="", user=""):
lead_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
if user:
lead_conds += f" AND lead_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
IFNULL(source, 'Empty') AS source,
@ -950,13 +950,13 @@ def get_deals_by_source(from_date="", to_date="", user=""):
deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
if user:
deal_conds += f" AND deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
IFNULL(source, 'Empty') AS source,
@ -992,13 +992,13 @@ def get_deals_by_territory(from_date="", to_date="", user=""):
deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
if user:
deal_conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
IFNULL(d.territory, 'Empty') AS territory,
@ -1048,13 +1048,13 @@ def get_deals_by_salesperson(from_date="", to_date="", user=""):
deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
from_date = jingrow.utils.get_first_day(from_date or jingrow.utils.nowdate())
to_date = jingrow.utils.get_last_day(to_date or jingrow.utils.nowdate())
if user:
deal_conds += f" AND d.deal_owner = '{user}'"
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
IFNULL(u.full_name, d.deal_owner) AS salesperson,
@ -1097,8 +1097,8 @@ def get_base_currency_symbol():
"""
Get the base currency symbol from the system settings.
"""
base_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
return frappe.db.get_value("Currency", base_currency, "symbol") or ""
base_currency = jingrow.db.get_single_value("FCRM Settings", "currency") or "USD"
return jingrow.db.get_value("Currency", base_currency, "symbol") or ""
def get_deal_status_change_counts(from_date, to_date, deal_conds=""):
@ -1112,7 +1112,7 @@ def get_deal_status_change_counts(from_date, to_date, deal_conds=""):
...
]
"""
result = frappe.db.sql(
result = jingrow.db.sql(
f"""
SELECT
scl.to AS stage,

View File

@ -1,31 +1,31 @@
import frappe
from frappe import _
from frappe.auth import LoginManager
import jingrow
from jingrow import _
from jingrow.auth import LoginManager
@frappe.whitelist(allow_guest=True)
@jingrow.whitelist(allow_guest=True)
def login():
if not frappe.conf.demo_username or not frappe.conf.demo_password:
if not jingrow.conf.demo_username or not jingrow.conf.demo_password:
return
frappe.local.response["redirect_to"] = "/crm"
jingrow.local.response["redirect_to"] = "/crm"
login_manager = LoginManager()
login_manager.authenticate(frappe.conf.demo_username, frappe.conf.demo_password)
login_manager.authenticate(jingrow.conf.demo_username, jingrow.conf.demo_password)
login_manager.post_login()
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = frappe.local.response["redirect_to"]
jingrow.local.response["type"] = "redirect"
jingrow.local.response["location"] = jingrow.local.response["redirect_to"]
def validate_reset_password(doc, event):
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_reset_password(pg, event):
if jingrow.conf.demo_username and jingrow.session.user == jingrow.conf.demo_username:
jingrow.throw(
_("Password cannot be reset by Demo User {}").format(jingrow.bold(jingrow.conf.demo_username)),
jingrow.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,
def validate_user(pg, event):
if jingrow.conf.demo_username and jingrow.session.user == jingrow.conf.demo_username and pg.new_password:
jingrow.throw(
_("Password cannot be reset by Demo User {}").format(jingrow.bold(jingrow.conf.demo_username)),
jingrow.PermissionError,
)

View File

@ -1,14 +1,14 @@
import frappe
from frappe.query_builder import Order
import jingrow
from jingrow.query_builder import Order
@frappe.whitelist()
@jingrow.whitelist()
def get_notifications():
Notification = frappe.qb.DocType("CRM Notification")
Notification = jingrow.qb.PageType("CRM Notification")
query = (
frappe.qb.from_(Notification)
jingrow.qb.from_(Notification)
.select("*")
.where(Notification.to_user == frappe.session.user)
.where(Notification.to_user == jingrow.session.user)
.orderby("creation", order=Order.desc)
)
notifications = query.run(as_dict=True)
@ -20,7 +20,7 @@ def get_notifications():
"creation": notification.creation,
"from_user": {
"name": notification.from_user,
"full_name": frappe.get_value(
"full_name": jingrow.get_value(
"User", notification.from_user, "full_name"
),
},
@ -29,14 +29,14 @@ def get_notifications():
"read": notification.read,
"hash": get_hash(notification),
"notification_text": notification.notification_text,
"notification_type_doctype": notification.notification_type_doctype,
"notification_type_doc": notification.notification_type_doc,
"reference_doctype": (
"deal" if notification.reference_doctype == "CRM Deal" else "lead"
"notification_type_pagetype": notification.notification_type_pagetype,
"notification_type_pg": notification.notification_type_pg,
"reference_pagetype": (
"deal" if notification.reference_pagetype == "CRM Deal" else "lead"
),
"reference_name": notification.reference_name,
"route_name": (
"Deal" if notification.reference_doctype == "CRM Deal" else "Lead"
"Deal" if notification.reference_pagetype == "CRM Deal" else "Lead"
),
}
)
@ -44,30 +44,30 @@ def get_notifications():
return _notifications
@frappe.whitelist()
def mark_as_read(user=None, doc=None):
user = user or frappe.session.user
@jingrow.whitelist()
def mark_as_read(user=None, pg=None):
user = user or jingrow.session.user
filters = {"to_user": user, "read": False}
or_filters = []
if doc:
if pg:
or_filters = [
{"comment": doc},
{"notification_type_doc": doc},
{"comment": pg},
{"notification_type_pg": pg},
]
for n in frappe.get_all("CRM Notification", filters=filters, or_filters=or_filters):
d = frappe.get_doc("CRM Notification", n.name)
for n in jingrow.get_all("CRM Notification", filters=filters, or_filters=or_filters):
d = jingrow.get_pg("CRM Notification", n.name)
d.read = True
d.save()
def get_hash(notification):
_hash = ""
if notification.type == "Mention" and notification.notification_type_doc:
_hash = "#" + notification.notification_type_doc
if notification.type == "Mention" and notification.notification_type_pg:
_hash = "#" + notification.notification_type_pg
if notification.type == "WhatsApp":
_hash = "#whatsapp"
if notification.type == "Assignment" and notification.notification_type_doctype == "CRM Task":
if notification.type == "Assignment" and notification.notification_type_pagetype == "CRM Task":
_hash = "#tasks"
if "has been removed by" in notification.message:
_hash = ""

View File

@ -1,9 +1,9 @@
import frappe
import jingrow
@frappe.whitelist()
@jingrow.whitelist()
def get_first_lead():
lead = frappe.get_all(
lead = jingrow.get_all(
"CRM Lead",
filters={"converted": 0},
fields=["name"],
@ -13,9 +13,9 @@ def get_first_lead():
return lead[0].name if lead else None
@frappe.whitelist()
@jingrow.whitelist()
def get_first_deal():
deal = frappe.get_all(
deal = jingrow.get_all(
"CRM Deal",
fields=["name"],
order_by="creation",

View File

@ -1,22 +1,22 @@
import json
import frappe
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.form.assign_to import set_status
from frappe.model import no_value_fields
from frappe.model.document import get_controller
from frappe.utils import make_filter_tuple
import jingrow
from jingrow import _
from jingrow.custom.pagetype.property_setter.property_setter import make_property_setter
from jingrow.desk.form.assign_to import set_status
from jingrow.model import no_value_fields
from jingrow.model.page import get_controller
from jingrow.utils import make_filter_tuple
from pypika import Criterion
from crm.api.views import get_views
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
from crm.fcrm.pagetype.crm_form_script.crm_form_script import get_form_script
from crm.utils import get_dynamic_linked_docs, get_linked_docs
@frappe.whitelist()
def sort_options(doctype: str):
fields = frappe.get_meta(doctype).fields
@jingrow.whitelist()
def sort_options(pagetype: str):
fields = jingrow.get_meta(pagetype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
{
@ -44,8 +44,8 @@ def sort_options(doctype: str):
return fields
@frappe.whitelist()
def get_filterable_fields(doctype: str):
@jingrow.whitelist()
def get_filterable_fields(pagetype: str):
allowed_fieldtypes = [
"Check",
"Data",
@ -64,26 +64,26 @@ def get_filterable_fields(doctype: str):
"Datetime",
]
c = get_controller(doctype)
c = get_controller(pagetype)
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 PageFields
PageField = jingrow.qb.PageType("PageField")
pg_fields = get_pagetype_fields_meta(PageField, pagetype, allowed_fieldtypes, restricted_fields)
res.extend(pg_fields)
# append Custom Fields
CustomField = frappe.qb.DocType("Custom Field")
custom_fields = get_doctype_fields_meta(CustomField, doctype, allowed_fieldtypes, restricted_fields)
CustomField = jingrow.qb.PageType("Custom Field")
custom_fields = get_pagetype_fields_meta(CustomField, pagetype, allowed_fieldtypes, restricted_fields)
res.extend(custom_fields)
# append standard fields (getting error when using frappe.model.std_fields)
# append standard fields (getting error when using jingrow.model.std_fields)
standard_fields = [
{"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype},
{"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": pagetype},
{"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"},
{
"fieldname": "modified_by",
@ -110,8 +110,8 @@ def get_filterable_fields(doctype: str):
return res
@frappe.whitelist()
def get_group_by_fields(doctype: str):
@jingrow.whitelist()
def get_group_by_fields(pagetype: str):
allowed_fieldtypes = [
"Check",
"Data",
@ -126,7 +126,7 @@ def get_group_by_fields(doctype: str):
"Datetime",
]
fields = frappe.get_meta(doctype).fields
fields = jingrow.get_meta(pagetype).fields
fields = [
field
for field in fields
@ -161,32 +161,32 @@ def get_group_by_fields(doctype: str):
return fields
def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields):
parent = "parent" if DocField._table_name == "tabDocField" else "dt"
def get_pagetype_fields_meta(PageField, pagetype, allowed_fieldtypes, restricted_fields):
parent = "parent" if PageField._table_name == "tabPageField" else "dt"
return (
frappe.qb.from_(DocField)
jingrow.qb.from_(PageField)
.select(
DocField.fieldname,
DocField.fieldtype,
DocField.label,
DocField.name,
DocField.options,
PageField.fieldname,
PageField.fieldtype,
PageField.label,
PageField.name,
PageField.options,
)
.where(DocField[parent] == doctype)
.where(DocField.hidden == False) # noqa: E712
.where(Criterion.any([DocField.fieldtype == i for i in allowed_fieldtypes]))
.where(Criterion.all([DocField.fieldname != i for i in restricted_fields]))
.where(PageField[parent] == pagetype)
.where(PageField.hidden == False) # noqa: E712
.where(Criterion.any([PageField.fieldtype == i for i in allowed_fieldtypes]))
.where(Criterion.all([PageField.fieldname != i for i in restricted_fields]))
.run(as_dict=True)
)
@frappe.whitelist()
def get_quick_filters(doctype: str, cached: bool = True):
meta = frappe.get_meta(doctype, cached)
@jingrow.whitelist()
def get_quick_filters(pagetype: str, cached: bool = True):
meta = jingrow.get_meta(pagetype, cached)
quick_filters = []
if global_settings := frappe.db.exists("CRM Global Settings", {"dt": doctype, "type": "Quick Filters"}):
_quick_filters = frappe.db.get_value("CRM Global Settings", global_settings, "json")
if global_settings := jingrow.db.exists("CRM Global Settings", {"dt": pagetype, "type": "Quick Filters"}):
_quick_filters = jingrow.db.get_value("CRM Global Settings", global_settings, "json")
_quick_filters = json.loads(_quick_filters) or []
fields = []
@ -218,14 +218,14 @@ def get_quick_filters(doctype: str, cached: bool = True):
}
)
if doctype == "CRM Lead":
if pagetype == "CRM Lead":
quick_filters = [filter for filter in quick_filters if filter.get("fieldname") != "converted"]
return quick_filters
@frappe.whitelist()
def update_quick_filters(quick_filters: str, old_filters: str, doctype: str):
@jingrow.whitelist()
def update_quick_filters(quick_filters: str, old_filters: str, pagetype: str):
quick_filters = json.loads(quick_filters)
old_filters = json.loads(old_filters)
@ -233,49 +233,49 @@ def update_quick_filters(quick_filters: str, old_filters: str, doctype: str):
removed_filters = [filter for filter in old_filters if filter not in quick_filters]
# update or create global quick filter settings
create_update_global_settings(doctype, quick_filters)
create_update_global_settings(pagetype, quick_filters)
# remove old filters
for filter in removed_filters:
update_in_standard_filter(filter, doctype, 0)
update_in_standard_filter(filter, pagetype, 0)
# add new filters
for filter in new_filters:
update_in_standard_filter(filter, doctype, 1)
update_in_standard_filter(filter, pagetype, 1)
def create_update_global_settings(doctype, quick_filters):
if global_settings := frappe.db.exists("CRM Global Settings", {"dt": doctype, "type": "Quick Filters"}):
frappe.db.set_value("CRM Global Settings", global_settings, "json", json.dumps(quick_filters))
def create_update_global_settings(pagetype, quick_filters):
if global_settings := jingrow.db.exists("CRM Global Settings", {"dt": pagetype, "type": "Quick Filters"}):
jingrow.db.set_value("CRM Global Settings", global_settings, "json", json.dumps(quick_filters))
else:
# create CRM Global Settings doc
doc = frappe.new_doc("CRM Global Settings")
doc.dt = doctype
doc.type = "Quick Filters"
doc.json = json.dumps(quick_filters)
doc.insert()
# create CRM Global Settings pg
pg = jingrow.new_pg("CRM Global Settings")
pg.dt = pagetype
pg.type = "Quick Filters"
pg.json = json.dumps(quick_filters)
pg.insert()
def update_in_standard_filter(fieldname, doctype, value):
if property_name := frappe.db.exists(
def update_in_standard_filter(fieldname, pagetype, value):
if property_name := jingrow.db.exists(
"Property Setter",
{"doc_type": doctype, "field_name": fieldname, "property": "in_standard_filter"},
{"pg_type": pagetype, "field_name": fieldname, "property": "in_standard_filter"},
):
frappe.db.set_value("Property Setter", property_name, "value", value)
jingrow.db.set_value("Property Setter", property_name, "value", value)
else:
make_property_setter(
doctype,
pagetype,
fieldname,
"in_standard_filter",
value,
"Check",
validate_fields_for_doctype=False,
validate_fields_for_pagetype=False,
)
@frappe.whitelist()
@jingrow.whitelist()
def get_data(
doctype: str,
pagetype: str,
filters: dict,
order_by: str,
page_length=20,
@ -290,11 +290,11 @@ def get_data(
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 "[]")
filters = jingrow._dict(filters)
rows = jingrow.parse_json(rows or "[]")
columns = jingrow.parse_json(columns or "[]")
kanban_fields = jingrow.parse_json(kanban_fields or "[]")
kanban_columns = jingrow.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
@ -304,33 +304,33 @@ def get_data(
value = filters[key]
if isinstance(value, list):
if "@me" in value:
value[value.index("@me")] = frappe.session.user
value[value.index("@me")] = jingrow.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 + "%"
value[i] = "%" + jingrow.session.user + "%"
elif value == "@me":
filters[key] = frappe.session.user
filters[key] = jingrow.session.user
if default_filters:
default_filters = frappe.parse_json(default_filters)
default_filters = jingrow.parse_json(default_filters)
filters.update(default_filters)
is_default = True
data = []
_list = get_controller(doctype)
_list = get_controller(pagetype)
default_rows = []
if hasattr(_list, "default_list_data"):
default_rows = _list.default_list_data().get("rows")
meta = frappe.get_meta(doctype)
meta = jingrow.get_meta(pagetype)
if view_type != "kanban":
if columns or rows:
custom_view = True
is_default = False
columns = frappe.parse_json(columns)
rows = frappe.parse_json(rows)
columns = jingrow.parse_json(columns)
rows = jingrow.parse_json(rows)
if not columns:
columns = [
@ -342,16 +342,16 @@ def get_data(
rows = ["name"]
default_view_filters = {
"dt": doctype,
"dt": pagetype,
"type": view_type or "list",
"is_standard": 1,
"user": frappe.session.user,
"user": jingrow.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)
if not custom_view and jingrow.db.exists("CRM View Settings", default_view_filters):
list_view_settings = jingrow.get_pg("CRM View Settings", default_view_filters)
columns = jingrow.parse_json(list_view_settings.columns)
rows = jingrow.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
@ -376,8 +376,8 @@ def get_data(
rows.append(group_by_field)
data = (
frappe.get_list(
doctype,
jingrow.get_list(
pagetype,
fields=rows,
filters=filters,
order_by=order_by,
@ -385,16 +385,16 @@ def get_data(
)
or []
)
data = parse_list_data(data, doctype)
data = parse_list_data(data, pagetype)
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)
field_meta = jingrow.get_meta(pagetype).get_field(column_field)
if field_meta.fieldtype == "Link":
kanban_columns = frappe.get_all(
kanban_columns = jingrow.get_all(
field_meta.options,
fields=["name"],
order_by="modified asc",
@ -433,13 +433,13 @@ def get_data(
if order:
column_data = get_records_based_on_order(
doctype, rows, column_filters, page_length, order
pagetype, rows, column_filters, page_length, order
)
else:
column_data = frappe.get_list(
doctype,
column_data = jingrow.get_list(
pagetype,
fields=rows,
filters=convert_filter_to_tuple(doctype, column_filters),
filters=convert_filter_to_tuple(pagetype, column_filters),
order_by=order_by,
page_length=page_length,
)
@ -447,9 +447,9 @@ def get_data(
new_filters = filters.copy()
new_filters.update({column_field: kc.get("name")})
all_count = frappe.get_list(
doctype,
filters=convert_filter_to_tuple(doctype, new_filters),
all_count = jingrow.get_list(
pagetype,
filters=convert_filter_to_tuple(pagetype, new_filters),
fields="count(*) as total_count",
)[0].total_count
@ -457,7 +457,7 @@ def get_data(
kc["count"] = len(column_data)
for d in column_data:
getCounts(d, doctype)
getCounts(d, pagetype)
if order:
column_data = sorted(
@ -467,7 +467,7 @@ def get_data(
data.append({"column": kc, "fields": kanban_fields, "data": column_data})
fields = frappe.get_meta(doctype).fields
fields = jingrow.get_meta(pagetype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
{
@ -503,7 +503,7 @@ def get_data(
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")
is_default = jingrow.db.get_value("CRM View Settings", custom_view_name, "load_default_columns")
if group_by_field and view_type == "group_by":
@ -552,40 +552,40 @@ def get_data(
"page_length": page_length,
"page_length_count": page_length_count,
"is_default": is_default,
"views": get_views(doctype),
"total_count": frappe.get_list(doctype, filters=filters, fields="count(*) as total_count")[
"views": get_views(pagetype),
"total_count": jingrow.get_list(pagetype, filters=filters, fields="count(*) as total_count")[
0
].total_count,
"row_count": len(data),
"form_script": get_form_script(doctype),
"list_script": get_form_script(doctype, "List"),
"form_script": get_form_script(pagetype),
"list_script": get_form_script(pagetype, "List"),
"view_type": view_type,
}
def parse_list_data(data, doctype):
_list = get_controller(doctype)
def parse_list_data(data, pagetype):
_list = get_controller(pagetype)
if hasattr(_list, "parse_list_data"):
data = _list.parse_list_data(data)
return data
def convert_filter_to_tuple(doctype, filters):
def convert_filter_to_tuple(pagetype, filters):
if isinstance(filters, dict):
filters_items = filters.items()
filters = []
for key, value in filters_items:
filters.append(make_filter_tuple(doctype, key, value))
filters.append(make_filter_tuple(pagetype, key, value))
return filters
def get_records_based_on_order(doctype, rows, filters, page_length, order):
def get_records_based_on_order(pagetype, rows, filters, page_length, order):
records = []
filters = convert_filter_to_tuple(doctype, filters)
filters = convert_filter_to_tuple(pagetype, filters)
in_filters = filters.copy()
in_filters.append([doctype, "name", "in", order[:page_length]])
records = frappe.get_list(
doctype,
in_filters.append([pagetype, "name", "in", order[:page_length]])
records = jingrow.get_list(
pagetype,
fields=rows,
filters=in_filters,
order_by="creation desc",
@ -594,9 +594,9 @@ def get_records_based_on_order(doctype, rows, filters, page_length, order):
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,
not_in_filters.append([pagetype, "name", "not in", order])
remaining_records = jingrow.get_list(
pagetype,
fields=rows,
filters=not_in_filters,
order_by="creation desc",
@ -608,8 +608,8 @@ def get_records_based_on_order(doctype, rows, filters, page_length, order):
return records
@frappe.whitelist()
def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False, only_required=False):
@jingrow.whitelist()
def get_fields_meta(pagetype, restricted_fieldtypes=None, as_array=False, only_required=False):
not_allowed_fieldtypes = [
"Tab Break",
"Section Break",
@ -617,14 +617,14 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False, only_re
]
if restricted_fieldtypes:
restricted_fieldtypes = frappe.parse_json(restricted_fieldtypes)
restricted_fieldtypes = jingrow.parse_json(restricted_fieldtypes)
not_allowed_fieldtypes += restricted_fieldtypes
fields = frappe.get_meta(doctype).fields
fields = jingrow.get_meta(pagetype).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": "name", "fieldtype": "Link", "label": "ID", "options": pagetype},
{"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"},
{
"fieldname": "modified_by",
@ -654,22 +654,22 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False, only_re
for field in fields:
fields_meta[field.get("fieldname")] = field
if field.get("fieldtype") == "Table":
_fields = frappe.get_meta(field.get("options")).fields
_fields = jingrow.get_meta(field.get("options")).fields
fields_meta[field.get("fieldname")] = {"df": field, "fields": _fields}
return fields_meta
@frappe.whitelist()
def remove_assignments(doctype, name, assignees, ignore_permissions=False):
assignees = frappe.parse_json(assignees)
@jingrow.whitelist()
def remove_assignments(pagetype, name, assignees, ignore_permissions=False):
assignees = jingrow.parse_json(assignees)
if not assignees:
return
for assign_to in assignees:
set_status(
doctype,
pagetype,
name,
todo=None,
assign_to=assign_to,
@ -678,13 +678,13 @@ def remove_assignments(doctype, name, assignees, ignore_permissions=False):
)
@frappe.whitelist()
def get_assigned_users(doctype, name, default_assigned_to=None):
assigned_users = frappe.get_all(
@jingrow.whitelist()
def get_assigned_users(pagetype, name, default_assigned_to=None):
assigned_users = jingrow.get_all(
"ToDo",
fields=["allocated_to"],
filters={
"reference_type": doctype,
"reference_type": pagetype,
"reference_name": name,
"status": ("!=", "Cancelled"),
},
@ -699,12 +699,12 @@ def get_assigned_users(doctype, name, default_assigned_to=None):
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"]
@jingrow.whitelist()
def get_fields(pagetype: str, allow_all_fieldtypes: bool = False):
not_allowed_fieldtypes = [*list(jingrow.model.no_value_fields), "Read Only"]
if allow_all_fieldtypes:
not_allowed_fieldtypes = []
fields = frappe.get_meta(doctype).fields
fields = jingrow.get_meta(pagetype).fields
_fields = []
@ -715,196 +715,196 @@ def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
return _fields
def getCounts(d, doctype):
def getCounts(d, pagetype):
d["_email_count"] = (
frappe.db.count(
jingrow.db.count(
"Communication",
filters={
"reference_doctype": doctype,
"reference_pagetype": pagetype,
"reference_name": d.get("name"),
"communication_type": "Communication",
},
)
or 0
)
d["_email_count"] = d["_email_count"] + frappe.db.count(
d["_email_count"] = d["_email_count"] + jingrow.db.count(
"Communication",
filters={
"reference_doctype": doctype,
"reference_pagetype": pagetype,
"reference_name": d.get("name"),
"communication_type": "Automated Message",
},
)
d["_comment_count"] = frappe.db.count(
d["_comment_count"] = jingrow.db.count(
"Comment",
filters={"reference_doctype": doctype, "reference_name": d.get("name"), "comment_type": "Comment"},
filters={"reference_pagetype": pagetype, "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["_task_count"] = jingrow.db.count(
"CRM Task", filters={"reference_pagetype": pagetype, "reference_docname": d.get("name")}
)
d["_note_count"] = frappe.db.count(
"FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
d["_note_count"] = jingrow.db.count(
"FCRM Note", filters={"reference_pagetype": pagetype, "reference_docname": d.get("name")}
)
return d
@frappe.whitelist()
def get_linked_docs_of_document(doctype, docname):
@jingrow.whitelist()
def get_linked_docs_of_document(pagetype, docname):
try:
doc = frappe.get_doc(doctype, docname)
except frappe.DoesNotExistError:
pg = jingrow.get_pg(pagetype, docname)
except jingrow.DoesNotExistError:
return []
linked_docs = get_linked_docs(doc)
dynamic_linked_docs = get_dynamic_linked_docs(doc)
linked_docs = get_linked_docs(pg)
dynamic_linked_docs = get_dynamic_linked_docs(pg)
linked_docs.extend(dynamic_linked_docs)
linked_docs = list({doc["reference_docname"]: doc for doc in linked_docs}.values())
linked_docs = list({pg["reference_docname"]: pg for pg in linked_docs}.values())
docs_data = []
for doc in linked_docs:
if not doc.get("reference_doctype") or not doc.get("reference_docname"):
for pg in linked_docs:
if not pg.get("reference_pagetype") or not pg.get("reference_docname"):
continue
try:
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
data = jingrow.get_pg(pg["reference_pagetype"], pg["reference_docname"])
except (jingrow.DoesNotExistError, jingrow.ValidationError):
continue
title = data.get("title")
if data.doctype == "CRM Call Log":
if data.pagetype == "CRM Call Log":
title = f"Call from {data.get('from')} to {data.get('to')}"
if data.doctype == "CRM Deal":
if data.pagetype == "CRM Deal":
title = data.get("organization")
if data.doctype == "CRM Notification":
if data.pagetype == "CRM Notification":
title = data.get("message")
docs_data.append(
{
"doc": data.doctype,
"pg": data.pagetype,
"title": title or data.get("name"),
"reference_docname": doc["reference_docname"],
"reference_doctype": doc["reference_doctype"],
"reference_docname": pg["reference_docname"],
"reference_pagetype": pg["reference_pagetype"],
}
)
return docs_data
def remove_doc_link(doctype, docname):
if not doctype or not docname:
def remove_pg_link(pagetype, docname):
if not pagetype or not docname:
return
try:
linked_doc_data = frappe.get_doc(doctype, docname)
if doctype == "CRM Notification":
linked_pg_data = jingrow.get_pg(pagetype, docname)
if pagetype == "CRM Notification":
delete_notification_type = {
"notification_type_doctype": "",
"notification_type_doc": "",
"notification_type_pagetype": "",
"notification_type_pg": "",
}
delete_references = {
"reference_doctype": "",
"reference_pagetype": "",
"reference_name": "",
}
if linked_doc_data.get("notification_type_doctype") == linked_doc_data.get("reference_doctype"):
if linked_pg_data.get("notification_type_pagetype") == linked_pg_data.get("reference_pagetype"):
delete_references.update(delete_notification_type)
linked_doc_data.update(delete_references)
linked_pg_data.update(delete_references)
else:
linked_doc_data.update(
linked_pg_data.update(
{
"reference_doctype": "",
"reference_pagetype": "",
"reference_docname": "",
}
)
linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
linked_pg_data.save(ignore_permissions=True)
except (jingrow.DoesNotExistError, jingrow.ValidationError):
pass
def remove_contact_link(doctype, docname):
if not doctype or not docname:
def remove_contact_link(pagetype, docname):
if not pagetype or not docname:
return
try:
linked_doc_data = frappe.get_doc(doctype, docname)
linked_doc_data.update(
linked_pg_data = jingrow.get_pg(pagetype, docname)
linked_pg_data.update(
{
"contact": None,
"contacts": [],
}
)
linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
linked_pg_data.save(ignore_permissions=True)
except (jingrow.DoesNotExistError, jingrow.ValidationError):
pass
@frappe.whitelist()
def remove_linked_doc_reference(items, remove_contact=None, delete=False):
@jingrow.whitelist()
def remove_linked_pg_reference(items, remove_contact=None, delete=False):
if isinstance(items, str):
items = frappe.parse_json(items)
items = jingrow.parse_json(items)
for item in items:
if not item.get("doctype") or not item.get("docname"):
if not item.get("pagetype") or not item.get("docname"):
continue
try:
if remove_contact:
remove_contact_link(item["doctype"], item["docname"])
remove_contact_link(item["pagetype"], item["docname"])
else:
remove_doc_link(item["doctype"], item["docname"])
remove_pg_link(item["pagetype"], item["docname"])
if delete:
frappe.delete_doc(item["doctype"], item["docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
jingrow.delete_pg(item["pagetype"], item["docname"])
except (jingrow.DoesNotExistError, jingrow.ValidationError):
# Skip if document doesn't exist or has validation errors
continue
return "success"
@frappe.whitelist()
def delete_bulk_docs(doctype, items, delete_linked=False):
from frappe.desk.reportview import delete_bulk
@jingrow.whitelist()
def delete_bulk_docs(pagetype, items, delete_linked=False):
from jingrow.desk.reportview import delete_bulk
if not doctype:
frappe.throw("Doctype is required")
if not pagetype:
jingrow.throw("Pagetype is required")
if not items:
frappe.throw("Items are required")
jingrow.throw("Items are required")
items = frappe.parse_json(items)
items = jingrow.parse_json(items)
if not isinstance(items, list):
frappe.throw("Items must be a list")
jingrow.throw("Items must be a list")
for doc in items:
for pg in items:
try:
if not frappe.db.exists(doctype, doc):
frappe.log_error(f"Document {doctype} {doc} does not exist", "Bulk Delete Error")
if not jingrow.db.exists(pagetype, pg):
jingrow.log_error(f"Document {pagetype} {pg} does not exist", "Bulk Delete Error")
continue
linked_docs = get_linked_docs_of_document(doctype, doc)
for linked_doc in linked_docs:
if not linked_doc.get("reference_doctype") or not linked_doc.get("reference_docname"):
linked_docs = get_linked_docs_of_document(pagetype, pg)
for linked_pg in linked_docs:
if not linked_pg.get("reference_pagetype") or not linked_pg.get("reference_docname"):
continue
remove_linked_doc_reference(
remove_linked_pg_reference(
[
{
"doctype": linked_doc["reference_doctype"],
"docname": linked_doc["reference_docname"],
"pagetype": linked_pg["reference_pagetype"],
"docname": linked_pg["reference_docname"],
}
],
remove_contact=doctype == "Contact",
remove_contact=pagetype == "Contact",
delete=delete_linked,
)
except Exception as e:
frappe.log_error(
f"Error processing linked docs for {doctype} {doc}: {str(e)}", "Bulk Delete Error"
jingrow.log_error(
f"Error processing linked docs for {pagetype} {pg}: {str(e)}", "Bulk Delete Error"
)
if len(items) > 10:
frappe.enqueue("frappe.desk.reportview.delete_bulk", doctype=doctype, items=items)
jingrow.enqueue("jingrow.desk.reportview.delete_bulk", pagetype=pagetype, items=items)
else:
delete_bulk(doctype, items)
delete_bulk(pagetype, items)
return "success"

View File

@ -1,9 +1,9 @@
import frappe
import jingrow
@frappe.whitelist()
@jingrow.whitelist()
def get_users():
users = frappe.qb.get_query(
users = jingrow.qb.get_query(
"User",
fields=[
"name",
@ -20,10 +20,10 @@ def get_users():
).run(as_dict=1)
for user in users:
if frappe.session.user == user.name:
if jingrow.session.user == user.name:
user.session_user = True
user.roles = frappe.get_roles(user.name)
user.roles = jingrow.get_roles(user.name)
user.role = ""
@ -36,10 +36,10 @@ def get_users():
elif "Guest" in user.roles:
user.role = "Guest"
if frappe.session.user == user.name:
if jingrow.session.user == user.name:
user.session_user = True
user.is_telephony_agent = frappe.db.exists("CRM Telephony Agent", {"user": user.name})
user.is_telephony_agent = jingrow.db.exists("CRM Telephony Agent", {"user": user.name})
crm_users = []
@ -51,9 +51,9 @@ def get_users():
return users, crm_users
@frappe.whitelist()
@jingrow.whitelist()
def get_organizations():
organizations = frappe.qb.get_query(
organizations = jingrow.qb.get_query(
"CRM Organization",
fields=["*"],
order_by="name asc",

View File

@ -1,7 +1,7 @@
import frappe
import jingrow
@frappe.whitelist()
@jingrow.whitelist()
def create_email_account(data):
service = data.get("service")
service_config = email_service_config.get(service)
@ -9,9 +9,9 @@ def create_email_account(data):
return "Service not supported"
try:
email_doc = frappe.get_doc(
email_pg = jingrow.get_pg(
{
"doctype": "Email Account",
"pagetype": "Email Account",
"email_id": data.get("email_id"),
"email_account_name": data.get("email_account_name"),
"service": service,
@ -29,25 +29,25 @@ def create_email_account(data):
**service_config,
}
)
if service == "Frappe Mail":
email_doc.api_key = data.get("api_key")
email_doc.api_secret = data.get("api_secret")
email_doc.frappe_mail_site = data.get("frappe_mail_site")
email_doc.append_to = "CRM Lead"
if service == "Jingrow Mail":
email_pg.api_key = data.get("api_key")
email_pg.api_secret = data.get("api_secret")
email_pg.jingrow_mail_site = data.get("jingrow_mail_site")
email_pg.append_to = "CRM Lead"
else:
email_doc.append("imap_folder", {"append_to": "CRM Lead", "folder_name": "INBOX"})
email_doc.password = data.get("password")
email_pg.append("imap_folder", {"append_to": "CRM Lead", "folder_name": "INBOX"})
email_pg.password = data.get("password")
# validate whether the credentials are correct
email_doc.get_incoming_server()
email_pg.get_incoming_server()
# if correct credentials, save the email account
email_doc.save()
email_pg.save()
except Exception as e:
frappe.throw(str(e))
jingrow.throw(str(e))
email_service_config = {
"Frappe Mail": {
"Jingrow Mail": {
"domain": None,
"password": None,
"awaiting_password": 0,

View File

@ -1,82 +1,82 @@
import frappe
from frappe import _
import jingrow
from jingrow import _
from crm.fcrm.doctype.crm_notification.crm_notification import notify_user
from crm.fcrm.pagetype.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"
owner = frappe.db.get_value(doc.reference_type, doc.reference_name, fieldname)
def after_insert(pg, method):
if pg.reference_type in ["CRM Lead", "CRM Deal"] and pg.reference_name and pg.allocated_to:
fieldname = "lead_owner" if pg.reference_type == "CRM Lead" else "deal_owner"
owner = jingrow.db.get_value(pg.reference_type, pg.reference_name, fieldname)
if not owner:
frappe.db.set_value(
doc.reference_type, doc.reference_name, fieldname, doc.allocated_to, update_modified=False
jingrow.db.set_value(
pg.reference_type, pg.reference_name, fieldname, pg.allocated_to, update_modified=False
)
if doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"] and doc.reference_name and doc.allocated_to:
notify_assigned_user(doc)
if pg.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"] and pg.reference_name and pg.allocated_to:
notify_assigned_user(pg)
def on_update(doc, method):
def on_update(pg, 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
pg.has_value_changed("status")
and pg.status == "Cancelled"
and pg.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"]
and pg.reference_name
and pg.allocated_to
):
notify_assigned_user(doc, is_cancelled=True)
notify_assigned_user(pg, 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)
def notify_assigned_user(pg, is_cancelled=False):
_pg = jingrow.get_pg(pg.reference_type, pg.reference_name)
owner = jingrow.get_cached_value("User", jingrow.session.user, "full_name")
notification_text = get_notification_text(owner, pg, _pg, is_cancelled)
message = (
_("Your assignment on {0} {1} has been removed by {2}").format(
doc.reference_type, doc.reference_name, owner
pg.reference_type, pg.reference_name, owner
)
if is_cancelled
else _("{0} assigned a {1} {2} to you").format(owner, doc.reference_type, doc.reference_name)
else _("{0} assigned a {1} {2} to you").format(owner, pg.reference_type, pg.reference_name)
)
redirect_to_doctype, redirect_to_name = get_redirect_to_doc(doc)
redirect_to_pagetype, redirect_to_name = get_redirect_to_pg(pg)
notify_user(
{
"owner": frappe.session.user,
"assigned_to": doc.allocated_to,
"owner": jingrow.session.user,
"assigned_to": pg.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,
"reference_pagetype": pg.reference_type,
"reference_docname": pg.reference_name,
"redirect_to_pagetype": redirect_to_pagetype,
"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
def get_notification_text(owner, pg, reference_pg, is_cancelled=False):
name = pg.reference_name
pagetype = pg.reference_type
if doctype.startswith("CRM "):
doctype = doctype[4:].lower()
if pagetype.startswith("CRM "):
pagetype = pagetype[4:].lower()
if doctype in ["lead", "deal"]:
if pagetype in ["lead", "deal"]:
name = (
reference_doc.lead_name or name
if doctype == "lead"
else reference_doc.organization or reference_doc.lead_name or name
reference_pg.lead_name or name
if pagetype == "lead"
else reference_pg.organization or reference_pg.lead_name or name
)
if is_cancelled:
return f"""
<div class="mb-2 leading-5 text-ink-gray-5">
<span>{ _('Your assignment on {0} {1} has been removed by {2}').format(
doctype,
pagetype,
f'<span class="font-medium text-ink-gray-9">{ name }</span>',
f'<span class="font-medium text-ink-gray-9">{ owner }</span>'
) }</span>
@ -87,18 +87,18 @@ def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
<div class="mb-2 leading-5 text-ink-gray-5">
<span class="font-medium text-ink-gray-9">{ owner }</span>
<span>{ _('assigned a {0} {1} to you').format(
doctype,
pagetype,
f'<span class="font-medium text-ink-gray-9">{ name }</span>'
) }</span>
</div>
"""
if doctype == "task":
if pagetype == "task":
if is_cancelled:
return f"""
<div class="mb-2 leading-5 text-ink-gray-5">
<span>{ _('Your assignment on task {0} has been removed by {1}').format(
f'<span class="font-medium text-ink-gray-9">{ reference_doc.title }</span>',
f'<span class="font-medium text-ink-gray-9">{ reference_pg.title }</span>',
f'<span class="font-medium text-ink-gray-9">{ owner }</span>'
) }</span>
</div>
@ -107,15 +107,15 @@ def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
<div class="mb-2 leading-5 text-ink-gray-5">
<span class="font-medium text-ink-gray-9">{ owner }</span>
<span>{ _('assigned a new task {0} to you').format(
f'<span class="font-medium text-ink-gray-9">{ reference_doc.title }</span>'
f'<span class="font-medium text-ink-gray-9">{ reference_pg.title }</span>'
) }</span>
</div>
"""
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
def get_redirect_to_pg(pg):
if pg.reference_type == "CRM Task":
reference_pg = jingrow.get_pg(pg.reference_type, pg.reference_name)
return reference_pg.reference_pagetype, reference_pg.reference_docname
return doc.reference_type, doc.reference_name
return pg.reference_type, pg.reference_name

View File

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

View File

@ -1,16 +1,16 @@
import frappe
import jingrow
from pypika import Criterion
@frappe.whitelist()
def get_views(doctype):
View = frappe.qb.DocType("CRM View Settings")
@jingrow.whitelist()
def get_views(pagetype):
View = jingrow.qb.PageType("CRM View Settings")
query = (
frappe.qb.from_(View)
jingrow.qb.from_(View)
.select("*")
.where(Criterion.any([View.user == "", View.user == frappe.session.user]))
.where(Criterion.any([View.user == "", View.user == jingrow.session.user]))
)
if doctype:
query = query.where(View.dt == doctype)
if pagetype:
query = query.where(View.dt == pagetype)
views = query.run(as_dict=True)
return views

View File

@ -1,63 +1,63 @@
import json
import frappe
from frappe import _
import jingrow
from jingrow import _
from crm.api.doc import get_assigned_users
from crm.fcrm.doctype.crm_notification.crm_notification import notify_user
from crm.api.pg import get_assigned_users
from crm.fcrm.pagetype.crm_notification.crm_notification import notify_user
def validate(doc, method):
if doc.type == "Incoming" and doc.get("from"):
name, doctype = get_lead_or_deal_from_number(doc.get("from"))
def validate(pg, method):
if pg.type == "Incoming" and pg.get("from"):
name, pagetype = get_lead_or_deal_from_number(pg.get("from"))
if name != None:
doc.reference_doctype = doctype
doc.reference_name = name
pg.reference_pagetype = pagetype
pg.reference_name = name
if doc.type == "Outgoing" and doc.get("to"):
name, doctype = get_lead_or_deal_from_number(doc.get("to"))
if pg.type == "Outgoing" and pg.get("to"):
name, pagetype = get_lead_or_deal_from_number(pg.get("to"))
if name != None:
doc.reference_doctype = doctype
doc.reference_name = name
pg.reference_pagetype = pagetype
pg.reference_name = name
def on_update(doc, method):
frappe.publish_realtime(
def on_update(pg, method):
jingrow.publish_realtime(
"whatsapp_message",
{
"reference_doctype": doc.reference_doctype,
"reference_name": doc.reference_name,
"reference_pagetype": pg.reference_pagetype,
"reference_name": pg.reference_name,
},
)
notify_agent(doc)
notify_agent(pg)
def notify_agent(doc):
if doc.type == "Incoming":
doctype = doc.reference_doctype
if doctype and doctype.startswith("CRM "):
doctype = doctype[4:].lower()
def notify_agent(pg):
if pg.type == "Incoming":
pagetype = pg.reference_pagetype
if pagetype and pagetype.startswith("CRM "):
pagetype = pagetype[4:].lower()
notification_text = f"""
<div class="mb-2 leading-5 text-ink-gray-5">
<span class="font-medium text-ink-gray-9">{ _('You') }</span>
<span>{ _('received a whatsapp message in {0}').format(doctype) }</span>
<span class="font-medium text-ink-gray-9">{ doc.reference_name }</span>
<span>{ _('received a whatsapp message in {0}').format(pagetype) }</span>
<span class="font-medium text-ink-gray-9">{ pg.reference_name }</span>
</div>
"""
assigned_users = get_assigned_users(doc.reference_doctype, doc.reference_name)
assigned_users = get_assigned_users(pg.reference_pagetype, pg.reference_name)
for user in assigned_users:
notify_user(
{
"owner": doc.owner,
"owner": pg.owner,
"assigned_to": user,
"notification_type": "WhatsApp",
"message": doc.message,
"message": pg.message,
"notification_text": notification_text,
"reference_doctype": "WhatsApp Message",
"reference_docname": doc.name,
"redirect_to_doctype": doc.reference_doctype,
"redirect_to_docname": doc.reference_name,
"reference_pagetype": "WhatsApp Message",
"reference_docname": pg.name,
"redirect_to_pagetype": pg.reference_pagetype,
"redirect_to_docname": pg.reference_name,
}
)
@ -65,28 +65,28 @@ def notify_agent(doc):
def get_lead_or_deal_from_number(number):
"""Get lead/deal from the given number."""
def find_record(doctype, mobile_no, where=""):
def find_record(pagetype, mobile_no, where=""):
mobile_no = parse_mobile_no(mobile_no)
query = f"""
SELECT name, mobile_no
FROM `tab{doctype}`
FROM `tab{pagetype}`
WHERE CONCAT('+', REGEXP_REPLACE(mobile_no, '[^0-9]', '')) = {mobile_no}
"""
data = frappe.db.sql(query + where, as_dict=True)
data = jingrow.db.sql(query + where, as_dict=True)
return data[0].name if data else None
doctype = "CRM Deal"
pagetype = "CRM Deal"
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)
pg = find_record(pagetype, number) or None
if not pg:
pagetype = "CRM Lead"
pg = find_record(pagetype, number, "AND converted is not True")
if not pg:
pg = find_record(pagetype, number)
return doc, doctype
return pg, pagetype
def parse_mobile_no(mobile_no: str):
@ -97,37 +97,37 @@ def parse_mobile_no(mobile_no: str):
return "".join([c for c in mobile_no if c.isdigit() or c == "+"])
@frappe.whitelist()
@jingrow.whitelist()
def is_whatsapp_enabled():
if not frappe.db.exists("DocType", "WhatsApp Settings"):
if not jingrow.db.exists("PageType", "WhatsApp Settings"):
return False
return frappe.get_cached_value("WhatsApp Settings", "WhatsApp Settings", "enabled")
return jingrow.get_cached_value("WhatsApp Settings", "WhatsApp Settings", "enabled")
@frappe.whitelist()
@jingrow.whitelist()
def is_whatsapp_installed():
if not frappe.db.exists("DocType", "WhatsApp Settings"):
if not jingrow.db.exists("PageType", "WhatsApp Settings"):
return False
return True
@frappe.whitelist()
def get_whatsapp_messages(reference_doctype, reference_name):
@jingrow.whitelist()
def get_whatsapp_messages(reference_pagetype, reference_name):
# twilio integration app is not compatible with crm app
# crm has its own twilio integration in built
if "twilio_integration" in frappe.get_installed_apps():
if "twilio_integration" in jingrow.get_installed_apps():
return []
if not frappe.db.exists("DocType", "WhatsApp Message"):
if not jingrow.db.exists("PageType", "WhatsApp Message"):
return []
messages = []
if reference_doctype == "CRM Deal":
lead = frappe.db.get_value(reference_doctype, reference_name, "lead")
if reference_pagetype == "CRM Deal":
lead = jingrow.db.get_value(reference_pagetype, reference_name, "lead")
if lead:
messages = frappe.get_all(
messages = jingrow.get_all(
"WhatsApp Message",
filters={
"reference_doctype": "CRM Lead",
"reference_pagetype": "CRM Lead",
"reference_name": lead,
},
fields=[
@ -146,17 +146,17 @@ def get_whatsapp_messages(reference_doctype, reference_name):
"creation",
"message",
"status",
"reference_doctype",
"reference_pagetype",
"reference_name",
"template_parameters",
"template_header_parameters",
],
)
messages += frappe.get_all(
messages += jingrow.get_all(
"WhatsApp Message",
filters={
"reference_doctype": reference_doctype,
"reference_pagetype": reference_pagetype,
"reference_name": reference_name,
},
fields=[
@ -175,7 +175,7 @@ def get_whatsapp_messages(reference_doctype, reference_name):
"creation",
"message",
"status",
"reference_doctype",
"reference_pagetype",
"reference_name",
"template_parameters",
"template_header_parameters",
@ -188,7 +188,7 @@ def get_whatsapp_messages(reference_doctype, reference_name):
# Iterate through template messages
for template_message in template_messages:
# Find the template that this message is using
template = frappe.get_doc("WhatsApp Templates", template_message["template"])
template = jingrow.get_pg("WhatsApp Templates", template_message["template"])
# If the template is found, add the template details to the template message
if template:
@ -249,9 +249,9 @@ def get_whatsapp_messages(reference_doctype, reference_name):
return [message for message in messages if message["content_type"] != "reaction"]
@frappe.whitelist()
@jingrow.whitelist()
def create_whatsapp_message(
reference_doctype,
reference_pagetype,
reference_name,
message,
to,
@ -259,20 +259,20 @@ def create_whatsapp_message(
reply_to,
content_type="text",
):
doc = frappe.new_doc("WhatsApp Message")
pg = jingrow.new_pg("WhatsApp Message")
if reply_to:
reply_doc = frappe.get_doc("WhatsApp Message", reply_to)
doc.update(
reply_pg = jingrow.get_pg("WhatsApp Message", reply_to)
pg.update(
{
"is_reply": True,
"reply_to_message_id": reply_doc.message_id,
"reply_to_message_id": reply_pg.message_id,
}
)
doc.update(
pg.update(
{
"reference_doctype": reference_doctype,
"reference_pagetype": reference_pagetype,
"reference_name": reference_name,
"message": message or attach,
"to": to,
@ -280,16 +280,16 @@ def create_whatsapp_message(
"content_type": content_type,
}
)
doc.insert(ignore_permissions=True)
return doc.name
pg.insert(ignore_permissions=True)
return pg.name
@frappe.whitelist()
def send_whatsapp_template(reference_doctype, reference_name, template, to):
doc = frappe.new_doc("WhatsApp Message")
doc.update(
@jingrow.whitelist()
def send_whatsapp_template(reference_pagetype, reference_name, template, to):
pg = jingrow.new_pg("WhatsApp Message")
pg.update(
{
"reference_doctype": reference_doctype,
"reference_pagetype": reference_pagetype,
"reference_name": reference_name,
"message_type": "Template",
"message": "Template message",
@ -299,27 +299,27 @@ def send_whatsapp_template(reference_doctype, reference_name, template, to):
"to": to,
}
)
doc.insert(ignore_permissions=True)
return doc.name
pg.insert(ignore_permissions=True)
return pg.name
@frappe.whitelist()
@jingrow.whitelist()
def react_on_whatsapp_message(emoji, reply_to_name):
reply_to_doc = frappe.get_doc("WhatsApp Message", reply_to_name)
to = reply_to_doc.type == "Incoming" and reply_to_doc.get("from") or reply_to_doc.to
doc = frappe.new_doc("WhatsApp Message")
doc.update(
reply_to_pg = jingrow.get_pg("WhatsApp Message", reply_to_name)
to = reply_to_pg.type == "Incoming" and reply_to_pg.get("from") or reply_to_pg.to
pg = jingrow.new_pg("WhatsApp Message")
pg.update(
{
"reference_doctype": reply_to_doc.reference_doctype,
"reference_name": reply_to_doc.reference_name,
"reference_pagetype": reply_to_pg.reference_pagetype,
"reference_name": reply_to_pg.reference_name,
"message": emoji,
"to": to,
"reply_to_message_id": reply_to_doc.message_id,
"reply_to_message_id": reply_to_pg.message_id,
"content_type": "reaction",
}
)
doc.insert(ignore_permissions=True)
return doc.name
pg.insert(ignore_permissions=True)
return pg.name
def parse_template_parameters(string, parameters):
@ -331,16 +331,16 @@ def parse_template_parameters(string, parameters):
def get_from_name(message):
doc = frappe.get_doc(message["reference_doctype"], message["reference_name"])
pg = jingrow.get_pg(message["reference_pagetype"], message["reference_name"])
from_name = ""
if message["reference_doctype"] == "CRM Deal":
if doc.get("contacts"):
for c in doc.get("contacts"):
if message["reference_pagetype"] == "CRM Deal":
if pg.get("contacts"):
for c in pg.get("contacts"):
if c.is_primary:
from_name = c.full_name or c.mobile_no
break
else:
from_name = doc.get("lead_name")
from_name = pg.get("lead_name")
else:
from_name = " ".join(filter(None, [doc.get("first_name"), doc.get("last_name")]))
from_name = " ".join(filter(None, [pg.get("first_name"), pg.get("last_name")]))
return from_name

View File

@ -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 Call Log", {
// refresh(frm) {
// },
// });

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMCallLog(UnitTestCase):
pass

View File

@ -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 Communication Status", {
// refresh(frm) {
// },
// });

View File

@ -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 CRMCommunicationStatus(Document):
pass

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMCommunicationStatus(UnitTestCase):
pass

View File

@ -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 CRMContacts(Document):
pass

View File

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

View File

@ -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

View File

@ -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 Status", {
// refresh(frm) {
// },
// });

View File

@ -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 CRMDealStatus(Document):
pass

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMDealStatus(UnitTestCase):
pass

View File

@ -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 CRMDropdownItem(Document):
pass

View File

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

View File

@ -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 Fields Layout", {
// refresh(frm) {
// },
// });

View File

@ -1,9 +0,0 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMFieldsLayout(UnitTestCase):
pass

View File

@ -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

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMFormScript(UnitTestCase):
pass

View File

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

View File

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

View File

@ -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

View File

@ -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) {
// },
// });

View File

@ -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

View File

@ -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

View File

@ -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) {
// },
// });

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) {
// },
// });

View File

@ -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

View File

@ -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

View File

@ -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 Status", {
// refresh(frm) {
// },
// });

View File

@ -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 CRMLeadStatus(Document):
pass

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMLeadStatus(UnitTestCase):
pass

View File

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

View File

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

View File

@ -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 Notification", {
// refresh(frm) {
// },
// });

View File

@ -1,37 +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):
if self.to_user:
frappe.publish_realtime("crm_notification", user= self.to_user)
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)

View File

@ -1,9 +0,0 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMNotification(UnitTestCase):
pass

View File

@ -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) {
// },
// });

View File

@ -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

View File

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

View File

@ -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

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMServiceLevelAgreement(UnitTestCase):
pass

View File

@ -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),
}

View File

@ -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) {
// },
// });

View File

@ -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

View File

@ -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

View File

@ -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 Task", {
// refresh(frm) {
// },
// });

View File

@ -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

View File

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

View File

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

View File

@ -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) {
// },
// });

View File

@ -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

View File

@ -1,9 +0,0 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMTerritory(UnitTestCase):
pass

View File

@ -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 Twilio Settings", {
// refresh(frm) {
// },
// });

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMTwilioSettings(UnitTestCase):
pass

View File

@ -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 View Settings", {
// refresh(frm) {
// },
// });

View File

@ -1,246 +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 get_route_name(view.doctype)
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 get_route_name(view.doctype)
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 set_as_default(name=None, type=None, doctype=None):
if name:
frappe.db.set_value("CRM View Settings", name, "is_default", 1)
else:
doc = create_or_update_standard_view({"type": type, "doctype": doctype, "is_default": 1})
name = doc.name
# remove default from other views of same user
frappe.db.set_value(
"CRM View Settings",
{"name": ("!=", name), "user": frappe.session.user, "is_default": 1},
"is_default",
0,
)
@frappe.whitelist()
def create_or_update_standard_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 "[]")
view.column_field = view.column_field or "status"
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_standard": 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 get_route_name(view.doctype)
doc.load_default_columns = view.load_default_columns or False
doc.filters = json.dumps(filters)
doc.order_by = view.order_by or "modified desc"
doc.group_by_field = view.group_by_field or "owner"
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 = view.is_default or False
doc.save()
else:
doc = frappe.new_doc("CRM View Settings")
label = "List"
if view.type == "group_by":
label = "Group By"
elif view.type == "kanban":
label = "Kanban"
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 get_route_name(view.doctype)
doc.load_default_columns = view.load_default_columns or False
doc.filters = json.dumps(filters)
doc.order_by = view.order_by or "modified desc"
doc.group_by_field = view.group_by_field or "owner"
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_standard = True
doc.is_default = view.is_default or False
doc.insert()
return doc
def get_route_name(doctype):
# Example: "CRM Lead" -> "Leads"
if doctype.startswith("CRM "):
doctype = doctype[4:]
if doctype[-1] != "s":
doctype += "s"
return doctype

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestCRMViewSettings(UnitTestCase):
pass

View File

@ -1,9 +0,0 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestERPNextCRMSettings(UnitTestCase):
pass

View File

@ -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) {
// },
// });

View File

@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
class TestFCRMNote(UnitTestCase):
pass

View File

@ -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

View File

@ -0,0 +1,8 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
// jingrow.ui.form.on("CRM Call Log", {
// refresh(frm) {
// },
// });

View File

@ -5,7 +5,7 @@
"autoname": "field:id",
"creation": "2023-08-28 00:23:36.229137",
"default_view": "List",
"doctype": "DocType",
"pagetype": "PageType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@ -17,7 +17,7 @@
"duration",
"medium",
"start_time",
"reference_doctype",
"reference_pagetype",
"reference_docname",
"column_break_ufnp",
"to",
@ -105,14 +105,14 @@
"options": "FCRM Note"
},
{
"depends_on": "eval:doc.type == 'Incoming'",
"depends_on": "eval:pg.type == 'Incoming'",
"fieldname": "receiver",
"fieldtype": "Link",
"label": "Call Received By",
"options": "User"
},
{
"depends_on": "eval:doc.type == 'Outgoing'",
"depends_on": "eval:pg.type == 'Outgoing'",
"fieldname": "caller",
"fieldtype": "Link",
"label": "Caller",
@ -120,16 +120,16 @@
},
{
"default": "CRM Lead",
"fieldname": "reference_doctype",
"fieldname": "reference_pagetype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType"
"options": "PageType"
},
{
"fieldname": "reference_docname",
"fieldtype": "Dynamic Link",
"label": "Reference Name",
"options": "reference_doctype"
"options": "reference_pagetype"
},
{
"fieldname": "section_break_kebz",

View File

@ -1,14 +1,14 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# Copyright (c) 2023, JINGROW and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
import jingrow
from jingrow.model.page import Page
from crm.integrations.api import get_contact_by_phone_number
from crm.utils import seconds_to_duration
class CRMCallLog(Document):
class CRMCallLog(Page):
@staticmethod
def default_list_data():
columns = [
@ -74,7 +74,7 @@ class CRMCallLog(Document):
"to",
"note",
"recording_url",
"reference_doctype",
"reference_pagetype",
"reference_docname",
"creation",
]
@ -83,16 +83,16 @@ class CRMCallLog(Document):
def parse_list_data(calls):
return [parse_call_log(call) for call in calls] if calls else []
def has_link(self, doctype, name):
def has_link(self, pagetype, name):
for link in self.links:
if link.link_doctype == doctype and link.link_name == name:
if link.link_pagetype == pagetype and link.link_name == name:
return True
def link_with_reference_doc(self, reference_doctype, reference_name):
if self.has_link(reference_doctype, reference_name):
def link_with_reference_pg(self, reference_pagetype, reference_name):
if self.has_link(reference_pagetype, reference_name):
return
self.append("links", {"link_doctype": reference_doctype, "link_name": reference_name})
self.append("links", {"link_pagetype": reference_pagetype, "link_name": reference_name})
def parse_call_log(call):
@ -102,7 +102,7 @@ def parse_call_log(call):
call["activity_type"] = "incoming_call"
contact = get_contact_by_phone_number(call.get("from"))
receiver = (
frappe.db.get_values("User", call.get("receiver"), ["full_name", "user_image"])[0]
jingrow.db.get_values("User", call.get("receiver"), ["full_name", "user_image"])[0]
if call.get("receiver")
else [None, None]
)
@ -118,7 +118,7 @@ def parse_call_log(call):
call["activity_type"] = "outgoing_call"
contact = get_contact_by_phone_number(call.get("to"))
caller = (
frappe.db.get_values("User", call.get("caller"), ["full_name", "user_image"])[0]
jingrow.db.get_values("User", call.get("caller"), ["full_name", "user_image"])[0]
if call.get("caller")
else [None, None]
)
@ -134,9 +134,9 @@ def parse_call_log(call):
return call
@frappe.whitelist()
@jingrow.whitelist()
def get_call_log(name):
call = frappe.get_cached_doc(
call = jingrow.get_cached_pg(
"CRM Call Log",
name,
fields=[
@ -150,7 +150,7 @@ def get_call_log(name):
"to",
"note",
"recording_url",
"reference_doctype",
"reference_pagetype",
"reference_docname",
"creation",
],
@ -162,26 +162,26 @@ def get_call_log(name):
tasks = []
if call.get("note"):
note = frappe.get_cached_doc("FCRM Note", call.get("note")).as_dict()
note = jingrow.get_cached_pg("FCRM Note", call.get("note")).as_dict()
notes.append(note)
if call.get("reference_doctype") and call.get("reference_docname"):
if call.get("reference_doctype") == "CRM Lead":
if call.get("reference_pagetype") and call.get("reference_docname"):
if call.get("reference_pagetype") == "CRM Lead":
call["_lead"] = call.get("reference_docname")
elif call.get("reference_doctype") == "CRM Deal":
elif call.get("reference_pagetype") == "CRM Deal":
call["_deal"] = call.get("reference_docname")
if call.get("links"):
for link in call.get("links"):
if link.get("link_doctype") == "CRM Task":
task = frappe.get_cached_doc("CRM Task", link.get("link_name")).as_dict()
if link.get("link_pagetype") == "CRM Task":
task = jingrow.get_cached_pg("CRM Task", link.get("link_name")).as_dict()
tasks.append(task)
elif link.get("link_doctype") == "FCRM Note":
note = frappe.get_cached_doc("FCRM Note", link.get("link_name")).as_dict()
elif link.get("link_pagetype") == "FCRM Note":
note = jingrow.get_cached_pg("FCRM Note", link.get("link_name")).as_dict()
notes.append(note)
elif link.get("link_doctype") == "CRM Lead":
elif link.get("link_pagetype") == "CRM Lead":
call["_lead"] = link.get("link_name")
elif link.get("link_doctype") == "CRM Deal":
elif link.get("link_pagetype") == "CRM Deal":
call["_deal"] = link.get("link_name")
call["_tasks"] = tasks
@ -189,13 +189,13 @@ def get_call_log(name):
return call
@frappe.whitelist()
@jingrow.whitelist()
def create_lead_from_call_log(call_log, lead_details=None):
lead = frappe.new_doc("CRM Lead")
lead_details = frappe.parse_json(lead_details or "{}")
lead = jingrow.new_pg("CRM Lead")
lead_details = jingrow.parse_json(lead_details or "{}")
if not lead_details.get("lead_owner"):
lead_details["lead_owner"] = frappe.session.user
lead_details["lead_owner"] = jingrow.session.user
if not lead_details.get("mobile_no"):
lead_details["mobile_no"] = call_log.get("from") or ""
if not lead_details.get("first_name"):
@ -207,8 +207,8 @@ def create_lead_from_call_log(call_log, lead_details=None):
lead.save(ignore_permissions=True)
# link call log with lead
call_log = frappe.get_doc("CRM Call Log", call_log.get("name"))
call_log.link_with_reference_doc("CRM Lead", lead.name)
call_log = jingrow.get_pg("CRM Call Log", call_log.get("name"))
call_log.link_with_reference_pg("CRM Lead", lead.name)
call_log.save(ignore_permissions=True)
return lead.name

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMCallLog(UnitTestCase):
pass

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