1
0
forked from test/crm

frappecloud.com - jcloud.jingrow.com

This commit is contained in:
jingrow 2025-10-23 18:07:03 +08:00
parent 7d5b6efb17
commit e6400c939b
401 changed files with 35135 additions and 35135 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 https://github.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

@ -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_NAME: "Jingrow PR Bot"
GIT_AUTHOR_EMAIL: "developers@jingrow.com"
GIT_COMMITTER_NAME: "Frappe PR Bot"
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 = https://github.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": [
{

View File

@ -1,33 +1,33 @@
<div align="center" markdown="1">
<a href="https://framework.jingrow.com/products/crm">
<img src=".github/logo.svg" height="80" alt="Frappe CRM Logo">
<img src=".github/logo.svg" height="80" alt="Jingrow CRM Logo">
</a>
<h1>Frappe CRM</h1>
<h1>Jingrow CRM</h1>
**Simplify Sales, Amplify Relationships**
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/frappe/crm)](https://github.com/frappe/crm/releases)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/jingrow/crm)](https://github.com/jingrow/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">
<img width="1402" alt="Jingrow 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://framework.jingrow.com/crm) - [Documentation](https://docs.jingrow.com/crm)
[Live Demo](https://frappecrm-demo.jingrow.cloud/api/method/crm.api.demo.login) - [Website](https://framework.jingrow.com/crm) - [Documentation](https://docs.jingrow.com/crm)
</div>
## Frappe CRM
## Jingrow 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.
Jingrow CRM is a simple, affordable, open-source CRM tool designed for modern sales teams with unlimited users. Jingrow 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.
The motivation behind building Jingrow 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. Jingrow 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 Jingrow 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
@ -76,18 +76,18 @@ The motivation behind building Frappe CRM stems from the need for a simple, cust
- **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.
- **WhatsApp:** Integrate WhatsApp to send and receive messages from the CRM. [Jingrow WhatsApp](https://github.com/shridarpatil/jingrow_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.
- [Jingrow Framework](https://github.com/jingrow/jingrow): A full-stack web application framework.
- [Jingrow UI](https://github.com/jingrow/jingrow-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:
This app is compatible with the following versions of Jingrow and ERPNext:
| CRM branch | Stability | Frappe branch | ERPNext branch |
| CRM branch | Stability | Jingrow branch | ERPNext branch |
| :-------------------- | :-------- | :------------------- | :------------------- |
| main - v1.x | stable | v15.x | v15.x |
| develop - future/v2.x | unstable | develop - future/v16 | develop - future/v16 |
@ -96,19 +96,19 @@ This app is compatible with the following versions of Frappe and ERPNext:
### Managed Hosting
Get started with your personal or business site with a few clicks on Frappe Cloud - our official hosting service.
Get started with your personal or business site with a few clicks on Jingrow Cloud - our official hosting service.
<div>
<a href="https://frappecloud.com/crm/signup" target="_blank">
<a href="https://jcloud.jingrow.com/crm/signup" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://framework.jingrow.com/files/try-on-fc-white.png">
<img src="https://framework.jingrow.com/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" />
<img src="https://framework.jingrow.com/files/try-on-fc-black.png" alt="Try on Jingrow Cloud" height="28" />
</picture>
</a>
</div>
### Self Hosting
Follow these steps to set up Frappe CRM in production:
Follow these steps to set up Jingrow CRM in production:
**Step 1**: Download the easy install script
@ -122,7 +122,7 @@ wget https://framework.jingrow.com/easy-install.py
python3 ./easy-install.py deploy \
--project=crm_prod_setup \
--email=email.example.com \
--image=ghcr.io/frappe/crm \
--image=ghcr.io/jingrow/crm \
--version=stable \
--app=crm \
--sitename subdomain.domain.tld
@ -133,15 +133,15 @@ 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.
The script will set up a production-ready instance of Jingrow CRM with all the necessary configurations in about 5 minutes.
## Getting Started (Development)
### Local Setup
1. [Setup Bench](https://docs.jingrow.com/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:
1. In the jingrow-bench directory, run `bench start` and keep it running.
1. Open a new terminal session and cd into `jingrow-bench` directory and run following commands:
```sh
$ bench get-app crm
$ bench new-site sitename.localhost --install-app crm
@ -150,14 +150,14 @@ The script will set up a production-ready instance of Frappe CRM with all the ne
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:
1. Open a new terminal session and cd into `jingrow-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`
**Note:** You'll find all the code related to Jingrow CRM's frontend inside `jingrow-bench/apps/crm/frontend`
### Docker
@ -165,14 +165,14 @@ You need Docker, docker-compose and git setup on your machine. Refer [Docker doc
**Step 1**: Setup folder and download the required files
mkdir frappe-crm
cd frappe-crm
mkdir jingrow-crm
cd jingrow-crm
# Download the docker-compose file
wget -O docker-compose.yml https://raw.githubusercontent.com/frappe/crm/develop/docker/docker-compose.yml
wget -O docker-compose.yml https://raw.githubusercontent.com/jingrow/crm/develop/docker/docker-compose.yml
# Download the setup script
wget -O init.sh https://raw.githubusercontent.com/frappe/crm/develop/docker/init.sh
wget -O init.sh https://raw.githubusercontent.com/jingrow/crm/develop/docker/init.sh
**Step 2**: Run the container and daemonize it
@ -186,7 +186,7 @@ You need Docker, docker-compose and git setup on your machine. Refer [Docker doc
## Learn and connect
- [Telegram Public Group](https://t.me/frappecrm)
- [Discuss Forum](https://discuss.jingrow.com/c/frappe-crm)
- [Discuss Forum](https://discuss.jingrow.com/c/jingrow-crm)
- [Documentation](https://docs.jingrow.com/crm)
- [YouTube](https://www.youtube.com/channel/UCn3bV5kx77HsVwtnlCeEi_A)
- [X/Twitter](https://x.com/frappetech)
@ -196,8 +196,8 @@ You need Docker, docker-compose and git setup on your machine. Refer [Docker doc
<div align="center" style="padding-top: 0.75rem;">
<a href="https://framework.jingrow.com" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://framework.jingrow.com/files/Frappe-white.png">
<img src="https://framework.jingrow.com/files/Frappe-black.png" alt="JINGROW" height="28"/>
<source media="(prefers-color-scheme: dark)" srcset="https://framework.jingrow.com/files/Jingrow-white.png">
<img src="https://framework.jingrow.com/files/Jingrow-black.png" alt="JINGROW" height="28"/>
</picture>
</a>
</div>

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,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.document 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,7 +64,7 @@ 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()
@ -72,18 +72,18 @@ def get_filterable_fields(doctype: str):
res = []
# append DocFields
DocField = frappe.qb.DocType("DocField")
doc_fields = get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields)
res.extend(doc_fields)
DocField = jingrow.qb.PageType("DocField")
pg_fields = get_pagetype_fields_meta(DocField, 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,10 +161,10 @@ def get_group_by_fields(doctype: str):
return fields
def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields):
def get_pagetype_fields_meta(DocField, pagetype, allowed_fieldtypes, restricted_fields):
parent = "parent" if DocField._table_name == "tabDocField" else "dt"
return (
frappe.qb.from_(DocField)
jingrow.qb.from_(DocField)
.select(
DocField.fieldname,
DocField.fieldtype,
@ -172,7 +172,7 @@ def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fi
DocField.name,
DocField.options,
)
.where(DocField[parent] == doctype)
.where(DocField[parent] == pagetype)
.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]))
@ -180,13 +180,13 @@ def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fi
)
@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,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,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,7 +1,7 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Call Log", {
// 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,8 +1,8 @@
# 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.document import Document
from crm.integrations.api import get_contact_by_phone_number
from crm.utils import seconds_to_duration
@ -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

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

View File

@ -1,7 +1,7 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Communication Status", {
// jingrow.ui.form.on("CRM Communication Status", {
// refresh(frm) {
// },

View File

@ -3,7 +3,7 @@
"allow_rename": 1,
"autoname": "field:status",
"creation": "2023-12-13 13:25:07.213100",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"status"

View File

@ -1,8 +1,8 @@
# 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.document import Document
class CRMCommunicationStatus(Document):

View File

@ -1,8 +1,8 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMCommunicationStatus(UnitTestCase):

View File

@ -2,7 +2,7 @@
"actions": [],
"allow_rename": 1,
"creation": "2023-08-25 19:08:36.356133",
"doctype": "DocType",
"pagetype": "PageType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [

View File

@ -1,8 +1,8 @@
# 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.document import Document
class CRMContacts(Document):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2025, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Dashboard", {
// jingrow.ui.form.on("CRM Dashboard", {
// refresh(frm) {
// },

View File

@ -3,7 +3,7 @@
"allow_rename": 1,
"autoname": "field:title",
"creation": "2025-07-14 12:19:49.725022",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"title",

View File

@ -1,9 +1,9 @@
# Copyright (c) 2025, JINGROW and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
import jingrow
from jingrow import _
from jingrow.model.document import Document
class CRMDashboard(Document):
@ -21,14 +21,14 @@ def create_default_manager_dashboard(force=False):
"""
Creates the default CRM Manager Dashboard if it does not exist.
"""
if not frappe.db.exists("CRM Dashboard", "Manager Dashboard"):
doc = frappe.new_doc("CRM Dashboard")
doc.title = "Manager Dashboard"
doc.layout = default_manager_dashboard_layout()
doc.insert(ignore_permissions=True)
if not jingrow.db.exists("CRM Dashboard", "Manager Dashboard"):
pg = jingrow.new_pg("CRM Dashboard")
pg.title = "Manager Dashboard"
pg.layout = default_manager_dashboard_layout()
pg.insert(ignore_permissions=True)
else:
doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard")
pg = jingrow.get_pg("CRM Dashboard", "Manager Dashboard")
if force:
doc.layout = default_manager_dashboard_layout()
doc.save(ignore_permissions=True)
return doc.layout
pg.layout = default_manager_dashboard_layout()
pg.save(ignore_permissions=True)
return pg.layout

View File

@ -1,11 +1,11 @@
# Copyright (c) 2025, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# import jingrow
from jingrow.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# On IntegrationTestCase, the pagetype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]

View File

@ -1,9 +1,9 @@
import frappe
import jingrow
@frappe.whitelist()
@jingrow.whitelist()
def get_deal_contacts(name):
contacts = frappe.get_all(
contacts = jingrow.get_all(
"CRM Contacts",
filters={"parenttype": "CRM Deal", "parent": name},
fields=["contact", "is_primary"],
@ -15,7 +15,7 @@ def get_deal_contacts(name):
continue
is_primary = contact.is_primary
contact = frappe.get_doc("Contact", contact.contact).as_dict()
contact = jingrow.get_pg("Contact", contact.contact).as_dict()
_contact = {
"name": contact.name,

View File

@ -1,23 +1,23 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
frappe.ui.form.on("CRM Deal", {
jingrow.ui.form.on("CRM Deal", {
refresh(frm) {
frm.add_web_link(`/crm/deals/${frm.doc.name}`, __("Open in Portal"));
frm.add_web_link(`/crm/deals/${frm.pg.name}`, __("Open in Portal"));
},
update_total: function (frm) {
let total = 0;
let total_qty = 0;
let net_total = 0;
frm.doc.products.forEach((d) => {
frm.pg.products.forEach((d) => {
total += d.amount;
total_qty += d.qty;
net_total += d.net_amount;
});
frappe.model.set_value(frm.doctype, frm.docname, "total", total);
frappe.model.set_value(
frm.doctype,
jingrow.model.set_value(frm.pagetype, frm.docname, "total", total);
jingrow.model.set_value(
frm.pagetype,
frm.docname,
"net_total",
net_total || total
@ -25,7 +25,7 @@ frappe.ui.form.on("CRM Deal", {
}
});
frappe.ui.form.on("CRM Products", {
jingrow.ui.form.on("CRM Products", {
products_add: function (frm, cdt, cdn) {
frm.trigger("update_total");
},
@ -33,34 +33,34 @@ frappe.ui.form.on("CRM Products", {
frm.trigger("update_total");
},
product_code: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, "product_name", d.product_code);
let d = jingrow.get_pg(cdt, cdn);
jingrow.model.set_value(cdt, cdn, "product_name", d.product_code);
},
rate: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
let d = jingrow.get_pg(cdt, cdn);
if (d.rate && d.qty) {
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
jingrow.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
}
frm.trigger("update_total");
},
qty: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
let d = jingrow.get_pg(cdt, cdn);
if (d.rate && d.qty) {
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
jingrow.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
}
frm.trigger("update_total");
},
discount_percentage: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
let d = jingrow.get_pg(cdt, cdn);
if (d.discount_percentage && d.amount) {
discount_amount = (d.discount_percentage / 100) * d.amount;
frappe.model.set_value(
jingrow.model.set_value(
cdt,
cdn,
"discount_amount",
discount_amount
);
frappe.model.set_value(
jingrow.model.set_value(
cdt,
cdn,
"net_amount",

View File

@ -4,7 +4,7 @@
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2023-11-06 17:56:25.210449",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"organization_tab",
@ -396,14 +396,14 @@
"fieldname": "lost_reason",
"fieldtype": "Link",
"label": "Lost Reason",
"mandatory_depends_on": "eval: doc.status == \"Lost\"",
"mandatory_depends_on": "eval: pg.status == \"Lost\"",
"options": "CRM Lost Reason"
},
{
"fieldname": "lost_notes",
"fieldtype": "Text",
"label": "Lost Notes",
"mandatory_depends_on": "eval: doc.lost_reason == \"Other\""
"mandatory_depends_on": "eval: pg.lost_reason == \"Other\""
},
{
"default": "1",

View File

@ -1,14 +1,14 @@
# Copyright (c) 2023, JINGROW and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.desk.form.assign_to import add as assign
from frappe.model.document import Document
import jingrow
from jingrow import _
from jingrow.desk.form.assign_to import add as assign
from jingrow.model.document import Document
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log
from crm.fcrm.doctype.fcrm_settings.fcrm_settings import get_exchange_rate
from crm.fcrm.pagetype.crm_service_level_agreement.utils import get_sla
from crm.fcrm.pagetype.crm_status_change_log.crm_status_change_log import add_status_change_log
from crm.fcrm.pagetype.fcrm_settings.fcrm_settings import get_exchange_rate
class CRMDeal(Document):
@ -23,8 +23,8 @@ class CRMDeal(Document):
self.assign_agent(self.deal_owner)
if self.has_value_changed("status"):
add_status_change_log(self)
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
self.closed_date = frappe.utils.nowdate()
if jingrow.db.get_value("CRM Deal Status", self.status, "type") == "Won":
self.closed_date = jingrow.utils.nowdate()
self.validate_forecasting_fields()
self.validate_lost_reason()
self.update_exchange_rate()
@ -57,7 +57,7 @@ class CRMDeal(Document):
return
if len([contact for contact in self.contacts if contact.is_primary]) > 1:
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold("Contact")))
jingrow.throw(_("Only one {0} can be set as primary.").format(jingrow.bold("Contact")))
primary_contact_exists = False
for d in self.contacts:
@ -84,34 +84,34 @@ class CRMDeal(Document):
# the agent is already set as an assignee
return
assign({"assign_to": [agent], "doctype": "CRM Deal", "name": self.name}, ignore_permissions=True)
assign({"assign_to": [agent], "pagetype": "CRM Deal", "name": self.name}, ignore_permissions=True)
def share_with_agent(self, agent):
if not agent:
return
docshares = frappe.get_all(
docshares = jingrow.get_all(
"DocShare",
filters={"share_name": self.name, "share_doctype": self.doctype},
filters={"share_name": self.name, "share_pagetype": self.pagetype},
fields=["name", "user"],
)
shared_with = [d.user for d in docshares] + [agent]
for user in shared_with:
if user == agent and not frappe.db.exists(
if user == agent and not jingrow.db.exists(
"DocShare",
{"user": agent, "share_name": self.name, "share_doctype": self.doctype},
{"user": agent, "share_name": self.name, "share_pagetype": self.pagetype},
):
frappe.share.add_docshare(
self.doctype,
jingrow.share.add_docshare(
self.pagetype,
self.name,
agent,
write=1,
flags={"ignore_share_permission": True},
)
elif user != agent:
frappe.share.remove(self.doctype, self.name, user)
jingrow.share.remove(self.pagetype, self.name, user)
def set_sla(self):
"""
@ -133,7 +133,7 @@ class CRMDeal(Document):
"""
if not self.sla:
return
sla = frappe.get_last_doc("CRM Service Level Agreement", {"name": self.sla})
sla = jingrow.get_last_pg("CRM Service Level Agreement", {"name": self.sla})
if sla:
sla.apply(self)
@ -142,21 +142,21 @@ class CRMDeal(Document):
Update the closed date based on the "Won" status.
"""
if self.status == "Won" and not self.closed_date:
self.closed_date = frappe.utils.nowdate()
self.closed_date = jingrow.utils.nowdate()
def update_default_probability(self):
"""
Update the default probability based on the status.
"""
if not self.probability or self.probability == 0:
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
self.probability = jingrow.db.get_value("CRM Deal Status", self.status, "probability") or 0
def update_expected_deal_value(self):
"""
Update the expected deal value based on the net total or total.
"""
if (
frappe.db.get_single_value("FCRM Settings", "auto_update_expected_deal_value")
jingrow.db.get_single_value("FCRM Settings", "auto_update_expected_deal_value")
and (self.net_total or self.total)
and self.expected_deal_value
):
@ -166,25 +166,25 @@ class CRMDeal(Document):
self.update_closed_date()
self.update_default_probability()
self.update_expected_deal_value()
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
if jingrow.db.get_single_value("FCRM Settings", "enable_forecasting"):
if not self.expected_deal_value or self.expected_deal_value == 0:
frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError)
jingrow.throw(_("Expected Deal Value is required."), jingrow.MandatoryError)
if not self.expected_closure_date:
frappe.throw(_("Expected Closure Date is required."), frappe.MandatoryError)
jingrow.throw(_("Expected Closure Date is required."), jingrow.MandatoryError)
def validate_lost_reason(self):
"""
Validate the lost reason if the status is set to "Lost".
"""
if self.status and frappe.get_cached_value("CRM Deal Status", self.status, "type") == "Lost":
if self.status and jingrow.get_cached_value("CRM Deal Status", self.status, "type") == "Lost":
if not self.lost_reason:
frappe.throw(_("Please specify a reason for losing the deal."), frappe.ValidationError)
jingrow.throw(_("Please specify a reason for losing the deal."), jingrow.ValidationError)
elif self.lost_reason == "Other" and not self.lost_notes:
frappe.throw(_("Please specify the reason for losing the deal."), frappe.ValidationError)
jingrow.throw(_("Please specify the reason for losing the deal."), jingrow.ValidationError)
def update_exchange_rate(self):
if self.has_value_changed("currency") or not self.exchange_rate:
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
system_currency = jingrow.db.get_single_value("FCRM Settings", "currency") or "USD"
exchange_rate = 1
if self.currency and self.currency != system_currency:
exchange_rate = get_exchange_rate(self.currency, system_currency)
@ -266,96 +266,96 @@ class CRMDeal(Document):
}
@frappe.whitelist()
@jingrow.whitelist()
def add_contact(deal, contact):
if not frappe.has_permission("CRM Deal", "write", deal):
frappe.throw(_("Not allowed to add contact to Deal"), frappe.PermissionError)
if not jingrow.has_permission("CRM Deal", "write", deal):
jingrow.throw(_("Not allowed to add contact to Deal"), jingrow.PermissionError)
deal = frappe.get_cached_doc("CRM Deal", deal)
deal = jingrow.get_cached_pg("CRM Deal", deal)
deal.append("contacts", {"contact": contact})
deal.save()
return True
@frappe.whitelist()
@jingrow.whitelist()
def remove_contact(deal, contact):
if not frappe.has_permission("CRM Deal", "write", deal):
frappe.throw(_("Not allowed to remove contact from Deal"), frappe.PermissionError)
if not jingrow.has_permission("CRM Deal", "write", deal):
jingrow.throw(_("Not allowed to remove contact from Deal"), jingrow.PermissionError)
deal = frappe.get_cached_doc("CRM Deal", deal)
deal = jingrow.get_cached_pg("CRM Deal", deal)
deal.contacts = [d for d in deal.contacts if d.contact != contact]
deal.save()
return True
@frappe.whitelist()
@jingrow.whitelist()
def set_primary_contact(deal, contact):
if not frappe.has_permission("CRM Deal", "write", deal):
frappe.throw(_("Not allowed to set primary contact for Deal"), frappe.PermissionError)
if not jingrow.has_permission("CRM Deal", "write", deal):
jingrow.throw(_("Not allowed to set primary contact for Deal"), jingrow.PermissionError)
deal = frappe.get_cached_doc("CRM Deal", deal)
deal = jingrow.get_cached_pg("CRM Deal", deal)
deal.set_primary_contact(contact)
deal.save()
return True
def create_organization(doc):
if not doc.get("organization_name"):
def create_organization(pg):
if not pg.get("organization_name"):
return
existing_organization = frappe.db.exists(
"CRM Organization", {"organization_name": doc.get("organization_name")}
existing_organization = jingrow.db.exists(
"CRM Organization", {"organization_name": pg.get("organization_name")}
)
if existing_organization:
return existing_organization
organization = frappe.new_doc("CRM Organization")
organization = jingrow.new_pg("CRM Organization")
organization.update(
{
"organization_name": doc.get("organization_name"),
"website": doc.get("website"),
"territory": doc.get("territory"),
"industry": doc.get("industry"),
"annual_revenue": doc.get("annual_revenue"),
"organization_name": pg.get("organization_name"),
"website": pg.get("website"),
"territory": pg.get("territory"),
"industry": pg.get("industry"),
"annual_revenue": pg.get("annual_revenue"),
}
)
organization.insert(ignore_permissions=True)
return organization.name
def contact_exists(doc):
email_exist = frappe.db.exists("Contact Email", {"email_id": doc.get("email")})
mobile_exist = frappe.db.exists("Contact Phone", {"phone": doc.get("mobile_no")})
def contact_exists(pg):
email_exist = jingrow.db.exists("Contact Email", {"email_id": pg.get("email")})
mobile_exist = jingrow.db.exists("Contact Phone", {"phone": pg.get("mobile_no")})
doctype = "Contact Email" if email_exist else "Contact Phone"
pagetype = "Contact Email" if email_exist else "Contact Phone"
name = email_exist or mobile_exist
if name:
return frappe.db.get_value(doctype, name, "parent")
return jingrow.db.get_value(pagetype, name, "parent")
return False
def create_contact(doc):
existing_contact = contact_exists(doc)
def create_contact(pg):
existing_contact = contact_exists(pg)
if existing_contact:
return existing_contact
contact = frappe.new_doc("Contact")
contact = jingrow.new_pg("Contact")
contact.update(
{
"first_name": doc.get("first_name"),
"last_name": doc.get("last_name"),
"salutation": doc.get("salutation"),
"company_name": doc.get("organization") or doc.get("organization_name"),
"first_name": pg.get("first_name"),
"last_name": pg.get("last_name"),
"salutation": pg.get("salutation"),
"company_name": pg.get("organization") or pg.get("organization_name"),
}
)
if doc.get("email"):
contact.append("email_ids", {"email_id": doc.get("email"), "is_primary": 1})
if pg.get("email"):
contact.append("email_ids", {"email_id": pg.get("email"), "is_primary": 1})
if doc.get("mobile_no"):
contact.append("phone_nos", {"phone": doc.get("mobile_no"), "is_primary_mobile_no": 1})
if pg.get("mobile_no"):
contact.append("phone_nos", {"phone": pg.get("mobile_no"), "is_primary_mobile_no": 1})
contact.insert(ignore_permissions=True)
contact.reload() # load changes by hooks on contact
@ -363,9 +363,9 @@ def create_contact(doc):
return contact.name
@frappe.whitelist()
@jingrow.whitelist()
def create_deal(args):
deal = frappe.new_doc("CRM Deal")
deal = jingrow.new_pg("CRM Deal")
contact = args.get("contact")
if not contact and (

View File

@ -1,8 +1,8 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMDeal(UnitTestCase):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Deal Status", {
// jingrow.ui.form.on("CRM Deal Status", {
// refresh(frm) {
// },

View File

@ -3,7 +3,7 @@
"allow_rename": 1,
"autoname": "field:deal_status",
"creation": "2023-11-29 11:24:55.543387",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"deal_status",

View File

@ -1,8 +1,8 @@
# 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.document import Document
class CRMDealStatus(Document):

View File

@ -1,8 +1,8 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMDealStatus(UnitTestCase):

View File

@ -2,7 +2,7 @@
"actions": [],
"allow_rename": 1,
"creation": "2024-12-27 17:42:33.089685",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"name1",
@ -21,7 +21,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"mandatory_depends_on": "eval:doc.type == 'Route'"
"mandatory_depends_on": "eval:pg.type == 'Route'"
},
{
"fieldname": "type",
@ -29,15 +29,15 @@
"in_list_view": 1,
"label": "Type",
"options": "Route\nSeparator",
"read_only_depends_on": "eval:doc.is_standard"
"read_only_depends_on": "eval:pg.is_standard"
},
{
"depends_on": "eval:doc.type == 'Route'",
"depends_on": "eval:pg.type == 'Route'",
"fieldname": "route",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Route",
"mandatory_depends_on": "eval:doc.type == 'Route'"
"mandatory_depends_on": "eval:pg.type == 'Route'"
},
{
"default": "0",
@ -65,13 +65,13 @@
},
{
"default": "1",
"depends_on": "eval:doc.type == 'Route'",
"depends_on": "eval:pg.type == 'Route'",
"fieldname": "open_in_new_window",
"fieldtype": "Check",
"label": "Open in new window"
},
{
"depends_on": "eval:doc.is_standard",
"depends_on": "eval:pg.is_standard",
"fieldname": "name1",
"fieldtype": "Data",
"label": "Name",

View File

@ -1,8 +1,8 @@
# Copyright (c) 2024, JINGROW and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
# import jingrow
from jingrow.model.document import Document
class CRMDropdownItem(Document):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2025, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Exotel Settings", {
// jingrow.ui.form.on("CRM Exotel Settings", {
// refresh(frm) {
// },

View File

@ -2,7 +2,7 @@
"actions": [],
"allow_rename": 1,
"creation": "2025-01-08 15:55:50.710356",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"enabled",

View File

@ -1,10 +1,10 @@
# Copyright (c) 2025, JINGROW and contributors
# For license information, please see license.txt
import frappe
import jingrow
import requests
from frappe import _
from frappe.model.document import Document
from jingrow import _
from jingrow.model.document import Document
class CRMExotelSettings(Document):
@ -20,7 +20,7 @@ class CRMExotelSettings(Document):
auth=(self.api_key, self.get_password("api_token")),
)
if response.status_code != 200:
frappe.throw(
jingrow.throw(
_(f"Please enter valid exotel Account SID, API key & API token: {response.reason}"),
title=_("Invalid credentials"),
)

View File

@ -1,10 +1,10 @@
# Copyright (c) 2025, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# import jingrow
from jingrow.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# On IntegrationTestCase, the pagetype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]

View File

@ -1,7 +1,7 @@
// Copyright (c) 2024, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Fields Layout", {
// jingrow.ui.form.on("CRM Fields Layout", {
// refresh(frm) {
// },

View File

@ -3,7 +3,7 @@
"allow_rename": 1,
"autoname": "format:{dt}-{type}",
"creation": "2024-06-07 16:42:05.495324",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"dt",
@ -19,7 +19,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Document Type",
"options": "DocType"
"options": "PageType"
},
{
"fieldname": "type",

View File

@ -3,29 +3,29 @@
import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import random_string
import jingrow
from jingrow import _
from jingrow.model.document import Document
from jingrow.utils import random_string
class CRMFieldsLayout(Document):
pass
@frappe.whitelist()
def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None):
@jingrow.whitelist()
def get_fields_layout(pagetype: str, type: str, parent_pagetype: str | None = None):
tabs = []
layout = None
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type})
if jingrow.db.exists("CRM Fields Layout", {"dt": pagetype, "type": type}):
layout = jingrow.get_pg("CRM Fields Layout", {"dt": pagetype, "type": type})
if layout and layout.layout:
tabs = json.loads(layout.layout)
if not tabs and type != "Required Fields":
tabs = get_default_layout(doctype)
tabs = get_default_layout(pagetype)
has_tabs = False
if isinstance(tabs, list) and len(tabs) > 0 and isinstance(tabs[0], dict):
@ -44,14 +44,14 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
continue
allowed_fields.extend(column.get("fields"))
fields = frappe.get_meta(doctype).fields
fields = jingrow.get_meta(pagetype).fields
fields = [field for field in fields if field.fieldname in allowed_fields]
required_fields = []
if type == "Required Fields":
required_fields = [
field for field in frappe.get_meta(doctype, False).fields if field.reqd and not field.default
field for field in jingrow.get_meta(pagetype, False).fields if field.reqd and not field.default
]
for tab in tabs:
@ -64,7 +64,7 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
field = next((f for f in fields if f.fieldname == field), None)
if field:
field = field.as_dict()
handle_perm_level_restrictions(field, doctype, parent_doctype)
handle_perm_level_restrictions(field, pagetype, parent_pagetype)
column["fields"][column.get("fields").index(field["fieldname"])] = field
# remove field from required_fields if it is already present
@ -96,11 +96,11 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
return tabs or []
@frappe.whitelist()
def get_sidepanel_sections(doctype):
if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}):
@jingrow.whitelist()
def get_sidepanel_sections(pagetype):
if not jingrow.db.exists("CRM Fields Layout", {"dt": pagetype, "type": "Side Panel"}):
return []
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}).layout
layout = jingrow.get_pg("CRM Fields Layout", {"dt": pagetype, "type": "Side Panel"}).layout
if not layout:
return []
@ -113,10 +113,10 @@ def get_sidepanel_sections(doctype):
"Column Break",
]
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]
add_forecasting_section(layout, doctype)
add_forecasting_section(layout, pagetype)
for section in layout:
section["name"] = section.get("name") or section.get("label")
@ -125,7 +125,7 @@ def get_sidepanel_sections(doctype):
field_obj = next((f for f in fields if f.fieldname == field), None)
if field_obj:
field_obj = field_obj.as_dict()
handle_perm_level_restrictions(field_obj, doctype)
handle_perm_level_restrictions(field_obj, pagetype)
column["fields"][column.get("fields").index(field)] = get_field_obj(field_obj)
fields_meta = {}
@ -135,10 +135,10 @@ def get_sidepanel_sections(doctype):
return layout
def add_forecasting_section(layout, doctype):
def add_forecasting_section(layout, pagetype):
if (
doctype == "CRM Deal"
and frappe.db.get_single_value("FCRM Settings", "enable_forecasting")
pagetype == "CRM Deal"
and jingrow.db.get_single_value("FCRM Settings", "enable_forecasting")
and not any(section.get("name") == "forecasted_sales_section" for section in layout)
):
contacts_section_index = next(
@ -167,11 +167,11 @@ def add_forecasting_section(layout, doctype):
)
def handle_perm_level_restrictions(field, doctype, parent_doctype=None):
def handle_perm_level_restrictions(field, pagetype, parent_pagetype=None):
if field.permlevel == 0:
return
field_has_write_access = field.permlevel in get_permlevel_access("write", doctype, parent_doctype)
field_has_read_access = field.permlevel in get_permlevel_access("read", doctype, parent_doctype)
field_has_write_access = field.permlevel in get_permlevel_access("write", pagetype, parent_pagetype)
field_has_read_access = field.permlevel in get_permlevel_access("read", pagetype, parent_pagetype)
if not field_has_write_access and field_has_read_access:
field.read_only = 1
@ -179,15 +179,15 @@ def handle_perm_level_restrictions(field, doctype, parent_doctype=None):
field.hidden = 1
def get_permlevel_access(permission_type="write", doctype=None, parent_doctype=None):
def get_permlevel_access(permission_type="write", pagetype=None, parent_pagetype=None):
allowed_permlevels = []
roles = frappe.get_roles()
roles = jingrow.get_roles()
meta = frappe.get_meta(doctype)
meta = jingrow.get_meta(pagetype)
if meta.istable and parent_doctype:
meta = frappe.get_meta(parent_doctype)
elif meta.istable and not parent_doctype:
if meta.istable and parent_pagetype:
meta = jingrow.get_meta(parent_pagetype)
elif meta.istable and not parent_pagetype:
return [1, 0]
for perm in meta.permissions:
@ -212,27 +212,27 @@ def get_field_obj(field):
return field
@frappe.whitelist()
def save_fields_layout(doctype: str, type: str, layout: str):
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):
doc = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type})
@jingrow.whitelist()
def save_fields_layout(pagetype: str, type: str, layout: str):
if jingrow.db.exists("CRM Fields Layout", {"dt": pagetype, "type": type}):
pg = jingrow.get_pg("CRM Fields Layout", {"dt": pagetype, "type": type})
else:
doc = frappe.new_doc("CRM Fields Layout")
pg = jingrow.new_pg("CRM Fields Layout")
doc.update(
pg.update(
{
"dt": doctype,
"dt": pagetype,
"type": type,
"layout": layout,
}
)
doc.save(ignore_permissions=True)
pg.save(ignore_permissions=True)
return doc.layout
return pg.layout
def get_default_layout(doctype: str):
fields = frappe.get_meta(doctype).fields
def get_default_layout(pagetype: str):
fields = jingrow.get_meta(pagetype).fields
tabs = []

View File

@ -1,8 +1,8 @@
# Copyright (c) 2024, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMFieldsLayout(UnitTestCase):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
frappe.ui.form.on("CRM Form Script", {
jingrow.ui.form.on("CRM Form Script", {
refresh(frm) {
frm.set_query("dt", {
filters: {
@ -9,16 +9,16 @@ frappe.ui.form.on("CRM Form Script", {
},
});
if (frm.doc.is_standard && !frappe.boot.developer_mode) {
if (frm.pg.is_standard && !jingrow.boot.developer_mode) {
frm.disable_form();
frappe.show_alert(
jingrow.show_alert(
__(
"Standard Form Scripts can not be modified, duplicate the Form Script instead."
)
);
}
if (!frappe.boot.developer_mode) {
if (!jingrow.boot.developer_mode) {
frm.toggle_enable("is_standard", 0);
}
@ -27,33 +27,33 @@ frappe.ui.form.on("CRM Form Script", {
add_enable_button(frm) {
frm.add_custom_button(
frm.doc.enabled ? __("Disable") : __("Enable"),
frm.pg.enabled ? __("Disable") : __("Enable"),
() => {
frm.set_value("enabled", !frm.doc.enabled);
frm.set_value("enabled", !frm.pg.enabled);
frm.save();
}
);
},
view(frm) {
let has_form_boilerplate = frm.doc.script.includes(
let has_form_boilerplate = frm.pg.script.includes(
"function setupForm("
);
let has_list_boilerplate = frm.doc.script.includes(
let has_list_boilerplate = frm.pg.script.includes(
"function setupList("
);
if (frm.doc.view == "Form" && !has_form_boilerplate) {
frm.doc.script = `
function setupForm({ doc }) {
if (frm.pg.view == "Form" && !has_form_boilerplate) {
frm.pg.script = `
function setupForm({ pg }) {
return {
actions: [],
statuses: [],
}
}`.trim();
}
if (frm.doc.view == "List" && !has_list_boilerplate) {
frm.doc.script = `
if (frm.pg.view == "List" && !has_list_boilerplate) {
frm.pg.script = `
function setupList({ list }) {
return {
actions: [],

View File

@ -3,7 +3,7 @@
"allow_rename": 1,
"autoname": "prompt",
"creation": "2023-12-28 14:18:09.329868",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"dt",
@ -27,8 +27,8 @@
"fieldname": "dt",
"fieldtype": "Link",
"in_list_view": 1,
"label": "DocType",
"options": "DocType",
"label": "PageType",
"options": "PageType",
"reqd": 1
},
{
@ -39,7 +39,7 @@
"label": "Enabled"
},
{
"default": "function setupForm({ doc }) {\n return {\n actions: [],\n }\n}",
"default": "function setupForm({ pg }) {\n return {\n actions: [],\n }\n}",
"documentation_url": "https://docs.jingrow.com/crm/custom-actions",
"fieldname": "script",
"fieldtype": "Code",

View File

@ -1,41 +1,41 @@
# Copyright (c) 2023, JINGROW and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
import jingrow
from jingrow import _
from jingrow.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
jingrow.flags.in_install
or jingrow.flags.in_patch
or jingrow.flags.in_test
or jingrow.flags.in_fixtures
)
if in_user_env and not self.is_new() and self.is_standard and not frappe.conf.developer_mode:
if in_user_env and not self.is_new() and self.is_standard and not jingrow.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"))
jingrow.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")
"""Returns the form script for the given pagetype"""
FormScript = jingrow.qb.PageType("CRM Form Script")
query = (
frappe.qb.from_(FormScript)
jingrow.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
pg = query.run(as_dict=True)
if pg:
return [d.script for d in pg] if len(pg) > 1 else pg[0].script
else:
return None

View File

@ -1,8 +1,8 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMFormScript(UnitTestCase):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2025, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Global Settings", {
// jingrow.ui.form.on("CRM Global Settings", {
// refresh(frm) {
// },

View File

@ -3,7 +3,7 @@
"allow_rename": 1,
"autoname": "format:{type}-{dt}",
"creation": "2025-02-28 14:37:10.002433",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"dt",
@ -14,12 +14,12 @@
],
"fields": [
{
"default": "DocType",
"default": "PageType",
"fieldname": "dt",
"fieldtype": "Link",
"in_list_view": 1,
"label": "DocType",
"options": "DocType",
"label": "PageType",
"options": "PageType",
"reqd": 1
},
{

View File

@ -1,8 +1,8 @@
# Copyright (c) 2025, JINGROW and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
# import jingrow
from jingrow.model.document import Document
class CRMGlobalSettings(Document):

View File

@ -1,11 +1,11 @@
# Copyright (c) 2025, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# import jingrow
from jingrow.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# On IntegrationTestCase, the pagetype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]

View File

@ -2,7 +2,7 @@
"actions": [],
"allow_rename": 1,
"creation": "2023-12-14 11:16:15.476366",
"doctype": "DocType",
"pagetype": "PageType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [

View File

@ -1,8 +1,8 @@
# 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.document import Document
class CRMHoliday(Document):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Holiday List", {
// jingrow.ui.form.on("CRM Holiday List", {
// refresh(frm) {
// },

View File

@ -3,7 +3,7 @@
"allow_rename": 1,
"autoname": "field:holiday_list_name",
"creation": "2023-12-14 11:09:12.876640",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"holiday_list_name",

View File

@ -1,8 +1,8 @@
# 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.document import Document
class CRMHolidayList(Document):

View File

@ -1,8 +1,8 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMHolidayList(UnitTestCase):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Industry", {
// jingrow.ui.form.on("CRM Industry", {
// refresh(frm) {
// },

View File

@ -5,7 +5,7 @@
"autoname": "field:industry",
"creation": "2023-07-24 19:40:31.980882",
"default_view": "List",
"doctype": "DocType",
"pagetype": "PageType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [

View File

@ -1,8 +1,8 @@
# 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.document import Document
class CRMIndustry(Document):

View File

@ -1,8 +1,8 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMIndustry(UnitTestCase):

View File

@ -1,9 +1,9 @@
// Copyright (c) 2024, JINGROW and contributors
// For license information, please see license.txt
frappe.ui.form.on("CRM Invitation", {
jingrow.ui.form.on("CRM Invitation", {
refresh(frm) {
if (frm.doc.status != "Accepted") {
if (frm.pg.status != "Accepted") {
frm.add_custom_button(__("Accept Invitation"), () => {
return frm.call("accept_invitation");
});

View File

@ -2,7 +2,7 @@
"actions": [],
"allow_rename": 1,
"creation": "2024-09-03 12:19:18.933810",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"email",

View File

@ -1,46 +1,46 @@
# Copyright (c) 2024, JINGROW and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
import jingrow
from jingrow.model.document import Document
class CRMInvitation(Document):
def before_insert(self):
frappe.utils.validate_email_address(self.email, True)
jingrow.utils.validate_email_address(self.email, True)
self.key = frappe.generate_hash(length=12)
self.invited_by = frappe.session.user
self.key = jingrow.generate_hash(length=12)
self.invited_by = jingrow.session.user
self.status = "Pending"
def after_insert(self):
self.invite_via_email()
def invite_via_email(self):
invite_link = frappe.utils.get_url(f"/api/method/crm.api.accept_invitation?key={self.key}")
if frappe.local.dev_server:
invite_link = jingrow.utils.get_url(f"/api/method/crm.api.accept_invitation?key={self.key}")
if jingrow.local.dev_server:
print(f"Invite link for {self.email}: {invite_link}")
title = "Frappe CRM"
title = "Jingrow CRM"
template = "crm_invitation"
frappe.sendmail(
jingrow.sendmail(
recipients=self.email,
subject=f"You have been invited to join {title}",
template=template,
args={"title": title, "invite_link": invite_link},
now=True,
)
self.db_set("email_sent_at", frappe.utils.now())
self.db_set("email_sent_at", jingrow.utils.now())
@frappe.whitelist()
@jingrow.whitelist()
def accept_invitation(self):
frappe.only_for(["System Manager", "Sales Manager"])
jingrow.only_for(["System Manager", "Sales Manager"])
self.accept()
def accept(self):
if self.status == "Expired":
frappe.throw("Invalid or expired key")
jingrow.throw("Invalid or expired key")
user = self.create_user_if_not_exists()
user.append_roles(self.role)
@ -53,11 +53,11 @@ class CRMInvitation(Document):
user.save(ignore_permissions=True)
self.status = "Accepted"
self.accepted_at = frappe.utils.now()
self.accepted_at = jingrow.utils.now()
self.save(ignore_permissions=True)
def update_module_in_user(self, user, module):
block_modules = frappe.get_all(
block_modules = jingrow.get_all(
"Module Def",
fields=["name as module"],
filters={"name": ["!=", module]},
@ -67,29 +67,29 @@ class CRMInvitation(Document):
user.set("block_modules", block_modules)
def create_user_if_not_exists(self):
if not frappe.db.exists("User", self.email):
if not jingrow.db.exists("User", self.email):
first_name = self.email.split("@")[0].title()
user = frappe.get_doc(
doctype="User",
user = jingrow.get_pg(
pagetype="User",
user_type="System User",
email=self.email,
send_welcome_email=0,
first_name=first_name,
).insert(ignore_permissions=True)
else:
user = frappe.get_doc("User", self.email)
user = jingrow.get_pg("User", self.email)
return user
def expire_invitations():
"""expire invitations after 3 days"""
from frappe.utils import add_days, now
from jingrow.utils import add_days, now
days = 3
invitations_to_expire = frappe.db.get_all(
invitations_to_expire = jingrow.db.get_all(
"CRM Invitation", filters={"status": "Pending", "creation": ["<", add_days(now(), -days)]}
)
for invitation in invitations_to_expire:
invitation = frappe.get_doc("CRM Invitation", invitation.name)
invitation = jingrow.get_pg("CRM Invitation", invitation.name)
invitation.status = "Expired"
invitation.save(ignore_permissions=True)

View File

@ -1,8 +1,8 @@
# Copyright (c) 2024, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMInvitation(UnitTestCase):

View File

@ -1,23 +1,23 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
frappe.ui.form.on("CRM Lead", {
jingrow.ui.form.on("CRM Lead", {
refresh(frm) {
frm.add_web_link(`/crm/leads/${frm.doc.name}`, __("Open in Portal"));
frm.add_web_link(`/crm/leads/${frm.pg.name}`, __("Open in Portal"));
},
update_total: function (frm) {
let total = 0;
let total_qty = 0;
let net_total = 0;
frm.doc.products.forEach((d) => {
frm.pg.products.forEach((d) => {
total += d.amount;
total_qty += d.qty;
net_total += d.net_amount;
});
frappe.model.set_value(frm.doctype, frm.docname, "total", total);
frappe.model.set_value(
frm.doctype,
jingrow.model.set_value(frm.pagetype, frm.docname, "total", total);
jingrow.model.set_value(
frm.pagetype,
frm.docname,
"net_total",
net_total || total
@ -25,7 +25,7 @@ frappe.ui.form.on("CRM Lead", {
}
});
frappe.ui.form.on("CRM Products", {
jingrow.ui.form.on("CRM Products", {
products_add: function (frm, cdt, cdn) {
frm.trigger("update_total");
},
@ -33,34 +33,34 @@ frappe.ui.form.on("CRM Products", {
frm.trigger("update_total");
},
product_code: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, "product_name", d.product_code);
let d = jingrow.get_pg(cdt, cdn);
jingrow.model.set_value(cdt, cdn, "product_name", d.product_code);
},
rate: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
let d = jingrow.get_pg(cdt, cdn);
if (d.rate && d.qty) {
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
jingrow.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
}
frm.trigger("update_total");
},
qty: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
let d = jingrow.get_pg(cdt, cdn);
if (d.rate && d.qty) {
frappe.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
jingrow.model.set_value(cdt, cdn, "amount", d.rate * d.qty);
}
frm.trigger("update_total");
},
discount_percentage: function (frm, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
let d = jingrow.get_pg(cdt, cdn);
if (d.discount_percentage && d.amount) {
discount_amount = (d.discount_percentage / 100) * d.amount;
frappe.model.set_value(
jingrow.model.set_value(
cdt,
cdn,
"discount_amount",
discount_amount
);
frappe.model.set_value(
jingrow.model.set_value(
cdt,
cdn,
"net_amount",

View File

@ -5,7 +5,7 @@
"autoname": "naming_series:",
"creation": "2023-07-24 12:19:39.616298",
"default_view": "List",
"doctype": "DocType",
"pagetype": "PageType",
"editable_grid": 1,
"email_append_to": 1,
"engine": "InnoDB",

View File

@ -1,14 +1,14 @@
# Copyright (c) 2023, JINGROW and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.desk.form.assign_to import add as assign
from frappe.model.document import Document
from frappe.utils import has_gravatar, validate_email_address
import jingrow
from jingrow import _
from jingrow.desk.form.assign_to import add as assign
from jingrow.model.document import Document
from jingrow.utils import has_gravatar, validate_email_address
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
from crm.fcrm.pagetype.crm_service_level_agreement.utils import get_sla
from crm.fcrm.pagetype.crm_status_change_log.crm_status_change_log import (
add_status_change_log,
)
@ -53,7 +53,7 @@ class CRMLead(Document):
if not self.lead_name:
# Check for leads being created through data import
if not self.organization and not self.email and not self.flags.ignore_mandatory:
frappe.throw(_("A Lead requires either a person's name or an organization's name"))
jingrow.throw(_("A Lead requires either a person's name or an organization's name"))
elif self.organization:
self.lead_name = self.organization
elif self.email:
@ -70,7 +70,7 @@ class CRMLead(Document):
validate_email_address(self.email, throw=True)
if self.email == self.lead_owner:
frappe.throw(_("Lead Owner cannot be same as the Lead Email Address"))
jingrow.throw(_("Lead Owner cannot be same as the Lead Email Address"))
if self.is_new() or not self.image:
self.image = has_gravatar(self.email)
@ -86,34 +86,34 @@ class CRMLead(Document):
# the agent is already set as an assignee
return
assign({"assign_to": [agent], "doctype": "CRM Lead", "name": self.name})
assign({"assign_to": [agent], "pagetype": "CRM Lead", "name": self.name})
def share_with_agent(self, agent):
if not agent:
return
docshares = frappe.get_all(
docshares = jingrow.get_all(
"DocShare",
filters={"share_name": self.name, "share_doctype": self.doctype},
filters={"share_name": self.name, "share_pagetype": self.pagetype},
fields=["name", "user"],
)
shared_with = [d.user for d in docshares] + [agent]
for user in shared_with:
if user == agent and not frappe.db.exists(
if user == agent and not jingrow.db.exists(
"DocShare",
{"user": agent, "share_name": self.name, "share_doctype": self.doctype},
{"user": agent, "share_name": self.name, "share_pagetype": self.pagetype},
):
frappe.share.add_docshare(
self.doctype,
jingrow.share.add_docshare(
self.pagetype,
self.name,
agent,
write=1,
flags={"ignore_share_permission": True},
)
elif user != agent:
frappe.share.remove(self.doctype, self.name, user)
jingrow.share.remove(self.pagetype, self.name, user)
def create_contact(self, existing_contact=None, throw=True):
if not self.lead_name:
@ -125,7 +125,7 @@ class CRMLead(Document):
self.update_lead_contact(existing_contact)
return existing_contact
contact = frappe.new_doc("Contact")
contact = jingrow.new_pg("Contact")
contact.update(
{
"first_name": self.first_name or self.lead_name,
@ -156,14 +156,14 @@ class CRMLead(Document):
if not self.organization and not existing_organization:
return
existing_organization = existing_organization or frappe.db.exists(
existing_organization = existing_organization or jingrow.db.exists(
"CRM Organization", {"organization_name": self.organization}
)
if existing_organization:
self.db_set("organization", existing_organization)
return existing_organization
organization = frappe.new_doc("CRM Organization")
organization = jingrow.new_pg("CRM Organization")
organization.update(
{
"organization_name": self.organization,
@ -177,8 +177,8 @@ class CRMLead(Document):
return organization.name
def update_lead_contact(self, contact):
contact = frappe.get_cached_doc("Contact", contact)
frappe.db.set_value(
contact = jingrow.get_cached_pg("Contact", contact)
jingrow.db.set_value(
"CRM Lead",
self.name,
{
@ -191,11 +191,11 @@ class CRMLead(Document):
)
def contact_exists(self, throw=True):
email_exist = frappe.db.exists("Contact Email", {"email_id": self.email})
phone_exist = frappe.db.exists("Contact Phone", {"phone": self.phone})
mobile_exist = frappe.db.exists("Contact Phone", {"phone": self.mobile_no})
email_exist = jingrow.db.exists("Contact Email", {"email_id": self.email})
phone_exist = jingrow.db.exists("Contact Phone", {"phone": self.phone})
mobile_exist = jingrow.db.exists("Contact Phone", {"phone": self.mobile_no})
doctype = "Contact Email" if email_exist else "Contact Phone"
pagetype = "Contact Email" if email_exist else "Contact Phone"
name = email_exist or phone_exist or mobile_exist
if name:
@ -204,10 +204,10 @@ class CRMLead(Document):
value = "{0}: {1}".format(text, data)
contact = frappe.db.get_value(doctype, name, "parent")
contact = jingrow.db.get_value(pagetype, name, "parent")
if throw:
frappe.throw(
jingrow.throw(
_("Contact already exists with {0}").format(value),
title=_("Contact Already Exists"),
)
@ -216,7 +216,7 @@ class CRMLead(Document):
return False
def create_deal(self, contact, organization, deal=None):
new_deal = frappe.new_doc("CRM Deal")
new_deal = jingrow.new_pg("CRM Deal")
lead_deal_map = {
"lead_owner": "deal_owner",
@ -314,12 +314,12 @@ class CRMLead(Document):
"""
if not self.sla:
return
sla = frappe.get_last_doc("CRM Service Level Agreement", {"name": self.sla})
sla = jingrow.get_last_pg("CRM Service Level Agreement", {"name": self.sla})
if sla:
sla.apply(self)
def convert_to_deal(self, deal=None):
return convert_to_deal(lead=self.name, doc=self, deal=deal)
return convert_to_deal(lead=self.name, pg=self, deal=deal)
@staticmethod
def get_non_filterable_fields():
@ -400,18 +400,18 @@ class CRMLead(Document):
}
@frappe.whitelist()
def convert_to_deal(lead, doc=None, deal=None, existing_contact=None, existing_organization=None):
if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission(
@jingrow.whitelist()
def convert_to_deal(lead, pg=None, deal=None, existing_contact=None, existing_organization=None):
if not (pg and pg.flags.get("ignore_permissions")) and not jingrow.has_permission(
"CRM Lead", "write", lead
):
frappe.throw(_("Not allowed to convert Lead to Deal"), frappe.PermissionError)
jingrow.throw(_("Not allowed to convert Lead to Deal"), jingrow.PermissionError)
lead = frappe.get_cached_doc("CRM Lead", lead)
if frappe.db.exists("CRM Lead Status", "Qualified"):
lead = jingrow.get_cached_pg("CRM Lead", lead)
if jingrow.db.exists("CRM Lead Status", "Qualified"):
lead.db_set("status", "Qualified")
lead.db_set("converted", 1)
if lead.sla and frappe.db.exists("CRM Communication Status", "Replied"):
if lead.sla and jingrow.db.exists("CRM Communication Status", "Replied"):
lead.db_set("communication_status", "Replied")
contact = lead.create_contact(existing_contact, False)
organization = lead.create_organization(existing_organization)

View File

@ -1,8 +1,8 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMLead(UnitTestCase):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Lead Source", {
// jingrow.ui.form.on("CRM Lead Source", {
// refresh(frm) {
// },

View File

@ -5,7 +5,7 @@
"autoname": "field:source_name",
"creation": "2023-07-24 19:47:01.063203",
"default_view": "List",
"doctype": "DocType",
"pagetype": "PageType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [

View File

@ -1,8 +1,8 @@
# 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.document import Document
class CRMLeadSource(Document):

View File

@ -1,8 +1,8 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMLeadSource(UnitTestCase):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2023, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Lead Status", {
// jingrow.ui.form.on("CRM Lead Status", {
// refresh(frm) {
// },

View File

@ -3,7 +3,7 @@
"allow_rename": 1,
"autoname": "field:lead_status",
"creation": "2023-11-29 11:09:53.678414",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"lead_status",

View File

@ -1,8 +1,8 @@
# 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.document import Document
class CRMLeadStatus(Document):

View File

@ -1,8 +1,8 @@
# Copyright (c) 2023, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import UnitTestCase
# import jingrow
from jingrow.tests import UnitTestCase
class TestCRMLeadStatus(UnitTestCase):

View File

@ -1,7 +1,7 @@
// Copyright (c) 2025, JINGROW and contributors
// For license information, please see license.txt
// frappe.ui.form.on("CRM Lost Reason", {
// jingrow.ui.form.on("CRM Lost Reason", {
// refresh(frm) {
// },

View File

@ -3,7 +3,7 @@
"allow_rename": 1,
"autoname": "field:lost_reason",
"creation": "2025-06-30 16:51:31.082360",
"doctype": "DocType",
"pagetype": "PageType",
"engine": "InnoDB",
"field_order": [
"lost_reason",

View File

@ -1,8 +1,8 @@
# Copyright (c) 2025, JINGROW and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
# import jingrow
from jingrow.model.document import Document
class CRMLostReason(Document):

View File

@ -1,11 +1,11 @@
# Copyright (c) 2025, JINGROW and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# import jingrow
from jingrow.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# On IntegrationTestCase, the pagetype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]

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