Commit Graph

999 Commits

Author SHA1 Message Date
Soham Kulkarni
3eaea74a51 Merge pull request #56656 from sokumon/revamp-workspaces
chore: exporting workspaces with sidebars
2026-07-02 05:39:24 +05:30
Mihir Kandoi
59b49120b7 fix(crm): keep returning None from create_customer on a linking failure
Preserve the pre-existing contract: create_customer returned None when contact/address linking failed.
The savepoint fix kept the Customer (good) but started returning its name in that case, so a CRM caller
treating a non-None return as full success could skip its retry/error handling. Return None on a linking
failure while still keeping the Customer. (greptile #56683)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:28:36 +05:30
Mihir Kandoi
76b31d9269 fix(crm): scope create_customer rollback so a contact/address failure keeps the Customer
create_customer wrapped customer.insert() + create_contacts() + create_address() in one try whose except
did a full frappe.db.rollback(), so a failure while linking contacts/address discarded the Customer just
created (MariaDB kept it pre-migration). Split the try: the customer insert keeps its full rollback (safe
-- nothing precedes it), and contact/address linking runs under a savepoint so its failure rolls back only
the links, preserving the Customer and healing the Postgres txn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:55:42 +05:30
sokumon
5a32866b93 chore: export more workspaces 2026-06-30 17:00:54 +05:30
Mihir Kandoi
460bb9e5d0 fix(crm): scope create_address rollback to a savepoint (review)
create_address is a helper called by create_prospect/create_customer AFTER they insert the Prospect/Customer. Its full frappe.db.rollback() on an address-save failure rolled back the caller's just-inserted parent doc, then swallowed the exception, so the caller returned a Prospect/Customer name that no longer existed. Scope the rollback to savepoint('crm_create_address') so only the address work is undone; the parent doc survives and the failed address is just logged.
2026-06-30 13:01:32 +05:30
Mihir Kandoi
f41e8208d8 fix(crm): savepoint Email Campaign send loop (Postgres)
send_mail (called per campaign schedule in a loop) inserts a Communication via make(); on failure the except calls frappe.log_error with no rollback, raising InFailedSqlTransaction on Postgres and poisoning subsequent sends. Savepoint before make() + rollback(save_point=) before log_error. No-op on MariaDB.
2026-06-29 22:48:28 +05:30
Mihir Kandoi
6b0f3cd243 fix(crm): rollback before logging in Frappe CRM webhook handlers (Postgres)
create_prospect/create_address/create_customer insert docs and on failure call frappe.log_error with no rollback; on Postgres (untrusted external CRM webhook input) a failed insert poisons the txn so log_error raises InFailedSqlTransaction. Full frappe.db.rollback() before each log_error. No-op on MariaDB.
2026-06-29 22:45:22 +05:30
Diptanil Saha
79ad11e21b chore(crm_settings): remove unused delete_custom_fields import (#56558) 2026-06-27 14:38:59 +05:30
Mihir Kandoi
b161e5aa79 chore: rewrite user-facing messages in Crm module
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:

- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
  extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings

Part of #53976.
2026-06-25 17:37:20 +05:30
Diptanil Saha
9b4c8a8d7f fix(crm): using get_list instead of get_all in get_opportunities (#56463) 2026-06-25 10:00:44 +00:00
Mihir Kandoi
6a4afd1733 fix: parse native JSON contacts args in CRM prospect/customer endpoints
create_prospect_against_crm_deal and create_customer read `contacts`
from form_dict (json.loads(doc.contacts) / customer_data.get("contacts")),
which arrives as a native list under JSON body mode. Use frappe.parse_json.
2026-06-24 20:55:01 +05:30
Mihir Kandoi
ffa85a4ed6 refactor: parse native JSON request args in crm/frappe_crm_api.py
Use frappe.parse_json instead of json.loads so the whitelisted endpoints
accept native JSON types (list/dict/bool) in addition to JSON strings.
2026-06-24 20:37:32 +05:30
Mihir Kandoi
50f2654eb1 refactor: parse native JSON request args in crm/doctype/opportunity/opportunity.py
Use frappe.parse_json instead of json.loads so the whitelisted endpoints
accept native JSON types (list/dict/bool) in addition to JSON strings.
2026-06-24 20:37:32 +05:30
Mihir Kandoi
dcc8d08521 refactor: parse native JSON request args in crm/doctype/contract_template/contract_template.py
Use frappe.parse_json instead of json.loads so the whitelisted endpoints
accept native JSON types (list/dict/bool) in addition to JSON strings.
2026-06-24 20:37:32 +05:30
Diptanil Saha
0ab812c3ec feat(crm_settings): enable frappe crm data synchronization (#56268) 2026-06-23 21:14:03 +05:30
Mihir Kandoi
b4aae9dea1 fix(crm): render Lead Details address consistently across engines
The Lead Details report concatenated address_line1 and address_line2 with
CONCAT_WS. An unfilled optional Data field is stored as '' on MariaDB but as
NULL on PostgreSQL; CONCAT_WS keeps the empty string (leaving a trailing
", ") on MariaDB while Postgres drops the NULL, so the same lead rendered a
different address on each engine.

Wrap both parts in NULLIF(part, '') so empty values are treated as NULL on
both engines: the report now produces the same clean address (no trailing
separator) everywhere.
2026-06-23 08:58:30 +05:30
Nabin Hait
43d2c7335d refactor(lead): name the loops in remove_link_from_prospect
The outer and inner loops both used 'd'; name them linked_prospect and lead
so the prospect/lead iteration reads clearly. No behaviour change.
2026-06-22 15:59:26 +05:30
Nabin Hait
8f69697212 fix(lead): don't crash deriving lead name when only ignore_mandatory is set
set_lead_name fell through to email_id.split('@') when a lead had no name,
company or email but ignore_mandatory was set (e.g. data import), raising
AttributeError on a None email. Only derive from email when one exists; the
lead name is then left blank, as intended for that path.
2026-06-22 15:59:25 +05:30
Nabin Hait
b4c9827318 Merge pull request #56295 from nabinhait/test-lead-coverage
test(lead): cover lead details and prospect sync/unlink
2026-06-22 14:05:35 +05:30
Nabin Hait
95b82eeba8 Merge pull request #56290 from nabinhait/test-opportunity-lost-flow
test(opportunity): improve coverage (lost flow, auto-close, item details, prospect sync)
2026-06-22 14:05:06 +05:30
Nabin Hait
19ba681e16 Merge pull request #56296 from nabinhait/opportunity-auto-close-default-days
fix(crm): drive opportunity auto-close days from CRM Settings, not a hardcoded fallback
2026-06-22 14:04:00 +05:30
Nabin Hait
f1b6a7d690 fix(crm): drive opportunity auto-close days from CRM Settings, not a hardcoded fallback
auto_close_opportunity fell back to 15 days in code when the CRM Setting was
blank (and its docstring still said 7). The field already defaults to 15, so
read the value straight from CRM Settings and add a patch to backfill 15 for
existing sites that left it blank, keeping the same auto-close schedule.
2026-06-22 13:05:50 +05:30
Nabin Hait
4c6b030a4b test(lead): cover lead details fetch and prospect sync/unlink
Add tests for get_lead_details and the Lead <-> Prospect lifecycle: editing a
lead syncs into its Prospect Lead row, and deleting the only lead of a
prospect removes the prospect. Lead controller coverage 65% -> 74%.
2026-06-22 12:50:25 +05:30
Nabin Hait
017e09eaac test(opportunity): cover item details, auto-close and prospect sync
Add tests for get_item_details, auto_close_opportunity (a stale Replied
opportunity is closed, a recent one is not) and the Opportunity -> Prospect
opportunity sync. Opportunity controller coverage 62% -> 80%.
2026-06-22 12:45:12 +05:30
Nabin Hait
61c2e7ad6e test(opportunity): cover the mark-as-lost flow
declare_enquiry_lost had almost no coverage. Add tests that marking an
Opportunity as lost records the lost reasons, competitors and detailed
reason and sets status to Lost, and that it is blocked when an active
(submitted) Quotation exists.
2026-06-22 12:33:19 +05:30
Diptanil Saha
c188ed59ec fix(lead): added missing read permission check on get_lead_details (#56272) 2026-06-21 21:46:53 +00:00
Mihir Kandoi
413ec60a3e refactor(postgres): address Greptile review on prospects report
- Fix N+1: the per-lead conversion issued 4 queries per lead (Opportunity, Quotation,
  Issue, Communication). Collect the reference documents for all leads in 3 bulk
  queries, then one Communication query per lead -> ~N+3 round-trips instead of 4N,
  matching the original single-query-per-lead cost.
- Constrain reference_doctype in the Communication lookup (names are unique only within
  a doctype), closing a latent cross-doctype name-collision gap the original also had.

Both-engine test still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:26:45 +05:30
Mihir Kandoi
5104007d12 refactor(postgres): port CRM module queries to the query builder
Convert the remaining raw frappe.db.sql in the CRM module to frappe.qb / ORM so the
queries run on PostgreSQL as well as MariaDB. Faithful conversions -- no MariaDB
behaviour change:

- opportunity.py, doctype/utils.py (get_last_interaction)
- reports: campaign_efficiency, first_response_time_for_opportunity (GROUP BY on the
  grouped Date(creation) + Avg -- Postgres-valid), lead_conversion_time,
  prospects_engaged_but_not_converted

lead_conversion_time also keeps the IS NOT NULL communication-date guard (forward-port
of the fix already on develop). Also drops invalid backtick notation from two get_all
order_by clauses in doctype/utils.py (order_by="`creation` DESC"), which frappe's
query engine rejects -- a latent failure on both engines, surfaced by the new test.

Tests: existing opportunity suite plus new both-engine tests for the four previously
untested reports/utils. All green on MariaDB and PostgreSQL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:15:20 +05:30
Mihir Kandoi
1f4702bde7 fix(crm): skip rows with no first-contact date in lead conversion time
Address review (#56105): when there's no matching communication, first_contact is
None and date_diff(invoice_date, None) treats None as today, giving a wrong
(negative) duration. Skip the entry instead, mirroring the communication_count guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:04:33 +05:30
Mihir Kandoi
5787951ed1 fix(crm): guard first_contact result before indexing (lead conversion time)
Address review (#56105): the IS NOT NULL guard can return no rows (the count above
filters on sender, this query on recipients), so [0][0] would raise IndexError.
Fall back to None when empty, matching the prior behaviour for that case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 20:50:31 +05:30
Mihir Kandoi
cfa6d286ad fix(postgres): other-class parity fixes (case-folding & null-ordering) for merged queries
Parity sweep findings in queries already merged in develop:

- pos_invoice.py: serial_no return-match matched case-sensitively on Postgres (case-insensitive
  on MariaDB). Replaced the get_all or_filters lookup with a qb query that case-folds both sides
  via Lower() (no-op on MariaDB).
- controllers/queries.py + pick_list.py: the Locate()-based relevance ranking in link-query
  ORDER BY is case-sensitive on Postgres (Strpos) vs case-insensitive on MariaDB (Locate), so
  autocomplete order differed. Lower() both arguments so the ranking matches on both engines.
- crm/lead_conversion_time.py: "first contact" used ORDER BY communication_date LIMIT 1 read by
  index; a NULL date sorts first on MariaDB but last on Postgres, changing the result. Added
  `communication_date IS NOT NULL` so both engines return the earliest real contact date.

Verified on MariaDB and Postgres: test_pick_list 40/40, test_pos_invoice 26/26 on both engines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 20:36:30 +05:30
Mihir Kandoi
0b795a628f fix(postgres): use portable GroupConcat in Lost Opportunity report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:19:16 +05:30
Mihir Kandoi
8a566e6ba5 fix(postgres): satisfy strict GROUP BY in Sales Pipeline Analytics report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:00 +05:30
Raffael Meyer
93d6be2ed7 fix(Lead): stop storing Gravatar image URLs for Leads (#55880) 2026-06-13 19:03:29 +02:00
rohitwaghchaure
bd60a9be90 Merge pull request #55849 from rohitwaghchaure/fixed-permissions-for-whitelist-functions
fix: permission for whitelist functions
2026-06-13 18:36:46 +05:30
Rohit Waghchaure
a64466561f fix: permission for whitelist functions 2026-06-13 17:45:37 +05:30
Diptanil Saha
c933e34914 fix: opportunity creation from contact us page (#55841) 2026-06-13 04:47:45 +00:00
sokumon
90aba582ec chore: export workspaces with new schema 2026-06-12 13:08:48 +05:30
ruthra kumar
d6ba0f0eca Merge pull request #55486 from Shllokkk/crm-create-customer-fix
Validations in CRM-api endpoints
2026-06-08 15:10:26 +05:30
Ankush Menat
ba936eefab fix: Add authorization checks on internal functions (#55709) 2026-06-08 14:49:32 +05:30
Nabin Hait
9b1157c914 fix: set options Email for customer_email field in appointment 2026-06-06 20:41:16 +05:30
Nabin Hait
530e587bf2 refactor: use mapper paths directly, drop re-export shims
Repoint all JS method strings and Python imports for mapper functions
across 18 doctypes from the doctype module to its mapper module, and
remove the now-unused re-export shims from each doctype file (keeping
only names used internally).
2026-06-01 18:17:56 +05:30
Shllokkk
e460e83516 fix: use new_doc with field allowlist in CRM integration endpoints 2026-05-31 18:42:26 +05:30
Nabin Hait
28c3d24b86 refactor(lead): move mapping functions to mapper.py 2026-05-29 13:26:31 +05:30
Nabin Hait
9b85773757 refactor(opportunity): move mapping functions to mapper.py 2026-05-29 13:23:56 +05:30
Raffael Meyer
992800f3dd fix: implement get_notification_email hook on Opportunity, Prospect and Customer (#54789) 2026-05-08 22:32:38 +02:00
Sakthivel Murugan S
a4a389bd41 fix(crm): handle empty _assign in appointment auto assignment (#54782) 2026-05-08 17:51:08 +05:30
Raffael Meyer
6f9f3d0a5c feat(Lead)!: send notifications to lead owner (#53959) 2026-05-08 12:40:23 +02:00
ruthra kumar
b16dd3f2dd refactor(test): remove explicit sql delete calls 2026-04-22 10:33:25 +05:30
Nihantra Patel
a2ec597e3d fix: set letter_head_for for letterhead 2026-04-20 16:58:35 +05:30