diff --git a/.github/logo.svg b/.github/logo.svg new file mode 100644 index 00000000..e203a0f8 --- /dev/null +++ b/.github/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/.github/screenshots/CallLog.png b/.github/screenshots/CallLog.png new file mode 100644 index 00000000..016e3f41 Binary files /dev/null and b/.github/screenshots/CallLog.png differ diff --git a/.github/screenshots/CallLogs.png b/.github/screenshots/CallLogs.png deleted file mode 100644 index 0eab791a..00000000 Binary files a/.github/screenshots/CallLogs.png and /dev/null differ diff --git a/.github/screenshots/CallUI.png b/.github/screenshots/CallUI.png index 7ebddd01..e0397722 100644 Binary files a/.github/screenshots/CallUI.png and b/.github/screenshots/CallUI.png differ diff --git a/.github/screenshots/DealsList.png b/.github/screenshots/DealsList.png deleted file mode 100644 index c2911d70..00000000 Binary files a/.github/screenshots/DealsList.png and /dev/null differ diff --git a/.github/screenshots/EmailTemplate.png b/.github/screenshots/EmailTemplate.png new file mode 100644 index 00000000..57b73904 Binary files /dev/null and b/.github/screenshots/EmailTemplate.png differ diff --git a/.github/screenshots/Emailtemplates.png b/.github/screenshots/Emailtemplates.png deleted file mode 100644 index a30f08a8..00000000 Binary files a/.github/screenshots/Emailtemplates.png and /dev/null differ diff --git a/.github/screenshots/FrappeCRMHeroImage.png b/.github/screenshots/FrappeCRMHeroImage.png new file mode 100644 index 00000000..3c5fc600 Binary files /dev/null and b/.github/screenshots/FrappeCRMHeroImage.png differ diff --git a/.github/screenshots/LeadList.png b/.github/screenshots/LeadList.png new file mode 100644 index 00000000..6cea6927 Binary files /dev/null and b/.github/screenshots/LeadList.png differ diff --git a/.github/screenshots/LeadPage.png b/.github/screenshots/LeadPage.png index 854035e4..2bf92b35 100644 Binary files a/.github/screenshots/LeadPage.png and b/.github/screenshots/LeadPage.png differ diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml new file mode 100644 index 00000000..b130ab96 --- /dev/null +++ b/.github/workflows/builds.yml @@ -0,0 +1,66 @@ +name: Build Container Image +on: + workflow_dispatch: + push: + branches: + - main + - develop + tags: + - "*" + +jobs: + build: + name: Build + + runs-on: ubuntu-latest + + strategy: + matrix: + arch: [amd64, arm64] + + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/${{ matrix.arch }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set Branch + run: | + export APPS_JSON='[{"url": "https://github.com/${{ github.repository }}","branch": "${{ github.ref_name }}"}]' + echo "APPS_JSON_BASE64=$(echo $APPS_JSON | base64 -w 0)" >> $GITHUB_ENV + echo "FRAPPE_BRANCH=${{ github.ref_type == 'tag' || github.ref_name == 'main' && 'version-15' || 'develop' }}" >> $GITHUB_ENV + + - name: Set Image Tag + run: | + echo "IMAGE_TAG=${{ github.ref_name == 'develop' && 'latest' || 'v15' }}" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + with: + repository: frappe/frappe_docker + path: builds + + - name: Build and push + uses: docker/build-push-action@v6 + with: + push: true + context: builds + file: builds/images/layered/Containerfile + tags: > + ghcr.io/${{ github.repository }}:${{ github.ref_name }}, + ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} + build-args: | + "FRAPPE_BRANCH=${{ env.FRAPPE_BRANCH }}" + "APPS_JSON_BASE64=${{ env.APPS_JSON_BASE64 }}" diff --git a/README.md b/README.md index ed962a2a..e7123429 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,193 @@ -
- - Screenshot 2022-09-18 at 9 16 08 PM - +
+ + + Frappe CRM Logo + + +

Frappe CRM

+ +**Simplify Sales, Amplify Relationships** + +![GitHub release (latest by date)](https://img.shields.io/github/v/release/frappe/crm) + +
+ + + Frappe CRM Hero Image +
-

- - issues - - - license - -

+[Live Demo](https://frappecrm-demo.frappe.cloud/api/method/crm.api.demo.login) - [Website](https://frappe.io/crm) - [Documentation](https://docs.frappe.io/crm) -
- Show more screenshots - Screenshot 2022-09-18 at 9 18 17 PM - Screenshot 2022-09-18 at 11 47 06 PM - Screenshot 2022-09-18 at 9 18 47 PM - Screenshot 2022-09-18 at 9 18 47 PM - Screenshot 2022-09-18 at 9 18 47 PM -
+
-## Key Features +## Frappe CRM -- **Views:** Create custom views which is a combination of filters, sort and columns. - - **Pinned View:** Pin important leads and deals in the sidebar. - - **Public View:** Share views with all users. - - **Saved View:** Save views for later use. -- **Email Communication:** Send and receive emails directly from the Lead/Deal Page. -- **Email Templates:** Create and use email templates for faster communication. -- **Comments:** Add comments to leads and deals to keep track of the conversation. -- **Notifications:** Get notified when someone mentions you in a comment. -- **Service Level Agreement:** Set SLA for leads and deals and get notified when the SLA is breached. -- **Assignment Rule:** Automatically assign leads and deals to users based on the criteria. -- **Tasks:** Create tasks for leads and deals. -- **Notes:** Add notes to leads and deals. -- **Call Logs:** See the call logs with call details and recordings. +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. -## Integrations +### Motivation + +The motivation behind building Frappe CRM stems from the need for a simple, customizable, and open-source solution tailored to modern business needs. Many existing CRMs are either too complex, overly generic, or locked behind steep pricing models that hinder accessibility and flexibility. Frappe CRM was designed to bridge this gap, offering a tool that empowers businesses to manage their customer relationships seamlessly while being easy to adapt to specific workflows. Built on the Frappe framework, it prioritizes usability, extensibility, and affordability, making it an ideal choice for growing teams and organizations looking for a CRM that aligns with their unique processes. + +### Key Features + +- **User-Friendly and Flexible:** A simple, intuitive interface that’s easy to navigate and highly customizable, enabling teams to adapt it to their specific processes effortlessly. +- **All-in-One Lead/Deal Page:** Consolidate all essential actions and details—like activities, comments, notes, tasks, and more—into a single page for a seamless workflow experience. +- **Kanban View:** Manage leads and deals visually with a drag-and-drop Kanban board, offering clarity and efficiency in tracking progress across stages. +- **Custom Views:** Design personalized views to organize and display leads and deals using custom filters, sorting, and columns, ensuring quick access to the most relevant information. + +
+ Screenshots + +
+ + + Lead List + +
+
+ + + Lead Page + +
+
+ + + Email Template + +
+
+ + + Call UI + +
+
+ + + Call Log + +
+ +
+ +### Integrations - **Twilio:** Integrate Twilio to make and receive calls from the CRM. You can also record calls. It is a built-in integration. - **WhatsApp:** Integrate WhatsApp to send and receive messages from the CRM. [Frappe WhatsApp](https://github.com/shridarpatil/frappe_whatsapp) is used for this integration. +- **ERPNext:** Integrate with [ERPNext](https://erpnext.com) to extend the CRM capabilities to include invoicing, accounting, and more. -## Getting Started +### 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. + +## Getting Started (Production) ### Managed Hosting -Get started with your personal or business site with a few clicks on [Frappe Cloud](https://frappecloud.com/marketplace/apps/crm). +Get started with your personal or business site with a few clicks on Frappe Cloud - our official hosting service. +
+ + + + Try on Frappe Cloud + + +
-### Docker (Recommended) +### Self Hosting -The quickest way to set up Frappe CRM and take it for a test ride. +Follow these steps to set up Frappe CRM in production: -Frappe framework is multi-tenant and supports multiple apps by default. This docker compose is just a standalone version with Frappe CRM pre-installed. Just put it behind your desired reverse-proxy if needed, and you're good to go. - -If you wish to use multiple Frappe apps or need multi-tenancy. Take a look at our production ready self-hosted workflow, or join us on Frappe Cloud to get first party support and hassle-free hosting. +**Step 1**: Download the easy install script + +```bash +wget https://frappe.io/easy-install.py +``` + +**Step 2**: Run the deployment command + +```bash +python3 ./easy-install.py deploy \ + --project=crm_prod_setup \ + --email=email.example.com \ + --image=ghcr.io/frappe/crm \ + --version=stable \ + --app=crm \ + --sitename subdomain.domain.tld +``` + +Replace the following parameters with your values: + +- `email.example.com`: Your email address +- `subdomain.domain.tld`: Your domain name where CRM will be hosted + +The script will set up a production-ready instance of Frappe CRM with all the necessary configurations in about 5 minutes. + +## Getting Started (Development) + +### Local Setup + +1. [Setup Bench](https://docs.frappe.io/framework/user/en/installation). +1. In the frappe-bench directory, run `bench start` and keep it running. +1. Open a new terminal session and cd into `frappe-bench` directory and run following commands: + ```sh + $ bench get-app crm + $ bench new-site sitename.localhost --install-app crm + $ bench browse sitename.localhost --user Administrator + ``` +1. Access the crm page at `sitename.localhost:8000/crm` in your web browser. + +**For Frontend Development** +1. Open a new terminal session and cd into `frappe-bench/apps/crm`, and run the following commands: + ``` + yarn install + yarn dev + ``` +1. Now, you can access the site on vite dev server at `http://sitename.localhost:8080` + +**Note:** You'll find all the code related to Frappe CRM's frontend inside `frappe-bench/apps/crm/frontend` + +### Docker + +You need Docker, docker-compose and git setup on your machine. Refer [Docker documentation](https://docs.docker.com/). After that, follow below steps: **Step 1**: Setup folder and download the required files mkdir frappe-crm cd frappe-crm -**Step 2**: Download the required files - -Docker Compose File: - + # Download the docker-compose file wget -O docker-compose.yml https://raw.githubusercontent.com/frappe/crm/develop/docker/docker-compose.yml -Frappe CRM bench setup script - + # Download the setup script wget -O init.sh https://raw.githubusercontent.com/frappe/crm/develop/docker/init.sh -**Step 3**: Run the container and daemonize it +**Step 2**: Run the container and daemonize it docker compose up -d -**Step 4**: The site [http://crm.localhost](http://crm.localhost) should now be available. The default credentials are: +**Step 3**: The site [http://crm.localhost:8000/crm](http://crm.localhost:8000/crm) should now be available. The default credentials are: -> username: administrator -> password: admin +- Username: Administrator +- Password: admin -### Self-hosting +## Learn and connect -If you prefer self-hosting, follow the official [Frappe Bench Installation](https://github.com/frappe/bench#installation) instructions. +- [Telegram Public Group](https://t.me/frappecrm) +- [Discuss Forum](https://discuss.frappe.io/c/frappe-crm) +- [Documentation](https://docs.frappe.io/crm) +- [YouTube](https://www.youtube.com/channel/UCn3bV5kx77HsVwtnlCeEi_A) -## Want to Just Try Out or Contribute? - -### Codespaces - -1. Open [this link](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=668199241&skip_quickstart=true&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&geo=SoutheastAsia) and click on "Create Codespace". -2. Wait for initialization (~15 mins). -3. Run `bench start` from the terminal tab. -4. Click on the link beside "8000" port under "Ports" tab. -5. Log in with "Administrator" as the username and "admin" as the password. -6. Go to `.github.dev/crm` to access the crm interface. - -### Local Setup - -1. [Install Bench](https://github.com/frappe/bench). -2. Install Frappe CRM app: - ```sh - $ bench get-app crm - ``` -3. Create a site with the crm app: - ```sh - $ bench --site sitename.localhost install-app crm - ``` -4. Open the site in the browser: - ```sh - $ bench browse sitename.localhost --user Administrator - ``` -5. Access the crm page at `sitename.localhost:8000/crm` in your web browser. - -## Need help? - -Join our [telegram group](https://t.me/frappecrm) for instant help. - -## Documentation - -Check out the [official documentation](https://docs.frappe.io/crm) for more details. - -## License - -[GNU Affero General Public License v3.0](LICENSE) +
+
+
+ + + + Frappe Technologies + + +
diff --git a/crm/api/comment.py b/crm/api/comment.py index 7d78f0d4..c3b7ca65 100644 --- a/crm/api/comment.py +++ b/crm/api/comment.py @@ -5,6 +5,7 @@ from frappe import _ from bs4 import BeautifulSoup from crm.fcrm.doctype.crm_notification.crm_notification import notify_user + def on_update(self, method): notify_mentions(self) @@ -24,25 +25,31 @@ def notify_mentions(doc): doctype = doc.reference_doctype if doctype.startswith("CRM "): doctype = doctype[4:].lower() - name = reference_doc.lead_name or name if doctype == "lead" else reference_doc.organization or reference_doc.lead_name or name + name = ( + reference_doc.lead_name + if doctype == "lead" + else reference_doc.organization or reference_doc.lead_name + ) notification_text = f""" -
- { owner } +
+ { owner } { _('mentioned you in {0}').format(doctype) } - { name } + { name }
""" - notify_user({ - "owner": doc.owner, - "assigned_to": mention.email, - "notification_type": "Mention", - "message": doc.content, - "notification_text": notification_text, - "reference_doctype": "Comment", - "reference_docname": doc.name, - "redirect_to_doctype": doc.reference_doctype, - "redirect_to_docname": doc.reference_name, - }) + notify_user( + { + "owner": doc.owner, + "assigned_to": mention.email, + "notification_type": "Mention", + "message": doc.content, + "notification_text": notification_text, + "reference_doctype": "Comment", + "reference_docname": doc.name, + "redirect_to_doctype": doc.reference_doctype, + "redirect_to_docname": doc.reference_name, + } + ) def extract_mentions(html): @@ -56,39 +63,42 @@ def extract_mentions(html): ) return mentions + @frappe.whitelist() def add_attachments(name: str, attachments: Iterable[str | dict]) -> None: - """Add attachments to the given Comment + """Add attachments to the given Comment - :param name: Comment name - :param attachments: File names or dicts with keys "fname" and "fcontent" - """ - # loop through attachments - for a in attachments: - if isinstance(a, str): - attach = frappe.db.get_value("File", {"name": a}, ["file_url", "is_private"], as_dict=1) - file_args = { - "file_url": attach.file_url, - "is_private": attach.is_private, - } - elif isinstance(a, dict) and "fcontent" in a and "fname" in a: - # dict returned by frappe.attach_print() - file_args = { - "file_name": a["fname"], - "content": a["fcontent"], - "is_private": 1, - } - else: - continue + :param name: Comment name + :param attachments: File names or dicts with keys "fname" and "fcontent" + """ + # loop through attachments + for a in attachments: + if isinstance(a, str): + attach = frappe.db.get_value( + "File", {"name": a}, ["file_url", "is_private"], as_dict=1 + ) + file_args = { + "file_url": attach.file_url, + "is_private": attach.is_private, + } + elif isinstance(a, dict) and "fcontent" in a and "fname" in a: + # dict returned by frappe.attach_print() + file_args = { + "file_name": a["fname"], + "content": a["fcontent"], + "is_private": 1, + } + else: + continue - file_args.update( - { - "attached_to_doctype": "Comment", - "attached_to_name": name, - "folder": "Home/Attachments", - } - ) + file_args.update( + { + "attached_to_doctype": "Comment", + "attached_to_name": name, + "folder": "Home/Attachments", + } + ) - _file = frappe.new_doc("File") - _file.update(file_args) - _file.save(ignore_permissions=True) \ No newline at end of file + _file = frappe.new_doc("File") + _file.update(file_args) + _file.save(ignore_permissions=True) diff --git a/crm/api/doc.py b/crm/api/doc.py index 576127a1..b7e3e463 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -78,12 +78,7 @@ def get_filterable_fields(doctype: str): # append standard fields (getting error when using frappe.model.std_fields) standard_fields = [ {"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype}, - { - "fieldname": "owner", - "fieldtype": "Link", - "label": "Created By", - "options": "User" - }, + {"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"}, { "fieldname": "modified_by", "fieldtype": "Link", @@ -98,10 +93,7 @@ def get_filterable_fields(doctype: str): {"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"}, ] for field in standard_fields: - if ( - field.get("fieldname") not in restricted_fields and - field.get("fieldtype") in allowed_fieldtypes - ): + if field.get("fieldname") not in restricted_fields and field.get("fieldtype") in allowed_fieldtypes: field["name"] = field.get("fieldname") res.append(field) @@ -128,7 +120,11 @@ def get_group_by_fields(doctype: str): ] fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldtype not in no_value_fields and field.fieldtype in allowed_fieldtypes] + fields = [ + field + for field in fields + if field.fieldtype not in no_value_fields and field.fieldtype in allowed_fieldtypes + ] fields = [ { "label": _(field.label), @@ -176,6 +172,7 @@ def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fi .run(as_dict=True) ) + @frappe.whitelist() def get_quick_filters(doctype: str): meta = frappe.get_meta(doctype) @@ -183,23 +180,25 @@ def get_quick_filters(doctype: str): quick_filters = [] for field in fields: - if field.fieldtype == "Select": field.options = field.options.split("\n") field.options = [{"label": option, "value": option} for option in field.options] field.options.insert(0, {"label": "", "value": ""}) - quick_filters.append({ - "label": _(field.label), - "name": field.fieldname, - "type": field.fieldtype, - "options": field.options, - }) + quick_filters.append( + { + "label": _(field.label), + "name": field.fieldname, + "type": field.fieldtype, + "options": field.options, + } + ) if doctype == "CRM Lead": quick_filters = [filter for filter in quick_filters if filter.get("name") != "converted"] return quick_filters + @frappe.whitelist() def get_data( doctype: str, @@ -223,9 +222,9 @@ def get_data( kanban_fields = frappe.parse_json(kanban_fields or "[]") kanban_columns = frappe.parse_json(kanban_columns or "[]") - custom_view_name = view.get('custom_view_name') if view else None - view_type = view.get('view_type') if view else None - group_by_field = view.get('group_by_field') if view else None + custom_view_name = view.get("custom_view_name") if view else None + view_type = view.get("view_type") if view else None + group_by_field = view.get("group_by_field") if view else None for key in filters: value = filters[key] @@ -268,7 +267,7 @@ def get_data( default_view_filters = { "dt": doctype, - "type": view_type or 'list', + "type": view_type or "list", "is_default": 1, "user": frappe.session.user, } @@ -295,13 +294,16 @@ def get_data( if group_by_field and group_by_field not in rows: rows.append(group_by_field) - data = frappe.get_list( - doctype, - fields=rows, - filters=filters, - order_by=order_by, - page_length=page_length, - ) or [] + data = ( + frappe.get_list( + doctype, + fields=rows, + filters=filters, + order_by=order_by, + page_length=page_length, + ) + or [] + ) if view_type == "kanban": if not rows: @@ -336,9 +338,9 @@ def get_data( rows.append(field) for kc in kanban_columns: - column_filters = { column_field: kc.get('name') } + column_filters = {column_field: kc.get("name")} order = kc.get("order") - if column_field in filters and filters.get(column_field) != kc.name or kc.get('delete'): + if column_field in filters and filters.get(column_field) != kc.name or kc.get("delete"): column_data = [] else: column_filters.update(filters.copy()) @@ -348,7 +350,9 @@ def get_data( page_length = kc.get("page_length") if order: - column_data = get_records_based_on_order(doctype, rows, column_filters, page_length, order) + column_data = get_records_based_on_order( + doctype, rows, column_filters, page_length, order + ) else: column_data = frappe.get_list( doctype, @@ -359,9 +363,11 @@ def get_data( ) new_filters = filters.copy() - new_filters.update({ column_field: kc.get('name') }) + new_filters.update({column_field: kc.get("name")}) - all_count = len(frappe.get_list(doctype, filters=convert_filter_to_tuple(doctype, new_filters))) + all_count = len( + frappe.get_list(doctype, filters=convert_filter_to_tuple(doctype, new_filters)) + ) kc["all_count"] = all_count kc["count"] = len(column_data) @@ -371,8 +377,8 @@ def get_data( if order: column_data = sorted( - column_data, key=lambda x: order.index(x.get("name")) - if x.get("name") in order else len(order) + column_data, + key=lambda x: order.index(x.get("name")) if x.get("name") in order else len(order), ) data.append({"column": kc, "fields": kanban_fields, "data": column_data}) @@ -406,8 +412,8 @@ def get_data( ] for field in std_fields: - if field.get('value') not in rows: - rows.append(field.get('value')) + if field.get("value") not in rows: + rows.append(field.get("value")) if field not in fields: field["label"] = _(field["label"]) fields.append(field) @@ -416,6 +422,7 @@ def get_data( is_default = frappe.db.get_value("CRM View Settings", custom_view_name, "load_default_columns") if group_by_field and view_type == "group_by": + def get_options(type, options): if type == "Select": return [option for option in options.split("\n")] @@ -428,7 +435,9 @@ def get_data( if order_by and group_by_field in order_by: order_by_fields = order_by.split(",") - order_by_fields = [(field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields] + order_by_fields = [ + (field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields + ] if (group_by_field, "asc") in order_by_fields: options.sort() elif (group_by_field, "desc") in order_by_fields: @@ -467,6 +476,7 @@ def get_data( "view_type": view_type, } + def convert_filter_to_tuple(doctype, filters): if isinstance(filters, dict): filters_items = filters.items() @@ -504,6 +514,7 @@ 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): not_allowed_fieldtypes = [ @@ -521,12 +532,7 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False): standard_fields = [ {"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype}, - { - "fieldname": "owner", - "fieldtype": "Link", - "label": "Created By", - "options": "User" - }, + {"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"}, { "fieldname": "modified_by", "fieldtype": "Link", @@ -542,7 +548,7 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False): ] for field in standard_fields: - if not restricted_fieldtypes or field.get('fieldtype') not in restricted_fieldtypes: + if not restricted_fieldtypes or field.get("fieldtype") not in restricted_fieldtypes: fields.append(field) if as_array: @@ -550,10 +556,11 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False): fields_meta = {} for field in fields: - fields_meta[field.get('fieldname')] = field + fields_meta[field.get("fieldname")] = field return fields_meta + @frappe.whitelist() def get_sidebar_fields(doctype, name): if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}): @@ -562,7 +569,7 @@ def get_sidebar_fields(doctype, name): if not layout: return [] - + layout = json.loads(layout) not_allowed_fieldtypes = [ @@ -600,6 +607,7 @@ def get_sidebar_fields(doctype, name): return layout + def get_field_obj(field): obj = { "label": field.label, @@ -641,6 +649,7 @@ def get_type(field): return "read_only" return field.fieldtype.lower() + def get_assigned_users(doctype, name, default_assigned_to=None): assigned_users = frappe.get_all( "ToDo", @@ -671,32 +680,55 @@ def get_fields(doctype: str, allow_all_fieldtypes: bool = False): _fields = [] for field in fields: - if ( - field.fieldtype not in not_allowed_fieldtypes - and field.fieldname - ): - _fields.append({ - "label": field.label, - "type": field.fieldtype, - "value": field.fieldname, - "options": field.options, - "mandatory": field.reqd, - "read_only": field.read_only, - "hidden": field.hidden, - "depends_on": field.depends_on, - "mandatory_depends_on": field.mandatory_depends_on, - "read_only_depends_on": field.read_only_depends_on, - "link_filters": field.get("link_filters"), - "placeholder": field.get("placeholder"), - }) + if field.fieldtype not in not_allowed_fieldtypes and field.fieldname: + _fields.append( + { + "label": field.label, + "type": field.fieldtype, + "value": field.fieldname, + "options": field.options, + "mandatory": field.reqd, + "read_only": field.read_only, + "hidden": field.hidden, + "depends_on": field.depends_on, + "mandatory_depends_on": field.mandatory_depends_on, + "read_only_depends_on": field.read_only_depends_on, + "link_filters": field.get("link_filters"), + "placeholder": field.get("placeholder"), + } + ) return _fields def getCounts(d, doctype): - d["_email_count"] = frappe.db.count("Communication", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "communication_type": "Communication"}) or 0 - d["_email_count"] = d["_email_count"] + frappe.db.count("Communication", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "communication_type": "Automated Message"}) - d["_comment_count"] = frappe.db.count("Comment", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "comment_type": "Comment"}) - d["_task_count"] = frappe.db.count("CRM Task", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}) - d["_note_count"] = frappe.db.count("FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}) - return d \ No newline at end of file + d["_email_count"] = ( + frappe.db.count( + "Communication", + filters={ + "reference_doctype": doctype, + "reference_name": d.get("name"), + "communication_type": "Communication", + }, + ) + or 0 + ) + d["_email_count"] = d["_email_count"] + frappe.db.count( + "Communication", + filters={ + "reference_doctype": doctype, + "reference_name": d.get("name"), + "communication_type": "Automated Message", + }, + ) + d["_comment_count"] = frappe.db.count( + "Comment", + filters={"reference_doctype": doctype, "reference_name": d.get("name"), "comment_type": "Comment"}, + ) + d["_task_count"] = frappe.db.count( + "CRM Task", filters={"reference_doctype": doctype, "reference_docname": d.get("name")} + ) + d["_note_count"] = frappe.db.count( + "FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")} + ) + return d diff --git a/crm/api/todo.py b/crm/api/todo.py index f30e19f4..dc9dcf28 100644 --- a/crm/api/todo.py +++ b/crm/api/todo.py @@ -2,102 +2,131 @@ import frappe from frappe import _ from crm.fcrm.doctype.crm_notification.crm_notification import notify_user -def after_insert(doc, method): - if doc.reference_type in ["CRM Lead", "CRM Deal"] and doc.reference_name and doc.allocated_to: - fieldname = "lead_owner" if doc.reference_type == "CRM Lead" else "deal_owner" - lead_owner = frappe.db.get_value(doc.reference_type, doc.reference_name, fieldname) - if not lead_owner: - frappe.db.set_value(doc.reference_type, doc.reference_name, fieldname, doc.allocated_to) - if doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"] and doc.reference_name and doc.allocated_to: - notify_assigned_user(doc) +def after_insert(doc, method): + if ( + doc.reference_type in ["CRM Lead", "CRM Deal"] + and doc.reference_name + and doc.allocated_to + ): + fieldname = "lead_owner" if doc.reference_type == "CRM Lead" else "deal_owner" + lead_owner = frappe.db.get_value( + doc.reference_type, doc.reference_name, fieldname + ) + if not lead_owner: + frappe.db.set_value( + doc.reference_type, doc.reference_name, fieldname, doc.allocated_to + ) + + if ( + doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"] + and doc.reference_name + and doc.allocated_to + ): + notify_assigned_user(doc) + def on_update(doc, method): - if doc.has_value_changed("status") and doc.status == "Cancelled" and doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"] and doc.reference_name and doc.allocated_to: - notify_assigned_user(doc, is_cancelled=True) + if ( + doc.has_value_changed("status") + and doc.status == "Cancelled" + and doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"] + and doc.reference_name + and doc.allocated_to + ): + notify_assigned_user(doc, is_cancelled=True) + def notify_assigned_user(doc, is_cancelled=False): - _doc = frappe.get_doc(doc.reference_type, doc.reference_name) - owner = frappe.get_cached_value("User", frappe.session.user, "full_name") - notification_text = get_notification_text(owner, doc, _doc, is_cancelled) + _doc = frappe.get_doc(doc.reference_type, doc.reference_name) + owner = frappe.get_cached_value("User", frappe.session.user, "full_name") + notification_text = get_notification_text(owner, doc, _doc, is_cancelled) - message = _("Your assignment on {0} {1} has been removed by {2}").format( - doc.reference_type, - doc.reference_name, - owner - ) if is_cancelled else _("{0} assigned a {1} {2} to you").format( - owner, - doc.reference_type, - doc.reference_name - ) + message = ( + _("Your assignment on {0} {1} has been removed by {2}").format( + doc.reference_type, doc.reference_name, owner + ) + if is_cancelled + else _("{0} assigned a {1} {2} to you").format( + owner, doc.reference_type, doc.reference_name + ) + ) - redirect_to_doctype, redirect_to_name = get_redirect_to_doc(doc) + redirect_to_doctype, redirect_to_name = get_redirect_to_doc(doc) + + notify_user( + { + "owner": frappe.session.user, + "assigned_to": doc.allocated_to, + "notification_type": "Assignment", + "message": message, + "notification_text": notification_text, + "reference_doctype": doc.reference_type, + "reference_docname": doc.reference_name, + "redirect_to_doctype": redirect_to_doctype, + "redirect_to_docname": redirect_to_name, + } + ) - notify_user({ - "owner": frappe.session.user, - "assigned_to": doc.allocated_to, - "notification_type": "Assignment", - "message": message, - "notification_text": notification_text, - "reference_doctype": doc.reference_type, - "reference_docname": doc.reference_name, - "redirect_to_doctype": redirect_to_doctype, - "redirect_to_docname": redirect_to_name, - }) def get_notification_text(owner, doc, reference_doc, is_cancelled=False): - name = doc.reference_name - doctype = doc.reference_type + name = doc.reference_name + doctype = doc.reference_type - if doctype.startswith("CRM "): - doctype = doctype[4:].lower() + if doctype.startswith("CRM "): + doctype = doctype[4:].lower() - if doctype in ["lead", "deal"]: - name = reference_doc.lead_name or name if doctype == "lead" else reference_doc.organization or reference_doc.lead_name or name + if doctype in ["lead", "deal"]: + name = ( + reference_doc.lead_name or name + if doctype == "lead" + else reference_doc.organization or reference_doc.lead_name or name + ) - if is_cancelled: - return f""" -
- { _('Your assignment on {0} {1} has been removed by {2}').format( - doctype, - f'{ name }', - f'{ owner }' - ) } -
- """ + if is_cancelled: + return f""" +
+ { _('Your assignment on {0} {1} has been removed by {2}').format( + doctype, + f'{ name }', + f'{ owner }' + ) } +
+ """ - return f""" -
- { owner } - { _('assigned a {0} {1} to you').format( - doctype, - f'{ name }' - ) } -
- """ + return f""" +
+ { owner } + { _('assigned a {0} {1} to you').format( + doctype, + f'{ name }' + ) } +
+ """ + + if doctype == "task": + if is_cancelled: + return f""" +
+ { _('Your assignment on task {0} has been removed by {1}').format( + f'{ reference_doc.title }', + f'{ owner }' + ) } +
+ """ + return f""" +
+ { owner } + { _('assigned a new task {0} to you').format( + f'{ reference_doc.title }' + ) } +
+ """ - if doctype == "task": - if is_cancelled: - return f""" -
- { _('Your assignment on task {0} has been removed by {1}').format( - f'{ reference_doc.title }', - f'{ owner }' - ) } -
- """ - return f""" -
- { owner } - { _('assigned a new task {0} to you').format( - f'{ reference_doc.title }' - ) } -
- """ def get_redirect_to_doc(doc): - if doc.reference_type == "CRM Task": - reference_doc = frappe.get_doc(doc.reference_type, doc.reference_name) - return reference_doc.reference_doctype, reference_doc.reference_docname + if doc.reference_type == "CRM Task": + reference_doc = frappe.get_doc(doc.reference_type, doc.reference_name) + return reference_doc.reference_doctype, reference_doc.reference_docname - return doc.reference_type, doc.reference_name + return doc.reference_type, doc.reference_name diff --git a/crm/api/whatsapp.py b/crm/api/whatsapp.py index a38c1194..3e04b5b6 100644 --- a/crm/api/whatsapp.py +++ b/crm/api/whatsapp.py @@ -30,25 +30,27 @@ def notify_agent(doc): if doctype.startswith("CRM "): doctype = doctype[4:].lower() notification_text = f""" -
- { _('You') } +
+ { _('You') } { _('received a whatsapp message in {0}').format(doctype) } - { doc.reference_name } + { doc.reference_name }
""" assigned_users = get_assigned_users(doc.reference_doctype, doc.reference_name) for user in assigned_users: - notify_user({ - "owner": doc.owner, - "assigned_to": user, - "notification_type": "WhatsApp", - "message": doc.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, - }) + notify_user( + { + "owner": doc.owner, + "assigned_to": user, + "notification_type": "WhatsApp", + "message": doc.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, + } + ) def get_lead_or_deal_from_number(number): @@ -92,6 +94,7 @@ def is_whatsapp_enabled(): return False return frappe.get_cached_value("WhatsApp Settings", "WhatsApp Settings", "enabled") + @frappe.whitelist() def is_whatsapp_installed(): if not frappe.db.exists("DocType", "WhatsApp Settings"): @@ -105,8 +108,8 @@ def get_whatsapp_messages(reference_doctype, reference_name): return [] messages = [] - if reference_doctype == 'CRM Deal': - lead = frappe.db.get_value(reference_doctype, reference_name, 'lead') + if reference_doctype == "CRM Deal": + lead = frappe.db.get_value(reference_doctype, reference_name, "lead") if lead: messages = frappe.get_all( "WhatsApp Message", diff --git a/crm/fcrm/doctype/crm_deal/api.py b/crm/fcrm/doctype/crm_deal/api.py index 76a764ae..7cc776bd 100644 --- a/crm/fcrm/doctype/crm_deal/api.py +++ b/crm/fcrm/doctype/crm_deal/api.py @@ -1,38 +1,19 @@ import frappe -from frappe import _ -from crm.api.doc import get_fields_meta, get_assigned_users +from crm.api.doc import get_assigned_users, get_fields_meta from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script + @frappe.whitelist() def get_deal(name): - Deal = frappe.qb.DocType("CRM Deal") + deal = frappe.get_doc("CRM Deal", name).as_dict() - query = ( - frappe.qb.from_(Deal) - .select("*") - .where(Deal.name == name) - .limit(1) - ) - - deal = query.run(as_dict=True) - if not len(deal): - frappe.throw(_("Deal not found"), frappe.DoesNotExistError) - deal = deal.pop() - - - deal["contacts"] = frappe.get_all( - "CRM Contacts", - filters={"parenttype": "CRM Deal", "parent": deal.name}, - fields=["contact", "is_primary"], - ) - - deal["doctype"] = "CRM Deal" - deal["fields_meta"] = get_fields_meta("CRM Deal") - deal["_form_script"] = get_form_script('CRM Deal') + deal["fields_meta"] = get_fields_meta("CRM Deal") + deal["_form_script"] = get_form_script("CRM Deal") deal["_assign"] = get_assigned_users("CRM Deal", deal.name, deal.owner) return deal + @frappe.whitelist() def get_deal_contacts(name): contacts = frappe.get_all( @@ -44,16 +25,19 @@ def get_deal_contacts(name): for contact in contacts: is_primary = contact.is_primary contact = frappe.get_doc("Contact", contact.contact).as_dict() + def get_primary_email(contact): for email in contact.email_ids: if email.is_primary: return email.email_id return contact.email_ids[0].email_id if contact.email_ids else "" + def get_primary_mobile_no(contact): for phone in contact.phone_nos: if phone.is_primary: return phone.phone return contact.phone_nos[0].phone if contact.phone_nos else "" + _contact = { "name": contact.name, "image": contact.image, @@ -63,4 +47,4 @@ def get_deal_contacts(name): "is_primary": is_primary, } deal_contacts.append(_contact) - return deal_contacts \ No newline at end of file + return deal_contacts diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.json b/crm/fcrm/doctype/crm_deal/crm_deal.json index 34a22e35..133630ce 100644 --- a/crm/fcrm/doctype/crm_deal/crm_deal.json +++ b/crm/fcrm/doctype/crm_deal/crm_deal.json @@ -73,7 +73,7 @@ "fetch_from": ".annual_revenue", "fieldname": "annual_revenue", "fieldtype": "Currency", - "label": "Amount", + "label": "Annual Revenue", "options": "currency" }, { @@ -338,7 +338,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-09-17 18:34:15.873610", + "modified": "2024-12-11 14:31:41.058895", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Deal", diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json index 00012770..d31df954 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json @@ -27,7 +27,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Type", - "options": "Quick Entry\nSide Panel" + "options": "Quick Entry\nSide Panel\nData Fields" }, { "fieldname": "section_break_ttpm", @@ -46,7 +46,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-06-13 15:10:01.612851", + "modified": "2024-12-05 13:29:37.021412", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Fields Layout", diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py index 6926d0b2..0d06ca43 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import json + import frappe from frappe import _ from frappe.model.document import Document @@ -10,46 +11,54 @@ from frappe.model.document import Document class CRMFieldsLayout(Document): pass + @frappe.whitelist() def get_fields_layout(doctype: str, type: str): - sections = [] + tabs = [] if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}): layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type}) else: return [] if layout.layout: - sections = json.loads(layout.layout) + tabs = json.loads(layout.layout) + + has_tabs = tabs[0].get("sections") if tabs and tabs[0] else False + + if not has_tabs: + tabs = [{"no_tabs": True, "sections": tabs}] allowed_fields = [] - for section in sections: - if not section.get("fields"): - continue - allowed_fields.extend(section.get("fields")) + for tab in tabs: + for section in tab.get("sections"): + if not section.get("fields"): + continue + allowed_fields.extend(section.get("fields")) fields = frappe.get_meta(doctype).fields fields = [field for field in fields if field.fieldname in allowed_fields] - for section in sections: - for field in section.get("fields") if section.get("fields") else []: - field = next((f for f in fields if f.fieldname == field), None) - if field: - if field.fieldtype == "Select" and field.options: - field.options = field.options.split("\n") - field.options = [{"label": _(option), "value": option} for option in field.options] - field.options.insert(0, {"label": "", "value": ""}) - field = { - "label": _(field.label), - "name": field.fieldname, - "type": field.fieldtype, - "options": field.options, - "mandatory": field.reqd, - "placeholder": field.get("placeholder"), - "filters": field.get("link_filters") - } - section["fields"][section.get("fields").index(field["name"])] = field + for tab in tabs: + for section in tab.get("sections"): + for field in section.get("fields") if section.get("fields") else []: + field = next((f for f in fields if f.fieldname == field), None) + if field: + if field.fieldtype == "Select" and field.options: + field.options = field.options.split("\n") + field.options = [{"label": _(option), "value": option} for option in field.options] + field.options.insert(0, {"label": "", "value": ""}) + field = { + "label": _(field.label), + "name": field.fieldname, + "type": field.fieldtype, + "options": field.options, + "mandatory": field.reqd, + "placeholder": field.get("placeholder"), + "filters": field.get("link_filters"), + } + section["fields"][section.get("fields").index(field["name"])] = field - return sections or [] + return tabs or [] @frappe.whitelist() @@ -59,11 +68,13 @@ def save_fields_layout(doctype: str, type: str, layout: str): else: doc = frappe.new_doc("CRM Fields Layout") - doc.update({ - "dt": doctype, - "type": type, - "layout": layout, - }) + doc.update( + { + "dt": doctype, + "type": type, + "layout": layout, + } + ) doc.save(ignore_permissions=True) return doc.layout diff --git a/crm/fcrm/doctype/crm_lead/api.py b/crm/fcrm/doctype/crm_lead/api.py index e1bb4a4f..2ecaf112 100644 --- a/crm/fcrm/doctype/crm_lead/api.py +++ b/crm/fcrm/doctype/crm_lead/api.py @@ -1,22 +1,14 @@ import frappe -from frappe import _ -from crm.api.doc import get_fields_meta, get_assigned_users +from crm.api.doc import get_assigned_users, get_fields_meta from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script + @frappe.whitelist() def get_lead(name): - Lead = frappe.qb.DocType("CRM Lead") + lead = frappe.get_doc("CRM Lead", name).as_dict() - query = frappe.qb.from_(Lead).select("*").where(Lead.name == name).limit(1) - - lead = query.run(as_dict=True) - if not len(lead): - frappe.throw(_("Lead not found"), frappe.DoesNotExistError) - lead = lead.pop() - - lead["doctype"] = "CRM Lead" lead["fields_meta"] = get_fields_meta("CRM Lead") - lead["_form_script"] = get_form_script('CRM Lead') + lead["_form_script"] = get_form_script("CRM Lead") lead["_assign"] = get_assigned_users("CRM Lead", lead.name, lead.owner) return lead diff --git a/crm/install.py b/crm/install.py index 594b1ea5..4da0c359 100644 --- a/crm/install.py +++ b/crm/install.py @@ -1,14 +1,14 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals import click import frappe - from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + def before_install(): pass + def after_install(force=False): add_default_lead_statuses() add_default_deal_statuses() @@ -20,6 +20,7 @@ def after_install(force=False): add_default_lead_sources() frappe.db.commit() + def add_default_lead_statuses(): statuses = { "New": { @@ -58,6 +59,7 @@ def add_default_lead_statuses(): doc.position = statuses[status]["position"] doc.insert() + def add_default_deal_statuses(): statuses = { "Qualification": { @@ -100,6 +102,7 @@ def add_default_deal_statuses(): doc.position = statuses[status]["position"] doc.insert() + def add_default_communication_statuses(): statuses = ["Open", "Replied"] @@ -111,46 +114,58 @@ def add_default_communication_statuses(): doc.status = status doc.insert() + def add_default_fields_layout(force=False): quick_entry_layouts = { "CRM Lead-Quick Entry": { "doctype": "CRM Lead", - "layout": '[{"label":"Person","fields":["salutation","first_name","last_name","email","mobile_no", "gender"],"hideLabel":true},{"label":"Organization","fields":["organization","website","no_of_employees","territory","annual_revenue","industry"],"hideLabel":true,"hideBorder":false},{"label":"Other","columns":2,"fields":["status","lead_owner"],"hideLabel":true,"hideBorder":false}]' + "layout": '[{"label":"Person","fields":["salutation","first_name","last_name","email","mobile_no", "gender"],"hideLabel":true},{"label":"Organization","fields":["organization","website","no_of_employees","territory","annual_revenue","industry"],"hideLabel":true,"hideBorder":false},{"label":"Other","columns":2,"fields":["status","lead_owner"],"hideLabel":true,"hideBorder":false}]', }, "CRM Deal-Quick Entry": { "doctype": "CRM Deal", - "layout": '[{"label": "Select Organization", "fields": ["organization"], "hideLabel": true, "editable": true}, {"label": "Organization Details", "fields": ["organization_name", "website", "no_of_employees", "territory", "annual_revenue", "industry"], "hideLabel": true, "editable": true}, {"label": "Select Contact", "fields": ["contact"], "hideLabel": true, "editable": true}, {"label": "Contact Details", "fields": ["salutation", "first_name", "last_name", "email", "mobile_no", "gender"], "hideLabel": true, "editable": true}, {"label": "Other", "columns": 2, "fields": ["status", "deal_owner"], "hideLabel": true}]' + "layout": '[{"label": "Select Organization", "fields": ["organization"], "hideLabel": true, "editable": true}, {"label": "Organization Details", "fields": ["organization_name", "website", "no_of_employees", "territory", "annual_revenue", "industry"], "hideLabel": true, "editable": true}, {"label": "Select Contact", "fields": ["contact"], "hideLabel": true, "editable": true}, {"label": "Contact Details", "fields": ["salutation", "first_name", "last_name", "email", "mobile_no", "gender"], "hideLabel": true, "editable": true}, {"label": "Other", "columns": 2, "fields": ["status", "deal_owner"], "hideLabel": true}]', }, "Contact-Quick Entry": { "doctype": "Contact", - "layout": '[{"label":"Salutation","columns":1,"fields":["salutation"],"hideLabel":true},{"label":"Full Name","columns":2,"hideBorder":true,"fields":["first_name","last_name"],"hideLabel":true},{"label":"Email","columns":1,"hideBorder":true,"fields":["email_id"],"hideLabel":true},{"label":"Mobile No. & Gender","columns":2,"hideBorder":true,"fields":["mobile_no","gender"],"hideLabel":true},{"label":"Organization","columns":1,"hideBorder":true,"fields":["company_name"],"hideLabel":true},{"label":"Designation","columns":1,"hideBorder":true,"fields":["designation"],"hideLabel":true},{"label":"Address","columns":1,"hideBorder":true,"fields":["address"],"hideLabel":true}]' + "layout": '[{"label":"Salutation","columns":1,"fields":["salutation"],"hideLabel":true},{"label":"Full Name","columns":2,"hideBorder":true,"fields":["first_name","last_name"],"hideLabel":true},{"label":"Email","columns":1,"hideBorder":true,"fields":["email_id"],"hideLabel":true},{"label":"Mobile No. & Gender","columns":2,"hideBorder":true,"fields":["mobile_no","gender"],"hideLabel":true},{"label":"Organization","columns":1,"hideBorder":true,"fields":["company_name"],"hideLabel":true},{"label":"Designation","columns":1,"hideBorder":true,"fields":["designation"],"hideLabel":true},{"label":"Address","columns":1,"hideBorder":true,"fields":["address"],"hideLabel":true}]', }, "CRM Organization-Quick Entry": { "doctype": "CRM Organization", - "layout": '[{"label":"Organization Name","columns":1,"fields":["organization_name"],"hideLabel":true},{"label":"Website & Revenue","columns":2,"hideBorder":true,"fields":["website","annual_revenue"],"hideLabel":true},{"label":"Territory","columns":1,"hideBorder":true,"fields":["territory"],"hideLabel":true},{"label":"No of Employees & Industry","columns":2,"hideBorder":true,"fields":["no_of_employees","industry"],"hideLabel":true},{"label":"Address","columns":1,"hideBorder":true,"fields":["address"],"hideLabel":true}]' + "layout": '[{"label":"Organization Name","columns":1,"fields":["organization_name"],"hideLabel":true},{"label":"Website & Revenue","columns":2,"hideBorder":true,"fields":["website","annual_revenue"],"hideLabel":true},{"label":"Territory","columns":1,"hideBorder":true,"fields":["territory"],"hideLabel":true},{"label":"No of Employees & Industry","columns":2,"hideBorder":true,"fields":["no_of_employees","industry"],"hideLabel":true},{"label":"Address","columns":1,"hideBorder":true,"fields":["address"],"hideLabel":true}]', }, "Address-Quick Entry": { "doctype": "Address", - "layout": '[{"label":"Address","columns":1,"fields":["address_title","address_type","address_line1","address_line2","city","state","country","pincode"],"hideLabel":true}]' + "layout": '[{"label":"Address","columns":1,"fields":["address_title","address_type","address_line1","address_line2","city","state","country","pincode"],"hideLabel":true}]', }, } sidebar_fields_layouts = { "CRM Lead-Side Panel": { "doctype": "CRM Lead", - "layout": '[{"label": "Details", "name": "details", "opened": true, "fields": ["organization", "website", "territory", "industry", "job_title", "source", "lead_owner"]}, {"label": "Person", "name": "person_tab", "opened": true, "fields": ["salutation", "first_name", "last_name", "email", "mobile_no"]}]' + "layout": '[{"label": "Details", "name": "details", "opened": true, "fields": ["organization", "website", "territory", "industry", "job_title", "source", "lead_owner"]}, {"label": "Person", "name": "person_tab", "opened": true, "fields": ["salutation", "first_name", "last_name", "email", "mobile_no"]}]', }, "CRM Deal-Side Panel": { "doctype": "CRM Deal", - "layout": '[{"label":"Contacts","name":"contacts_section","opened":true,"editable":false,"contacts":[]},{"label":"Organization Details","name":"organization_tab","opened":true,"fields":["organization","website","territory","annual_revenue","close_date","probability","next_step","deal_owner"]}]' + "layout": '[{"label":"Contacts","name":"contacts_section","opened":true,"editable":false,"contacts":[]},{"label":"Organization Details","name":"organization_tab","opened":true,"fields":["organization","website","territory","annual_revenue","close_date","probability","next_step","deal_owner"]}]', }, "Contact-Side Panel": { "doctype": "Contact", - "layout": '[{"label":"Details","name":"details","opened":true,"fields":["salutation","first_name","last_name","email_id","mobile_no","gender","company_name","designation","address"]}]' + "layout": '[{"label":"Details","name":"details","opened":true,"fields":["salutation","first_name","last_name","email_id","mobile_no","gender","company_name","designation","address"]}]', }, "CRM Organization-Side Panel": { "doctype": "CRM Organization", - "layout": '[{"label":"Details","name":"details","opened":true,"fields":["organization_name","website","territory","industry","no_of_employees","address"]}]' + "layout": '[{"label":"Details","name":"details","opened":true,"fields":["organization_name","website","territory","industry","no_of_employees","address"]}]', + }, + } + + data_fields_layouts = { + "CRM Lead-Data Fields": { + "doctype": "CRM Lead", + "layout": '[{"no_tabs":true,"sections":[{"label": "Details", "name": "details", "opened": true, "fields": ["organization", "website", "territory", "industry", "job_title", "source", "lead_owner"]}, {"label": "Person", "name": "person_tab", "opened": true, "fields": ["salutation", "first_name", "last_name", "email", "mobile_no"]}]}]', + }, + "CRM Deal-Data Fields": { + "doctype": "CRM Deal", + "layout": '[{"no_tabs":true,"sections":[{"label":"Organization Details","name":"organization_tab","opened":true,"fields":["organization","website","territory","annual_revenue","close_date","probability","next_step","deal_owner"]}]}]', }, } @@ -180,6 +195,20 @@ def add_default_fields_layout(force=False): doc.layout = sidebar_fields_layouts[layout]["layout"] doc.insert() + for layout in data_fields_layouts: + if frappe.db.exists("CRM Fields Layout", layout): + if force: + frappe.delete_doc("CRM Fields Layout", layout) + else: + continue + + doc = frappe.new_doc("CRM Fields Layout") + doc.type = "Data Fields" + doc.dt = data_fields_layouts[layout]["doctype"] + doc.layout = data_fields_layouts[layout]["layout"] + doc.insert() + + def add_property_setter(): if not frappe.db.exists("Property Setter", {"name": "Contact-main-search_fields"}): doc = frappe.new_doc("Property Setter") @@ -190,6 +219,7 @@ def add_property_setter(): doc.value = "email_id" doc.insert() + def add_email_template_custom_fields(): if not frappe.get_meta("Email Template").has_field("enabled"): click.secho("* Installing Custom Fields in Email Template") @@ -219,7 +249,59 @@ def add_email_template_custom_fields(): def add_default_industries(): - industries = ["Accounting", "Advertising", "Aerospace", "Agriculture", "Airline", "Apparel & Accessories", "Automotive", "Banking", "Biotechnology", "Broadcasting", "Brokerage", "Chemical", "Computer", "Consulting", "Consumer Products", "Cosmetics", "Defense", "Department Stores", "Education", "Electronics", "Energy", "Entertainment & Leisure, Executive Search", "Financial Services", "Food", "Beverage & Tobacco", "Grocery", "Health Care", "Internet Publishing", "Investment Banking", "Legal", "Manufacturing", "Motion Picture & Video", "Music", "Newspaper Publishers", "Online Auctions", "Pension Funds", "Pharmaceuticals", "Private Equity", "Publishing", "Real Estate", "Retail & Wholesale", "Securities & Commodity Exchanges", "Service", "Soap & Detergent", "Software", "Sports", "Technology", "Telecommunications", "Television", "Transportation", "Venture Capital"] + industries = [ + "Accounting", + "Advertising", + "Aerospace", + "Agriculture", + "Airline", + "Apparel & Accessories", + "Automotive", + "Banking", + "Biotechnology", + "Broadcasting", + "Brokerage", + "Chemical", + "Computer", + "Consulting", + "Consumer Products", + "Cosmetics", + "Defense", + "Department Stores", + "Education", + "Electronics", + "Energy", + "Entertainment & Leisure, Executive Search", + "Financial Services", + "Food", + "Beverage & Tobacco", + "Grocery", + "Health Care", + "Internet Publishing", + "Investment Banking", + "Legal", + "Manufacturing", + "Motion Picture & Video", + "Music", + "Newspaper Publishers", + "Online Auctions", + "Pension Funds", + "Pharmaceuticals", + "Private Equity", + "Publishing", + "Real Estate", + "Retail & Wholesale", + "Securities & Commodity Exchanges", + "Service", + "Soap & Detergent", + "Software", + "Sports", + "Technology", + "Telecommunications", + "Television", + "Transportation", + "Venture Capital", + ] for industry in industries: if frappe.db.exists("CRM Industry", industry): @@ -231,7 +313,18 @@ def add_default_industries(): def add_default_lead_sources(): - lead_sources = ["Existing Customer", "Reference", "Advertisement", "Cold Calling", "Exhibition", "Supplier Reference", "Mass Mailing", "Customer's Vendor", "Campaign", "Walk In"] + lead_sources = [ + "Existing Customer", + "Reference", + "Advertisement", + "Cold Calling", + "Exhibition", + "Supplier Reference", + "Mass Mailing", + "Customer's Vendor", + "Campaign", + "Walk In", + ] for source in lead_sources: if frappe.db.exists("CRM Lead Source", source): diff --git a/crm/integrations/twilio/api.py b/crm/integrations/twilio/api.py index 1ea4d67d..5ed4c28e 100644 --- a/crm/integrations/twilio/api.py +++ b/crm/integrations/twilio/api.py @@ -130,14 +130,14 @@ def update_call_status_info(**kwargs): def get_datetime_from_timestamp(timestamp): from datetime import datetime - from pytz import timezone + from zoneinfo import ZoneInfo if not timestamp: return None datetime_utc_tz_str = timestamp.strftime('%Y-%m-%d %H:%M:%S%z') datetime_utc_tz = datetime.strptime(datetime_utc_tz_str, '%Y-%m-%d %H:%M:%S%z') system_timezone = frappe.utils.get_system_timezone() - converted_datetime = datetime_utc_tz.astimezone(timezone(system_timezone)) + converted_datetime = datetime_utc_tz.astimezone(ZoneInfo(system_timezone)) return frappe.utils.format_datetime(converted_datetime, 'yyyy-MM-dd HH:mm:ss') @frappe.whitelist() @@ -179,4 +179,4 @@ def get_lead_or_deal_from_number(call): if not doc: doc = find_record(doctype, number) - return doc, doctype \ No newline at end of file + return doc, doctype diff --git a/crm/patches.txt b/crm/patches.txt index b14faf13..a0e91a3d 100644 --- a/crm/patches.txt +++ b/crm/patches.txt @@ -6,6 +6,6 @@ crm.patches.v1_0.move_crm_note_data_to_fcrm_note [post_model_sync] # Patches added in this section will be executed after doctypes are migrated crm.patches.v1_0.create_email_template_custom_fields -crm.patches.v1_0.create_default_fields_layout #31/10/2024 +crm.patches.v1_0.create_default_fields_layout #10/12/2024 crm.patches.v1_0.create_default_sidebar_fields_layout crm.patches.v1_0.update_deal_quick_entry_layout \ No newline at end of file diff --git a/crm/www/crm.py b/crm/www/crm.py index 22b2ed58..04b14037 100644 --- a/crm/www/crm.py +++ b/crm/www/crm.py @@ -3,7 +3,14 @@ from __future__ import unicode_literals import frappe +from frappe.utils import cint, get_system_timezone from frappe.utils.telemetry import capture +from frappe.locale import ( + get_date_format, + get_first_day_of_the_week, + get_number_format, + get_time_format, +) no_cache = 1 @@ -32,6 +39,20 @@ def get_boot(): "site_name": frappe.local.site, "read_only_mode": frappe.flags.read_only, "csrf_token": frappe.sessions.get_csrf_token(), + "setup_complete": cint(frappe.get_system_settings("setup_complete")), + "sysdefaults": { + "float_precision": cint(frappe.get_system_settings("float_precision")) + or 3, + "date_format": get_date_format(), + "time_format": get_time_format(), + "first_day_of_the_week": get_first_day_of_the_week(), + "number_format": get_number_format().string, + }, + "timezone": { + "system": get_system_timezone(), + "user": frappe.db.get_value("User", frappe.session.user, "time_zone") + or get_system_timezone(), + }, } ) diff --git a/frappe-ui b/frappe-ui index b2dbd419..5a4f3c8d 160000 --- a/frappe-ui +++ b/frappe-ui @@ -1 +1 @@ -Subproject commit b2dbd41936905aa46b18d3c22e5d09a7b08a9b98 +Subproject commit 5a4f3c8d4f12efba37b9a83a51a59b53fa758be0 diff --git a/frontend/package.json b/frontend/package.json index 56683963..2782be37 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,7 @@ "@vueuse/core": "^10.3.0", "@vueuse/integrations": "^10.3.0", "feather-icons": "^4.28.0", - "frappe-ui": "^0.1.71", + "frappe-ui": "^0.1.91", "gemoji": "^8.1.0", "lodash": "^4.17.21", "mime": "^4.0.1", @@ -22,7 +22,6 @@ "socket.io-client": "^4.7.2", "sortablejs": "^1.15.0", "tailwindcss": "^3.3.3", - "vite": "^4.4.9", "vue": "^3.4.12", "vue-router": "^4.2.2", "vuedraggable": "^4.1.0" diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 61e774c9..02c42926 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -9,14 +9,14 @@ diff --git a/frontend/src/assets/Inter/Inter-Black.woff b/frontend/src/assets/Inter/Inter-Black.woff deleted file mode 100644 index c7737ed3..00000000 Binary files a/frontend/src/assets/Inter/Inter-Black.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Black.woff2 b/frontend/src/assets/Inter/Inter-Black.woff2 deleted file mode 100644 index b16b995b..00000000 Binary files a/frontend/src/assets/Inter/Inter-Black.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BlackItalic.woff b/frontend/src/assets/Inter/Inter-BlackItalic.woff deleted file mode 100644 index b5f14476..00000000 Binary files a/frontend/src/assets/Inter/Inter-BlackItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BlackItalic.woff2 b/frontend/src/assets/Inter/Inter-BlackItalic.woff2 deleted file mode 100644 index a3f1b70c..00000000 Binary files a/frontend/src/assets/Inter/Inter-BlackItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Bold.woff b/frontend/src/assets/Inter/Inter-Bold.woff deleted file mode 100644 index e3845558..00000000 Binary files a/frontend/src/assets/Inter/Inter-Bold.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Bold.woff2 b/frontend/src/assets/Inter/Inter-Bold.woff2 deleted file mode 100644 index 835dd497..00000000 Binary files a/frontend/src/assets/Inter/Inter-Bold.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BoldItalic.woff b/frontend/src/assets/Inter/Inter-BoldItalic.woff deleted file mode 100644 index ffac3f59..00000000 Binary files a/frontend/src/assets/Inter/Inter-BoldItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BoldItalic.woff2 b/frontend/src/assets/Inter/Inter-BoldItalic.woff2 deleted file mode 100644 index 1a41a14f..00000000 Binary files a/frontend/src/assets/Inter/Inter-BoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBold.woff b/frontend/src/assets/Inter/Inter-ExtraBold.woff deleted file mode 100644 index 885ac94f..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBold.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBold.woff2 b/frontend/src/assets/Inter/Inter-ExtraBold.woff2 deleted file mode 100644 index ae956b15..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBold.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff b/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff deleted file mode 100644 index d6cf8623..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2 b/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2 deleted file mode 100644 index 86578995..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLight.woff b/frontend/src/assets/Inter/Inter-ExtraLight.woff deleted file mode 100644 index ff769193..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLight.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLight.woff2 b/frontend/src/assets/Inter/Inter-ExtraLight.woff2 deleted file mode 100644 index 694b2df9..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLight.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff b/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff deleted file mode 100644 index c6ed13a4..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2 b/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2 deleted file mode 100644 index 9a7bd110..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Italic.woff b/frontend/src/assets/Inter/Inter-Italic.woff deleted file mode 100644 index 4fdb59dc..00000000 Binary files a/frontend/src/assets/Inter/Inter-Italic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Italic.woff2 b/frontend/src/assets/Inter/Inter-Italic.woff2 deleted file mode 100644 index deca637d..00000000 Binary files a/frontend/src/assets/Inter/Inter-Italic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Light.woff b/frontend/src/assets/Inter/Inter-Light.woff deleted file mode 100644 index 42850acc..00000000 Binary files a/frontend/src/assets/Inter/Inter-Light.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Light.woff2 b/frontend/src/assets/Inter/Inter-Light.woff2 deleted file mode 100644 index 65a7dadd..00000000 Binary files a/frontend/src/assets/Inter/Inter-Light.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-LightItalic.woff b/frontend/src/assets/Inter/Inter-LightItalic.woff deleted file mode 100644 index c4ed9a94..00000000 Binary files a/frontend/src/assets/Inter/Inter-LightItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-LightItalic.woff2 b/frontend/src/assets/Inter/Inter-LightItalic.woff2 deleted file mode 100644 index 555fc559..00000000 Binary files a/frontend/src/assets/Inter/Inter-LightItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Medium.woff b/frontend/src/assets/Inter/Inter-Medium.woff deleted file mode 100644 index 495faef7..00000000 Binary files a/frontend/src/assets/Inter/Inter-Medium.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Medium.woff2 b/frontend/src/assets/Inter/Inter-Medium.woff2 deleted file mode 100644 index 871ce4ce..00000000 Binary files a/frontend/src/assets/Inter/Inter-Medium.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-MediumItalic.woff b/frontend/src/assets/Inter/Inter-MediumItalic.woff deleted file mode 100644 index 389c7a2b..00000000 Binary files a/frontend/src/assets/Inter/Inter-MediumItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-MediumItalic.woff2 b/frontend/src/assets/Inter/Inter-MediumItalic.woff2 deleted file mode 100644 index aa805799..00000000 Binary files a/frontend/src/assets/Inter/Inter-MediumItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Regular.woff b/frontend/src/assets/Inter/Inter-Regular.woff deleted file mode 100644 index fa7715d1..00000000 Binary files a/frontend/src/assets/Inter/Inter-Regular.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Regular.woff2 b/frontend/src/assets/Inter/Inter-Regular.woff2 deleted file mode 100644 index b52dd0a0..00000000 Binary files a/frontend/src/assets/Inter/Inter-Regular.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBold.woff b/frontend/src/assets/Inter/Inter-SemiBold.woff deleted file mode 100644 index 18d7749f..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBold.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBold.woff2 b/frontend/src/assets/Inter/Inter-SemiBold.woff2 deleted file mode 100644 index ece5204a..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBold.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff b/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff deleted file mode 100644 index 8ee64396..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2 b/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2 deleted file mode 100644 index b32c0ba3..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Thin.woff b/frontend/src/assets/Inter/Inter-Thin.woff deleted file mode 100644 index 1a22286f..00000000 Binary files a/frontend/src/assets/Inter/Inter-Thin.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Thin.woff2 b/frontend/src/assets/Inter/Inter-Thin.woff2 deleted file mode 100644 index c56bc7ca..00000000 Binary files a/frontend/src/assets/Inter/Inter-Thin.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ThinItalic.woff b/frontend/src/assets/Inter/Inter-ThinItalic.woff deleted file mode 100644 index d8ec8373..00000000 Binary files a/frontend/src/assets/Inter/Inter-ThinItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ThinItalic.woff2 b/frontend/src/assets/Inter/Inter-ThinItalic.woff2 deleted file mode 100644 index eca5608c..00000000 Binary files a/frontend/src/assets/Inter/Inter-ThinItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-italic.var.woff2 b/frontend/src/assets/Inter/Inter-italic.var.woff2 deleted file mode 100644 index 1f5d9261..00000000 Binary files a/frontend/src/assets/Inter/Inter-italic.var.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-roman.var.woff2 b/frontend/src/assets/Inter/Inter-roman.var.woff2 deleted file mode 100644 index 05621d8d..00000000 Binary files a/frontend/src/assets/Inter/Inter-roman.var.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter.var.woff2 b/frontend/src/assets/Inter/Inter.var.woff2 deleted file mode 100644 index 46bb5153..00000000 Binary files a/frontend/src/assets/Inter/Inter.var.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/inter.css b/frontend/src/assets/Inter/inter.css deleted file mode 100644 index 3ca1bbf6..00000000 --- a/frontend/src/assets/Inter/inter.css +++ /dev/null @@ -1,152 +0,0 @@ -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 100; - font-display: swap; - src: url("Inter-Thin.woff2?v=3.12") format("woff2"), - url("Inter-Thin.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 100; - font-display: swap; - src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"), - url("Inter-ThinItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 200; - font-display: swap; - src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"), - url("Inter-ExtraLight.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 200; - font-display: swap; - src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"), - url("Inter-ExtraLightItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 300; - font-display: swap; - src: url("Inter-Light.woff2?v=3.12") format("woff2"), - url("Inter-Light.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 300; - font-display: swap; - src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"), - url("Inter-LightItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url("Inter-Regular.woff2?v=3.12") format("woff2"), - url("Inter-Regular.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 400; - font-display: swap; - src: url("Inter-Italic.woff2?v=3.12") format("woff2"), - url("Inter-Italic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url("Inter-Medium.woff2?v=3.12") format("woff2"), - url("Inter-Medium.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 500; - font-display: swap; - src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"), - url("Inter-MediumItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"), - url("Inter-SemiBold.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 600; - font-display: swap; - src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"), - url("Inter-SemiBoldItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url("Inter-Bold.woff2?v=3.12") format("woff2"), - url("Inter-Bold.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 700; - font-display: swap; - src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"), - url("Inter-BoldItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 800; - font-display: swap; - src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"), - url("Inter-ExtraBold.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 800; - font-display: swap; - src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"), - url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 900; - font-display: swap; - src: url("Inter-Black.woff2?v=3.12") format("woff2"), - url("Inter-Black.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 900; - font-display: swap; - src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"), - url("Inter-BlackItalic.woff?v=3.12") format("woff"); -} diff --git a/frontend/src/components/Activities/Activities.vue b/frontend/src/components/Activities/Activities.vue index 2a3f3dd6..baf28de0 100644 --- a/frontend/src/components/Activities/Activities.vue +++ b/frontend/src/components/Activities/Activities.vue @@ -16,7 +16,7 @@ >
{{ __('Loading...') }} @@ -50,13 +50,13 @@ class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-2 px-3 sm:gap-4 sm:px-10" >
- +
@@ -72,15 +72,15 @@ class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-3 sm:px-10" >
@@ -185,10 +185,10 @@ >
{{ activity.owner_name }} - {{ __(activity.data.type) }} + {{ __(activity.data.type) }}
- -
+ +
{{ __(timeAgo(activity.creation)) }}
@@ -225,7 +225,7 @@
{{ activity.show_others ? __('Hide') : __('Show') }} +{{ activity.other_versions.length + 1 }} @@ -243,22 +243,22 @@
- + {{ activity.owner_name }} {{ __(activity.type) }} {{ __(activity.data.field_label) }} {{ __(activity.value) }}
{{ __('to') }}
- -
+ +
{{ __(timeAgo(activity.creation)) }}
@@ -305,23 +305,23 @@ v-for="activity in [activity, ...activity.other_versions]" class="flex items-start justify-stretch gap-2 py-1.5 text-base" > -
+
{{ __(activity.data.field_label) }} {{ startCase(__(activity.type)) }}
{{ __('to') }}
- -
+ +
{{ __(timeAgo(activity.creation)) }}
@@ -366,9 +364,12 @@
+
+ +
{{ __(emptyText) }} @@ -456,9 +457,11 @@ import CallArea from '@/components/Activities/CallArea.vue' import NoteArea from '@/components/Activities/NoteArea.vue' import TaskArea from '@/components/Activities/TaskArea.vue' import AttachmentArea from '@/components/Activities/AttachmentArea.vue' +import DataFields from '@/components/Activities/DataFields.vue' import UserAvatar from '@/components/UserAvatar.vue' import ActivityIcon from '@/components/Icons/ActivityIcon.vue' import Email2Icon from '@/components/Icons/Email2Icon.vue' +import DetailsIcon from '@/components/Icons/DetailsIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import NoteIcon from '@/components/Icons/NoteIcon.vue' import TaskIcon from '@/components/Icons/TaskIcon.vue' @@ -481,13 +484,7 @@ import CommunicationArea from '@/components/CommunicationArea.vue' import WhatsappTemplateSelectorModal from '@/components/Modals/WhatsappTemplateSelectorModal.vue' import AllModals from '@/components/Activities/AllModals.vue' import FilesUploader from '@/components/FilesUploader/FilesUploader.vue' -import { - timeAgo, - dateFormat, - dateTooltipFormat, - secondsToDuration, - startCase, -} from '@/utils' +import { timeAgo, formatDate, secondsToDuration, startCase } from '@/utils' import { globalStore } from '@/stores/global' import { usersStore } from '@/stores/users' import { contactsStore } from '@/stores/contacts' @@ -727,6 +724,8 @@ const emptyText = computed(() => { text = 'No Email Communications' } else if (title.value == 'Comments') { text = 'No Comments' + } else if (title.value == 'Data') { + text = 'No Data' } else if (title.value == 'Calls') { text = 'No Call Logs' } else if (title.value == 'Notes') { @@ -747,6 +746,8 @@ const emptyTextIcon = computed(() => { icon = Email2Icon } else if (title.value == 'Comments') { icon = CommentIcon + } else if (title.value == 'Data') { + icon = DetailsIcon } else if (title.value == 'Calls') { icon = PhoneIcon } else if (title.value == 'Notes') { @@ -758,7 +759,7 @@ const emptyTextIcon = computed(() => { } else if (title.value == 'WhatsApp') { icon = WhatsAppIcon } - return h(icon, { class: 'text-gray-500' }) + return h(icon, { class: 'text-ink-gray-4' }) }) function timelineIcon(activity_type, is_lead) { diff --git a/frontend/src/components/Activities/ActivityHeader.vue b/frontend/src/components/Activities/ActivityHeader.vue index 9f734d4e..78ad93a7 100644 --- a/frontend/src/components/Activities/ActivityHeader.vue +++ b/frontend/src/components/Activities/ActivityHeader.vue @@ -1,8 +1,9 @@