Skip to content

Conversation

@aadhilpm
Copy link
Contributor

@aadhilpm aadhilpm commented Dec 3, 2025

This PR introduces a Letter Management, allowing users to create consistent, professional, and reusable official letters directly from ERPNext.

Since the company issues many letters regularly, it is important to maintain a proper record. This ensures that all letters released from the company can be tracked, referenced, and retrieved when required.

Screen.Recording.2025-12-04.at.12.24.14.AM.mov

Features included

  • Letter Type master for categorizing letter formats
  • Letter Template (Jinja) with dynamic subject and content placeholders
  • Letter (Submittable) document with: Dynamic recipient (Customer / Supplier / Employee / Shareholder / Contact)
  • Template rendering into letter content
  • Printing and layout
  • Letterhead selection based on Company defaults
  • Print language support

Benefits
Standardized and consistent letter formats across all departments
Quick creation and printing without manual formatting
Centralized tracking of all letters released by the company

Usage

  1. Create a Letter Type
  2. Create a Jinja Letter Template
  3. Create a Letter and select recipient and template
  4. Print using the Letter Standard Print Format
letterscreenshot

Docs: https://docs.frappe.io/erpnext/letter

- Letter Type master for categorizing letter formats
- Letter Template (Jinja) with subject and content placeholders
- Letter (Submittable) with dynamic recipient (Customer / Supplier / Employee / Shareholder / Contact)
- Auto-fetch recipient name and address
- Company and recipient address display formatting
- Letter template rendering into letter content
- Letterhead selection from company defaults
- Minimal professional print format (letterhead + footer)

This makes creating, formatting and printing official letters fast and consistent, without manual formatting each time.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

📝 Walkthrough

Walkthrough

Adds a new Letter feature to ERPNext: three doctypes (Letter, Letter Template, Letter Type), client scripts for Letter and templates, a standard print format, backend logic for recipient resolution and template rendering, and test coverage (comprehensive tests for Letter; scaffolds for Letter Template and Letter Type). Several init.py files received license headers. JSON fixtures define doctypes, print format, and a test record for Letter Type.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review backend recipient resolution and permission checks: erpnext/utilities/doctype/letter/letter.py
  • Review template rendering, JSON parsing, and permission enforcement: erpnext/utilities/doctype/letter_template/letter_template.py
  • Verify client-side form logic, server-call sequencing, and query filters: erpnext/utilities/doctype/letter/letter.js
  • Validate DocType JSONs and print format Jinja template for field names and conditional branches: letter.json, letter_template.json, letter_type.json, letter_standard.json
  • Review tests: test_letter.py (detailed), plus scaffolds test_letter_template.py, test_letter_type.py
  • Spot-check added license headers and non-functional files for accidental changes

Suggested labels

needs-tests

Suggested reviewers

  • ruthra-kumar
  • diptanilsaha

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Letter Management feature standardize official correspondence' clearly identifies the main change as introducing a Letter Management feature for standardizing official correspondence.
Description check ✅ Passed The description provides comprehensive context about the Letter Management feature, including its purpose, key components (Letter Type, Template, Document), features, benefits, and usage instructions, all directly related to the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (6)
erpnext/crm/doctype/letter/test_letter.py (1)

16-22: Placeholder test class with no test methods.

The Letter DocType includes significant logic for recipient validation, address resolution, and template rendering. Consider adding at least basic integration tests for core functionality before merging.

erpnext/crm/doctype/letter_type/letter_type.json (1)

31-44: Consider expanding permissions for Letter Type.

Currently, only "System Manager" has access to Letter Type. Depending on the use case, other roles like "HR Manager", "Sales Manager", or "CRM Manager" may need to create or manage letter types for their departments.

erpnext/crm/doctype/letter_template/test_letter_template.py (1)

16-22: Empty test class – consider adding tests for get_letter_template.

The get_letter_template function in letter_template.py has rendering logic that would benefit from test coverage, particularly:

  • Template rendering with valid Jinja placeholders
  • Handling of missing subject/content fields
  • JSON string parsing for the doc parameter

Would you like me to generate sample test cases for these scenarios?

erpnext/crm/doctype/letter/letter.json (1)

174-179: Consider using a Link field to Language doctype.

The language field is defined as Data type, but ERPNext has a Language doctype. Using a Link field would provide validation and a dropdown selection of available languages.

  {
   "fieldname": "language",
-  "fieldtype": "Data",
+  "fieldtype": "Link",
   "label": "Print Language",
+  "options": "Language",
   "print_hide": 1
  }
erpnext/crm/doctype/letter_template/letter_template.py (1)

34-36: Consider handling JSON parse errors gracefully.

If doc is an invalid JSON string, json.loads() will raise a JSONDecodeError. This could result in an unclear error message for API consumers.

 @frappe.whitelist()
 def get_letter_template(template_name, doc):
 	if isinstance(doc, str):
-		doc = json.loads(doc)
+		try:
+			doc = json.loads(doc)
+		except json.JSONDecodeError:
+			frappe.throw(_("Invalid JSON format for doc parameter"))

This requires adding the import:

from frappe import _
erpnext/crm/doctype/letter/letter.py (1)

36-53: Recipient name resolution logic is sound; consider centralizing it to avoid duplication.

set_recipient_name() and get_recipient_name_field() give you a clean mapping from recipient_type → name field, and a safe fallback to "name". The same mapping logic is reimplemented in get_recipient_details() below, which means adding a new recipient type or changing the mapping requires edits in two places.

Extracting this mapping/name‑resolution into a small shared helper (e.g. def resolve_recipient_name(recipient_type, recipient): ...) and using it both in Letter.set_recipient_name() and get_recipient_details() would keep behavior consistent and reduce maintenance overhead.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6515bb0 and a25de78.

📒 Files selected for processing (13)
  • erpnext/crm/doctype/letter/letter.js (1 hunks)
  • erpnext/crm/doctype/letter/letter.json (1 hunks)
  • erpnext/crm/doctype/letter/letter.py (1 hunks)
  • erpnext/crm/doctype/letter/test_letter.py (1 hunks)
  • erpnext/crm/doctype/letter_template/letter_template.js (1 hunks)
  • erpnext/crm/doctype/letter_template/letter_template.json (1 hunks)
  • erpnext/crm/doctype/letter_template/letter_template.py (1 hunks)
  • erpnext/crm/doctype/letter_template/test_letter_template.py (1 hunks)
  • erpnext/crm/doctype/letter_type/letter_type.js (1 hunks)
  • erpnext/crm/doctype/letter_type/letter_type.json (1 hunks)
  • erpnext/crm/doctype/letter_type/letter_type.py (1 hunks)
  • erpnext/crm/doctype/letter_type/test_letter_type.py (1 hunks)
  • erpnext/crm/print_format/letter_standard/letter_standard.json (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: Linters
erpnext/crm/doctype/letter/letter.js

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

erpnext/crm/doctype/letter/letter.json

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

🪛 Ruff (0.14.7)
erpnext/crm/doctype/letter/letter.py

28-28: Syntax error in forward annotation: Expected an expression

(F722)


32-32: Undefined name Customer

(F821)


32-32: Undefined name Supplier

(F821)


32-32: Undefined name Employee

(F821)


32-32: Undefined name Shareholder

(F821)


32-32: Undefined name Contact

(F821)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Python Unit Tests (1)
  • GitHub Check: Python Unit Tests (4)
  • GitHub Check: Python Unit Tests (3)
  • GitHub Check: Python Unit Tests (2)
  • GitHub Check: Check Commit Titles
  • GitHub Check: Patch Test
  • GitHub Check: Summary
🔇 Additional comments (9)
erpnext/crm/doctype/letter_type/letter_type.js (1)

1-8: LGTM!

Standard ERPNext DocType client-side scaffold. The commented-out placeholder is appropriate for a new DocType that doesn't require custom client-side logic yet.

erpnext/crm/doctype/letter_type/letter_type.py (1)

1-20: LGTM!

Standard ERPNext DocType class scaffold with auto-generated type hints. The pass statement is conventional for DocTypes that don't require custom server-side logic.

erpnext/crm/doctype/letter_type/test_letter_type.py (1)

1-22: LGTM!

Standard test scaffold for a simple master DocType. Given Letter Type's minimal logic (single unique field), the placeholder is acceptable.

erpnext/crm/doctype/letter/letter.js (1)

37-133: LGTM!

The event handlers for recipient, recipient_type, recipient_address, company, company_address, and letter_template follow consistent patterns for field dependencies, data fetching, and field clearing. The integration with server-side methods and ERPNext utilities is appropriate.

erpnext/crm/doctype/letter_template/letter_template.js (1)

1-8: Scaffold file with commented boilerplate is acceptable.

The file serves as a placeholder for future form event handling. If no immediate UI logic is planned, consider removing the commented code to keep the codebase clean, or leave it if you plan to add functionality soon.

erpnext/crm/doctype/letter/letter.json (1)

191-205: Permissions are limited to System Manager only.

Currently, only System Manager can access Letters. Consider whether additional roles (e.g., HR Manager for employee letters, Sales Manager for customer letters) should have access based on your use case requirements.

erpnext/crm/print_format/letter_standard/letter_standard.json (1)

1-33: Well-structured print format with proper status handling.

The print format template is comprehensive with:

  • Proper status badges for CANCELLED and DRAFT states
  • Good use of _() for internationalization
  • Defensive fallback with doc.content or ""
  • Clean CSS styling appropriate for official letters
erpnext/crm/doctype/letter_template/letter_template.py (2)

26-30: Good template validation on save.

Validating subject and content templates during document save prevents invalid Jinja syntax from being stored, which would cause runtime errors during rendering.


43-47: Template rendering context is appropriately scoped.

The rendering uses user-provided doc data as context for a trusted template from the database. Since validate_template is called on save (lines 26-30), the templates themselves are validated for safe Jinja syntax.

@codecov
Copy link

codecov bot commented Dec 3, 2025

Codecov Report

❌ Patch coverage is 77.63975% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.89%. Comparing base (ccbbc60) to head (9aebafd).
⚠️ Report is 99 commits behind head on develop.

Files with missing lines Patch % Lines
...ilities/doctype/letter_template/letter_template.py 0.00% 27 Missing ⚠️
erpnext/utilities/doctype/letter/test_letter.py 92.94% 6 Missing ⚠️
erpnext/utilities/doctype/letter/letter.py 91.42% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #50918      +/-   ##
===========================================
- Coverage    78.91%   78.89%   -0.03%     
===========================================
  Files         1174     1180       +6     
  Lines       119373   119592     +219     
===========================================
+ Hits         94200    94348     +148     
- Misses       25173    25244      +71     
Files with missing lines Coverage Δ
...es/doctype/letter_template/test_letter_template.py 100.00% <100.00%> (ø)
...pnext/utilities/doctype/letter_type/letter_type.py 100.00% <100.00%> (ø)
.../utilities/doctype/letter_type/test_letter_type.py 100.00% <100.00%> (ø)
erpnext/utilities/doctype/letter/letter.py 91.42% <91.42%> (ø)
erpnext/utilities/doctype/letter/test_letter.py 92.94% <92.94%> (ø)
...ilities/doctype/letter_template/letter_template.py 0.00% <0.00%> (ø)

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
erpnext/crm/doctype/letter/letter.py (1)

39-53: Recipient name resolution is sound; consider de-duplicating name-field logic

set_recipient_name defensively picks the appropriate display field per recipient_type and falls back to "name" if that column doesn’t exist, which should work across the supported doctypes and customizations.

The same mapping logic for "Shareholder", "Contact", and the <type>_name pattern is reimplemented inline in get_recipient_details. To keep behavior in sync long term, consider extracting this mapping into a small shared helper (or reusing get_recipient_name_field from there) and calling it from both places.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0416a19 and 4cd2e14.

📒 Files selected for processing (2)
  • erpnext/crm/doctype/letter/letter.py (1 hunks)
  • erpnext/crm/doctype/letter_template/letter_template.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
erpnext/crm/doctype/letter/letter.py (1)
erpnext/crm/doctype/letter_template/letter_template.py (1)
  • validate (26-30)
🪛 Ruff (0.14.7)
erpnext/crm/doctype/letter/letter.py

28-28: Syntax error in forward annotation: Expected an expression

(F722)


32-32: Undefined name Customer

(F821)


32-32: Undefined name Supplier

(F821)


32-32: Undefined name Employee

(F821)


32-32: Undefined name Shareholder

(F821)


32-32: Undefined name Contact

(F821)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Python Unit Tests (3)
  • GitHub Check: Python Unit Tests (1)
  • GitHub Check: Python Unit Tests (4)
  • GitHub Check: Python Unit Tests (2)
  • GitHub Check: Patch Test
  • GitHub Check: Summary
🔇 Additional comments (1)
erpnext/crm/doctype/letter_template/letter_template.py (1)

26-30: Jinja validation hook in validate is appropriate

Conditionally validating subject and content keeps the hook simple and avoids unnecessary work when fields are empty; this matches how other templated doctypes typically validate Jinja.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
erpnext/crm/doctype/letter/letter.py (1)

64-64: Critical: Invalid throw parameter will cause TypeError at runtime.

This issue was previously flagged: frappe.has_permission() does not accept a throw parameter. The call will fail with TypeError: has_permission() got an unexpected keyword argument 'throw' when invoked.

The correct approach is to check the return value and raise explicitly:

-	frappe.has_permission(recipient_type, doc=recipient, throw=True)
+	if not frappe.has_permission(recipient_type, doc=recipient):
+		frappe.throw(_("Not permitted to access {0} {1}").format(recipient_type, recipient), frappe.PermissionError)

Ensure from frappe import _ is imported at the top if not already present.

🧹 Nitpick comments (2)
erpnext/crm/doctype/letter/letter.py (2)

66-75: Consider extracting name field resolution to eliminate duplication.

The logic for determining name_field (lines 66-70) and retrieving the recipient name (lines 72-75) duplicates the logic in get_recipient_name_field() (lines 47-53) and set_recipient_name() (lines 42-45).

To improve maintainability, extract this into a shared module-level helper function:

def _get_recipient_name(recipient_type, recipient):
	"""Helper to resolve recipient name field and retrieve value."""
	name_field = (
		"title" if recipient_type == "Shareholder"
		else ("full_name" if recipient_type == "Contact" else recipient_type.lower() + "_name")
	)
	
	if frappe.db.has_column(recipient_type, name_field):
		return frappe.db.get_value(recipient_type, recipient, name_field)
	return frappe.db.get_value(recipient_type, recipient, "name")

Then use it in both set_recipient_name() and get_recipient_details().


56-57: Consider adding type hints for input validation.

Adding type annotations will enable Frappe's request validation to reject invalid inputs:

 @frappe.whitelist()
-def get_recipient_details(recipient_type, recipient):
+def get_recipient_details(recipient_type: str, recipient: str):

This provides an additional layer of defense against malformed requests.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd2e14 and 9c2148e.

📒 Files selected for processing (5)
  • erpnext/crm/doctype/letter/letter.py (1 hunks)
  • erpnext/crm/doctype/letter/test_letter.py (1 hunks)
  • erpnext/crm/doctype/letter_template/letter_template.py (1 hunks)
  • erpnext/crm/doctype/letter_template/test_letter_template.py (1 hunks)
  • erpnext/crm/doctype/letter_type/test_letter_type.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • erpnext/crm/doctype/letter_type/test_letter_type.py
  • erpnext/crm/doctype/letter_template/letter_template.py
  • erpnext/crm/doctype/letter_template/test_letter_template.py
  • erpnext/crm/doctype/letter/test_letter.py
🧰 Additional context used
🪛 Ruff (0.14.7)
erpnext/crm/doctype/letter/letter.py

28-28: Syntax error in forward annotation: Expected an expression

(F722)


32-32: Undefined name Customer

(F821)


32-32: Undefined name Supplier

(F821)


32-32: Undefined name Employee

(F821)


32-32: Undefined name Shareholder

(F821)


32-32: Undefined name Contact

(F821)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Python Unit Tests (4)
  • GitHub Check: Python Unit Tests (2)
  • GitHub Check: Python Unit Tests (1)
  • GitHub Check: Python Unit Tests (3)
  • GitHub Check: Patch Test
  • GitHub Check: Summary

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
erpnext/crm/doctype/letter/letter.py (1)

71-80: Consider extracting the recipient name field logic to reduce duplication.

The logic for determining name_field and retrieving recipient_name (lines 71-80) duplicates the implementation in the Letter class methods (get_recipient_name_field and set_recipient_name, lines 48-54 and 40-46). This creates a maintenance burden—any changes to the name field mapping would need to be updated in both places.

Consider extracting this logic into a module-level helper function that both the class and the whitelisted function can call:

+def _get_recipient_name_field(recipient_type: str) -> str:
+	"""Get the appropriate name field for a given recipient type."""
+	if recipient_type == "Shareholder":
+		return "title"
+	elif recipient_type == "Contact":
+		return "full_name"
+	else:
+		return recipient_type.lower() + "_name"
+
+
 class Letter(Document):
 	# ...
 
 	def get_recipient_name_field(self):
-		if self.recipient_type == "Shareholder":
-			return "title"
-		elif self.recipient_type == "Contact":
-			return "full_name"
-		else:
-			return self.recipient_type.lower() + "_name"
+		return _get_recipient_name_field(self.recipient_type)

Then update get_recipient_details to use the helper:

-	name_field = (
-		"title"
-		if recipient_type == "Shareholder"
-		else ("full_name" if recipient_type == "Contact" else recipient_type.lower() + "_name")
-	)
+	name_field = _get_recipient_name_field(recipient_type)

This improves maintainability and readability by eliminating the nested ternary expression.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c2148e and 743456f.

📒 Files selected for processing (2)
  • erpnext/crm/doctype/letter/letter.py (1 hunks)
  • erpnext/crm/doctype/letter_template/letter_template.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
erpnext/crm/doctype/letter/letter.py (1)
erpnext/crm/doctype/letter_template/letter_template.py (1)
  • validate (26-30)
🪛 Ruff (0.14.7)
erpnext/crm/doctype/letter/letter.py

29-29: Syntax error in forward annotation: Expected an expression

(F722)


33-33: Undefined name Customer

(F821)


33-33: Undefined name Supplier

(F821)


33-33: Undefined name Employee

(F821)


33-33: Undefined name Shareholder

(F821)


33-33: Undefined name Contact

(F821)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Python Unit Tests (2)
  • GitHub Check: Python Unit Tests (1)
  • GitHub Check: Python Unit Tests (4)
  • GitHub Check: Python Unit Tests (3)
  • GitHub Check: Patch Test
  • GitHub Check: Summary
🔇 Additional comments (2)
erpnext/crm/doctype/letter_template/letter_template.py (1)

26-53: LGTM! Security and best practices properly implemented.

The implementation correctly addresses all previous security concerns:

  • Type hints enable automatic request validation (line 34)
  • Permission check enforced before template access (line 42)
  • Jinja template validation prevents injection (lines 27-30)
  • Proper error handling for invalid JSON input (lines 35-39)
erpnext/crm/doctype/letter/letter.py (1)

37-54: LGTM! Validation logic is clean and correct.

The recipient name resolution is properly implemented with appropriate fallback logic when the expected name field doesn't exist.

@aadhilpm
Copy link
Contributor Author

aadhilpm commented Dec 4, 2025

Actionable comments posted: 0

🧹 Nitpick comments (1)

erpnext/crm/doctype/letter/letter.py (1)> 71-80: Consider extracting the recipient name field logic to reduce duplication.

The logic for determining name_field and retrieving recipient_name (lines 71-80) duplicates the implementation in the Letter class methods (get_recipient_name_field and set_recipient_name, lines 48-54 and 40-46). This creates a maintenance burden—any changes to the name field mapping would need to be updated in both places.
Consider extracting this logic into a module-level helper function that both the class and the whitelisted function can call:

+def _get_recipient_name_field(recipient_type: str) -> str:
+	"""Get the appropriate name field for a given recipient type."""
+	if recipient_type == "Shareholder":
+		return "title"
+	elif recipient_type == "Contact":
+		return "full_name"
+	else:
+		return recipient_type.lower() + "_name"
+
+
 class Letter(Document):
 	# ...
 
 	def get_recipient_name_field(self):
-		if self.recipient_type == "Shareholder":
-			return "title"
-		elif self.recipient_type == "Contact":
-			return "full_name"
-		else:
-			return self.recipient_type.lower() + "_name"
+		return _get_recipient_name_field(self.recipient_type)

Then update get_recipient_details to use the helper:

-	name_field = (
-		"title"
-		if recipient_type == "Shareholder"
-		else ("full_name" if recipient_type == "Contact" else recipient_type.lower() + "_name")
-	)
+	name_field = _get_recipient_name_field(recipient_type)

This improves maintainability and readability by eliminating the nested ternary expression.

📜 Review details

I followed the same approach used in Payment Entry. In Payment Entry, the party_name is fetched using this logic, and I’ve applied a similar concept to fetch recipient_name.

def get_party_details(company, party_type, party, date, cost_center=None):

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
erpnext/crm/doctype/letter/letter.js (1)

67-109: Consider extracting address display logic (optional).

The recipient_address and company_address handlers share identical logic for fetching and displaying addresses. If this pattern appears elsewhere in the codebase, a small helper function could reduce duplication:

function update_address_display(frm, address_field, display_field) {
	if (frm.doc[address_field]) {
		frappe.call({
			method: "frappe.contacts.doctype.address.address.get_address_display",
			args: { address_dict: frm.doc[address_field] },
			callback: function (r) {
				if (r.message) {
					frm.set_value(display_field, r.message);
				}
			},
		});
	} else {
		frm.set_value(display_field, "");
	}
}

However, given the simplicity and clarity of the inline approach, this refactor is entirely optional.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 743456f and 5e04cbd.

📒 Files selected for processing (1)
  • erpnext/crm/doctype/letter/letter.js (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: Linters
erpnext/crm/doctype/letter/letter.js

[error] 1-1: pre-commit: trailing-whitespace hook modified files and exited with code 1. Changes were applied to erpnext/crm/doctype/letter/letter.js.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Python Unit Tests (3)
  • GitHub Check: Python Unit Tests (2)
  • GitHub Check: Python Unit Tests (4)
  • GitHub Check: Python Unit Tests (1)
  • GitHub Check: Patch Test
  • GitHub Check: Summary
🔇 Additional comments (1)
erpnext/crm/doctype/letter/letter.js (1)

111-131: Well-structured template handler.

The letter_template handler correctly fetches and populates template data with appropriate null checks. The approach follows ERPNext conventions and integrates cleanly with the backend API.

@diptanilsaha
Copy link
Member

diptanilsaha commented Dec 5, 2025

@aadhilpm Currently, we are not adding any new features to ERPNext's CRM Module. Check out Frappe CRM.

Also, I recommend opening a GitHub issue for a feature request first and then working on it.

@aadhilpm
Copy link
Contributor Author

aadhilpm commented Dec 5, 2025

@diptanilsaha

Thanks for the update.

Can you please suggest which module this feature would be suitable for? I added it under this module because DocTypes like Contract, Appointment, etc. already belong here. If there is a better place, I will update the code accordingly.

@diptanilsaha
Copy link
Member

@aadhilpm you can add a feature request on Frappe CRM.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
erpnext/utilities/doctype/letter_template/letter_template.json (1)

42-42: Update the Jinja documentation link.

The help HTML contains an outdated Jinja documentation link (http://jinja.pocoo.org/docs/dev/templates/). Update it to the current official URL: https://jinja.palletsprojects.com/en/stable/.

🧹 Nitpick comments (4)
erpnext/utilities/doctype/letter_type/test_letter_type.py (1)

14-20: Consider adding basic CRUD tests.

While LetterType is a simple master doctype, adding basic tests for creation and validation would improve coverage.

erpnext/utilities/doctype/letter/letter.py (1)

48-54: Consider extracting recipient name field logic (already discussed).

The logic for determining the recipient name field is duplicated in lines 71-75 of get_recipient_details(). While the PR author noted this follows the Payment Entry pattern, extracting it to a module-level helper would reduce duplication and simplify maintenance.

However, given this was already discussed and defended as following established patterns, this is optional.

erpnext/utilities/doctype/letter/letter.js (2)

37-51: Add error handling to server calls.

None of the frappe.call callbacks handle errors. If server methods fail, users won't receive feedback. Consider adding error handlers to improve UX:

frappe.call({
    method: "...",
    args: { ... },
    callback: function(r) {
        // success handling
    },
    error: function(r) {
        frappe.msgprint(__("Failed to load data. Please try again."));
    }
});

Also applies to: 69-79, 95-105, 113-129


67-83: Consider extracting address display logic.

The recipient_address and company_address handlers have nearly identical logic. You could extract a helper function to reduce duplication:

function update_address_display(frm, address_field, display_field) {
    if (frm.doc[address_field]) {
        frappe.call({
            method: "frappe.contacts.doctype.address.address.get_address_display",
            args: { address_dict: frm.doc[address_field] },
            callback: function(r) {
                if (r.message) {
                    frm.set_value(display_field, r.message);
                }
            }
        });
    } else {
        frm.set_value(display_field, "");
    }
}

However, this pattern is common in ERPNext, so the current approach is acceptable.

Also applies to: 93-109

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de144d3 and ca47f32.

📒 Files selected for processing (18)
  • erpnext/utilities/doctype/letter/__init__.py (1 hunks)
  • erpnext/utilities/doctype/letter/letter.js (1 hunks)
  • erpnext/utilities/doctype/letter/letter.json (1 hunks)
  • erpnext/utilities/doctype/letter/letter.py (1 hunks)
  • erpnext/utilities/doctype/letter/test_letter.py (1 hunks)
  • erpnext/utilities/doctype/letter_template/__init__.py (1 hunks)
  • erpnext/utilities/doctype/letter_template/letter_template.js (1 hunks)
  • erpnext/utilities/doctype/letter_template/letter_template.json (1 hunks)
  • erpnext/utilities/doctype/letter_template/letter_template.py (1 hunks)
  • erpnext/utilities/doctype/letter_template/test_letter_template.py (1 hunks)
  • erpnext/utilities/doctype/letter_type/__init__.py (1 hunks)
  • erpnext/utilities/doctype/letter_type/letter_type.js (1 hunks)
  • erpnext/utilities/doctype/letter_type/letter_type.json (1 hunks)
  • erpnext/utilities/doctype/letter_type/letter_type.py (1 hunks)
  • erpnext/utilities/doctype/letter_type/test_letter_type.py (1 hunks)
  • erpnext/utilities/print_format/__init__.py (1 hunks)
  • erpnext/utilities/print_format/letter_standard/__init__.py (1 hunks)
  • erpnext/utilities/print_format/letter_standard/letter_standard.json (1 hunks)
✅ Files skipped from review due to trivial changes (7)
  • erpnext/utilities/doctype/letter_template/letter_template.js
  • erpnext/utilities/doctype/letter_type/init.py
  • erpnext/utilities/doctype/letter_template/init.py
  • erpnext/utilities/doctype/letter_type/letter_type.js
  • erpnext/utilities/print_format/init.py
  • erpnext/utilities/print_format/letter_standard/init.py
  • erpnext/utilities/doctype/letter/init.py
🧰 Additional context used
🧬 Code graph analysis (1)
erpnext/utilities/doctype/letter/letter.py (1)
erpnext/utilities/doctype/letter_template/letter_template.py (1)
  • validate (26-30)
🪛 Ruff (0.14.7)
erpnext/utilities/doctype/letter/letter.py

29-29: Syntax error in forward annotation: Expected an expression

(F722)


33-33: Undefined name Customer

(F821)


33-33: Undefined name Supplier

(F821)


33-33: Undefined name Employee

(F821)


33-33: Undefined name Shareholder

(F821)


33-33: Undefined name Contact

(F821)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Patch Test
  • GitHub Check: Python Unit Tests (3)
  • GitHub Check: Python Unit Tests (4)
  • GitHub Check: Python Unit Tests (1)
  • GitHub Check: Python Unit Tests (2)
  • GitHub Check: Summary
🔇 Additional comments (11)
erpnext/utilities/print_format/letter_standard/letter_standard.json (1)

1-33: LGTM! Print format configuration is well-structured.

The print format configuration correctly defines the Letter Standard template with appropriate settings for margins, PDF generation, and Jinja rendering. The embedded HTML template properly handles letterhead, status indicators (CANCELLED/DRAFT), and document field rendering.

erpnext/utilities/doctype/letter_type/letter_type.py (1)

8-20: LGTM! Appropriate placeholder for simple master doctype.

The LetterType class implementation is appropriate for a simple master doctype that requires no custom validation or business logic. The TYPE_CHECKING block is properly structured.

erpnext/utilities/doctype/letter/letter.json (1)

1-211: LGTM! DocType configuration is well-structured.

The Letter DocType configuration properly defines:

  • Dynamic recipient linking based on recipient_type
  • Appropriate field relationships (addresses, templates, company)
  • Submittable document workflow
  • Link filters for letter_template based on letter_type (line 139)
  • Proper permissions and naming series
erpnext/utilities/doctype/letter_template/letter_template.py (2)

26-30: LGTM! Template validation is properly implemented.

The validate method correctly validates both subject and content Jinja templates before saving, preventing invalid templates from being stored.


33-53: LGTM! Template rendering with proper security checks.

The function correctly:

  • Handles JSON parsing with appropriate error handling
  • Enforces read permissions on the template
  • Conditionally renders subject and content fields
  • Returns a well-structured dictionary

The permission check (line 42) ensures users can only render templates they have access to.

erpnext/utilities/doctype/letter/letter.py (3)

37-46: LGTM! Recipient name resolution with defensive field checking.

The validate and set_recipient_name methods correctly resolve the recipient name with appropriate defensive checks using frappe.db.has_column() to avoid errors when expected fields don't exist.


57-87: LGTM! Robust recipient details resolution with proper security.

The function correctly implements:

  • Input validation with early return (lines 59-60)
  • Existence verification (lines 62-63)
  • Permission enforcement using frappe.has_permission() (lines 65-69)
  • Defensive field access with has_column() checks (lines 77, 84)
  • Appropriate error messages with translation support

The security checks ensure users can only access recipient data they have permission to view.


29-33: Static analysis false positives can be safely ignored.

The Ruff warnings about undefined names and syntax errors in the TYPE_CHECKING block are false positives. The DF.Literal type annotations with string values are valid syntax for type checking purposes and don't execute at runtime.

erpnext/utilities/doctype/letter_type/letter_type.json (1)

1-51: LGTM!

The Letter Type DocType is well-structured. The quick_entry flag is appropriate for a simple lookup, and the configuration follows ERPNext conventions.

erpnext/utilities/doctype/letter/letter.js (2)

5-33: LGTM!

The query filters correctly handle missing prerequisites by returning empty result sets, and the address query integration follows ERPNext patterns.


35-65: LGTM!

The recipient handlers correctly manage field dependencies and maintain consistent state when selections change.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
erpnext/utilities/doctype/letter/test_letter.py (2)

10-15: Include Contact in EXTRA_TEST_RECORD_DEPENDENCIES for more reliable coverage

test_recipient_name_for_contact (Line 66 onward) depends on at least one Contact record but EXTRA_TEST_RECORD_DEPENDENCIES (Line 13) does not list "Contact". In environments where no contact fixtures are loaded by default, this test will be skipped rather than executed, silently reducing coverage.

Consider explicitly adding "Contact" to ensure the fixture is always available:

-EXTRA_TEST_RECORD_DEPENDENCIES = ["Customer", "Supplier", "Employee", "Shareholder", "Letter Type"]
+EXTRA_TEST_RECORD_DEPENDENCIES = [
+	"Customer",
+	"Supplier",
+	"Employee",
+	"Shareholder",
+	"Letter Type",
+	"Contact",
+]

23-25: Call super().setUp() to preserve IntegrationTestCase initialization

Overriding setUp (Line 23) without calling super().setUp() can bypass important initialization logic in IntegrationTestCase (e.g., environment setup, fixture loading hooks). To avoid subtle issues and keep this test aligned with framework patterns, call the parent implementation before clearing Letter records:

-	def setUp(self):
-		frappe.db.sql("delete from `tabLetter`")
+	def setUp(self):
+		super().setUp()
+		frappe.db.sql("delete from `tabLetter`")
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca47f32 and 9aebafd.

📒 Files selected for processing (2)
  • erpnext/utilities/doctype/letter/test_letter.py (1 hunks)
  • erpnext/utilities/doctype/letter_type/test_records.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
erpnext/utilities/doctype/letter/test_letter.py (1)
erpnext/utilities/doctype/letter/letter.py (3)
  • get_recipient_details (58-87)
  • get_recipient_name_field (48-54)
  • set_recipient_name (40-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Python Unit Tests (4)
  • GitHub Check: Python Unit Tests (1)
  • GitHub Check: Python Unit Tests (3)
  • GitHub Check: Python Unit Tests (2)
  • GitHub Check: Patch Test
  • GitHub Check: Summary
🔇 Additional comments (2)
erpnext/utilities/doctype/letter_type/test_records.json (1)

1-5: Fixture structure and naming are appropriate

The fixture provides a minimal, clearly named _Test Letter Type record that aligns with how create_letter() references it in tests. No issues found.

erpnext/utilities/doctype/letter/test_letter.py (1)

26-146: Good, focused coverage of recipient resolution and helper behavior

The tests around Lines 26–96 and 97–134 exercise the key behaviors of Letter and get_recipient_details:

  • recipient_name resolution for all supported recipient types.
  • Correct mapping from recipient_type to name fields.
  • Handling of empty parameters and non-existent recipients.
  • Guard behavior when recipient or recipient_type is missing.
    The create_letter helper (Lines 136–146) keeps scenarios concise and readable. Overall, this is solid, maintainable coverage for the feature.

@aadhilpm
Copy link
Contributor Author

aadhilpm commented Dec 5, 2025

@diptanilsaha Since Letters are needed across the company, I moved the feature from CRM to Utilities and updated all the code. Let me know if anything else needs to be changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants