diff --git a/erpnext/utilities/doctype/letter/__init__.py b/erpnext/utilities/doctype/letter/__init__.py new file mode 100644 index 000000000000..f5474eb179e3 --- /dev/null +++ b/erpnext/utilities/doctype/letter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt diff --git a/erpnext/utilities/doctype/letter/letter.js b/erpnext/utilities/doctype/letter/letter.js new file mode 100644 index 000000000000..2e69ca2e098d --- /dev/null +++ b/erpnext/utilities/doctype/letter/letter.js @@ -0,0 +1,132 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Letter", { + refresh(frm) { + // Filter for recipient address + frm.set_query("recipient_address", () => { + if (!frm.doc.recipient_type || !frm.doc.recipient) { + return { filters: { name: "" } }; + } + return { + query: "frappe.contacts.doctype.address.address.address_query", + filters: { + link_doctype: frm.doc.recipient_type, + link_name: frm.doc.recipient, + }, + }; + }); + + // Filter for company address + frm.set_query("company_address", () => { + if (!frm.doc.company) { + return { filters: { name: "" } }; + } + return { + query: "frappe.contacts.doctype.address.address.address_query", + filters: { + link_doctype: "Company", + link_name: frm.doc.company, + }, + }; + }); + }, + + recipient(frm) { + if (frm.doc.recipient_type && frm.doc.recipient) { + frappe.call({ + method: "erpnext.utilities.doctype.letter.letter.get_recipient_details", + args: { + recipient_type: frm.doc.recipient_type, + recipient: frm.doc.recipient, + }, + callback: function (r) { + if (r.message) { + frm.set_value("recipient_name", r.message.recipient_name); + if (r.message.language) { + frm.set_value("language", r.message.language); + } + } + }, + }); + } else { + frm.set_value("recipient_name", ""); + frm.set_value("language", ""); + } + frm.set_value("recipient_address", ""); + frm.set_value("address_display", ""); + }, + + recipient_type(frm) { + frm.set_value("recipient", ""); + frm.set_value("recipient_name", ""); + frm.set_value("recipient_address", ""); + frm.set_value("address_display", ""); + }, + + recipient_address(frm) { + if (frm.doc.recipient_address) { + frappe.call({ + method: "frappe.contacts.doctype.address.address.get_address_display", + args: { + address_dict: frm.doc.recipient_address, + }, + callback: function (r) { + if (r.message) { + frm.set_value("address_display", r.message); + } + }, + }); + } else { + frm.set_value("address_display", ""); + } + }, + + company(frm) { + frm.set_value("company_address", ""); + frm.set_value("company_address_display", ""); + + // Set default letter head from company + erpnext.utils.set_letter_head(frm); + }, + + company_address(frm) { + if (frm.doc.company_address) { + frappe.call({ + method: "frappe.contacts.doctype.address.address.get_address_display", + args: { + address_dict: frm.doc.company_address, + }, + callback: function (r) { + if (r.message) { + frm.set_value("company_address_display", r.message); + } + }, + }); + } else { + frm.set_value("company_address_display", ""); + } + }, + + letter_template(frm) { + if (frm.doc.letter_template) { + frappe.call({ + method: "erpnext.utilities.doctype.letter_template.letter_template.get_letter_template", + args: { + template_name: frm.doc.letter_template, + doc: frm.doc, + }, + callback: function (r) { + if (r && r.message) { + if (r.message.subject) { + frm.set_value("subject", r.message.subject); + } + if (r.message.content) { + frm.set_value("content", r.message.content); + } + } + }, + }); + } + }, +}); diff --git a/erpnext/utilities/doctype/letter/letter.json b/erpnext/utilities/doctype/letter/letter.json new file mode 100644 index 000000000000..3246d998fc1a --- /dev/null +++ b/erpnext/utilities/doctype/letter/letter.json @@ -0,0 +1,211 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2025-12-03 20:35:18.414308", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_letter", + "naming_series", + "recipient_type", + "recipient", + "recipient_name", + "recipient_address", + "address_display", + "column_break_pmit", + "letter_type", + "date", + "company", + "company_address", + "company_address_display", + "amended_from", + "letter_details_section", + "letter_template", + "subject", + "content", + "printing_settings_section", + "letter_head", + "column_break_ksdg", + "language" + ], + "fields": [ + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Letter", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "recipient_type", + "fieldtype": "Select", + "in_filter": 1, + "in_list_view": 1, + "label": "Recipient Type", + "options": "Customer\nSupplier\nEmployee\nShareholder\nContact", + "reqd": 1 + }, + { + "fieldname": "recipient_address", + "fieldtype": "Link", + "label": "Recipient Address", + "options": "Address" + }, + { + "fieldname": "column_break_pmit", + "fieldtype": "Column Break" + }, + { + "fieldname": "recipient", + "fieldtype": "Dynamic Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Recipient", + "options": "recipient_type", + "reqd": 1 + }, + { + "fieldname": "recipient_name", + "fieldtype": "Data", + "label": "Recipient Name", + "read_only": 1 + }, + { + "fieldname": "address_display", + "fieldtype": "Text Editor", + "label": "Recipient Address Details", + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "L-.YY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "company_address", + "fieldtype": "Link", + "label": "Company Address", + "options": "Address" + }, + { + "fieldname": "company_address_display", + "fieldtype": "Text Editor", + "label": "Company Address Details", + "read_only": 1 + }, + { + "fieldname": "letter_type", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Letter Type", + "options": "Letter Type", + "reqd": 1 + }, + { + "default": "Today", + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "reqd": 1 + }, + { + "fieldname": "letter_details_section", + "fieldtype": "Section Break", + "label": "Letter Details" + }, + { + "fieldname": "letter_template", + "fieldtype": "Link", + "label": "Letter Template", + "link_filters": "[[\"Letter Template\",\"letter_type\",\"=\",\"eval:doc.letter_type\"]]", + "options": "Letter Template" + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject" + }, + { + "fieldname": "content", + "fieldtype": "Text Editor", + "label": "Content" + }, + { + "fieldname": "section_break_letter", + "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "printing_settings_section", + "fieldtype": "Section Break", + "label": "Printing Settings" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "fieldname": "column_break_ksdg", + "fieldtype": "Column Break" + }, + { + "fieldname": "language", + "fieldtype": "Data", + "label": "Print Language", + "print_hide": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2025-12-03 23:40:09.332016", + "modified_by": "Administrator", + "module": "Utilities", + "name": "Letter", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/utilities/doctype/letter/letter.py b/erpnext/utilities/doctype/letter/letter.py new file mode 100644 index 000000000000..32a7df44d395 --- /dev/null +++ b/erpnext/utilities/doctype/letter/letter.py @@ -0,0 +1,87 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class Letter(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + address_display: DF.TextEditor | None + amended_from: DF.Link | None + company: DF.Link + company_address: DF.Link | None + company_address_display: DF.TextEditor | None + content: DF.TextEditor | None + date: DF.Date + language: DF.Data | None + letter_head: DF.Link | None + letter_template: DF.Link | None + letter_type: DF.Link + naming_series: DF.Literal["L-.YY.-"] + recipient: DF.DynamicLink + recipient_address: DF.Link | None + recipient_name: DF.Data | None + recipient_type: DF.Literal["Customer", "Supplier", "Employee", "Shareholder", "Contact"] + subject: DF.Data | None + # end: auto-generated types + + def validate(self): + self.set_recipient_name() + + def set_recipient_name(self): + if self.recipient_type and self.recipient: + name_field = self.get_recipient_name_field() + if frappe.db.has_column(self.recipient_type, name_field): + self.recipient_name = frappe.db.get_value(self.recipient_type, self.recipient, name_field) + else: + self.recipient_name = frappe.db.get_value(self.recipient_type, self.recipient, "name") + + 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" + + +@frappe.whitelist() +def get_recipient_details(recipient_type: str, recipient: str): + if not recipient_type or not recipient: + return {} + + if not frappe.db.exists(recipient_type, recipient): + frappe.throw(_("{0} {1} does not exist").format(recipient_type, recipient)) + + if not frappe.has_permission(recipient_type, doc=recipient): + frappe.throw( + _("Not permitted to access {0} {1}").format(recipient_type, recipient), + frappe.PermissionError, + ) + + 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): + recipient_name = frappe.db.get_value(recipient_type, recipient, name_field) + else: + recipient_name = frappe.db.get_value(recipient_type, recipient, "name") + + # Get language from recipient if available + language = None + if frappe.db.has_column(recipient_type, "language"): + language = frappe.db.get_value(recipient_type, recipient, "language") + + return {"recipient_name": recipient_name, "language": language} diff --git a/erpnext/utilities/doctype/letter/test_letter.py b/erpnext/utilities/doctype/letter/test_letter.py new file mode 100644 index 000000000000..b52ecbd7466f --- /dev/null +++ b/erpnext/utilities/doctype/letter/test_letter.py @@ -0,0 +1,146 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests import IntegrationTestCase +from frappe.utils import nowdate + +from erpnext.utilities.doctype.letter.letter import get_recipient_details + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = ["Customer", "Supplier", "Employee", "Shareholder", "Letter Type"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class TestLetter(IntegrationTestCase): + """ + Integration tests for Letter. + Use this class for testing interactions between multiple components. + """ + + def setUp(self): + frappe.db.sql("delete from `tabLetter`") + + def test_recipient_name_for_customer(self): + """Test that recipient_name is from customer_name field.""" + letter = create_letter(recipient_type="Customer", recipient="_Test Customer") + letter.insert() + + customer_name = frappe.db.get_value("Customer", "_Test Customer", "customer_name") + self.assertEqual(letter.recipient_name, customer_name) + + def test_recipient_name_for_supplier(self): + """Test that recipient_name is from supplier_name field.""" + letter = create_letter(recipient_type="Supplier", recipient="_Test Supplier") + letter.insert() + + supplier_name = frappe.db.get_value("Supplier", "_Test Supplier", "supplier_name") + self.assertEqual(letter.recipient_name, supplier_name) + + def test_recipient_name_for_employee(self): + """Test that recipient_name is from employee_name field.""" + employee = frappe.db.get_value("Employee", {"status": "Active"}, "name") + if not employee: + self.skipTest("No active employee found for testing") + + letter = create_letter(recipient_type="Employee", recipient=employee) + letter.insert() + + employee_name = frappe.db.get_value("Employee", employee, "employee_name") + self.assertEqual(letter.recipient_name, employee_name) + + def test_recipient_name_for_shareholder(self): + """Test that recipient_name is from title field for Shareholder.""" + shareholder = frappe.db.get_value("Shareholder", {}, "name") + if not shareholder: + self.skipTest("No shareholder found for testing") + + letter = create_letter(recipient_type="Shareholder", recipient=shareholder) + letter.insert() + + shareholder_title = frappe.db.get_value("Shareholder", shareholder, "title") + self.assertEqual(letter.recipient_name, shareholder_title) + + def test_recipient_name_for_contact(self): + """Test that recipient_name is from full_name field for Contact.""" + contact = frappe.db.get_value("Contact", {}, "name") + if not contact: + self.skipTest("No contact found for testing") + + letter = create_letter(recipient_type="Contact", recipient=contact) + letter.insert() + + contact_full_name = frappe.db.get_value("Contact", contact, "full_name") + self.assertEqual(letter.recipient_name, contact_full_name) + + def test_get_recipient_name_field_returns_correct_field(self): + """Test get_recipient_name_field returns the correct field name for each recipient type.""" + letter = create_letter() + + letter.recipient_type = "Customer" + self.assertEqual(letter.get_recipient_name_field(), "customer_name") + + letter.recipient_type = "Supplier" + self.assertEqual(letter.get_recipient_name_field(), "supplier_name") + + letter.recipient_type = "Employee" + self.assertEqual(letter.get_recipient_name_field(), "employee_name") + + letter.recipient_type = "Shareholder" + self.assertEqual(letter.get_recipient_name_field(), "title") + + letter.recipient_type = "Contact" + self.assertEqual(letter.get_recipient_name_field(), "full_name") + + def test_get_recipient_returns_name_and_language(self): + """Test get_recipient_details whitelist function returns correct data.""" + details = get_recipient_details("Customer", "_Test Customer") + + customer_name = frappe.db.get_value("Customer", "_Test Customer", "customer_name") + self.assertEqual(details.get("recipient_name"), customer_name) + self.assertIn("language", details) + + def test_get_recipient_with_empty_params(self): + """Test get_recipient_details returns empty dict for empty params.""" + details = get_recipient_details("", "") + self.assertEqual(details, {}) + + def test_get_recipient_nonexistent_recipient(self): + """Test get_recipient_details raises error for non-existent recipient.""" + self.assertRaises( + frappe.ValidationError, + get_recipient_details, + "Customer", + "Non Existent Customer", + ) + + def test_recipient_name_not_set_when_recipient_empty(self): + """Test that recipient_name is not set when recipient is empty.""" + letter = create_letter() + letter.recipient = None + letter.set_recipient_name() + + self.assertIsNone(letter.recipient_name) + + def test_recipient_name_not_set_when_recipient_type_empty(self): + """Test that recipient_name is not set when recipient_type is empty.""" + letter = create_letter() + letter.recipient_type = None + letter.set_recipient_name() + + self.assertIsNone(letter.recipient_name) + + +def create_letter(**args): + """Create a Letter document for testing.""" + letter = frappe.new_doc("Letter") + letter.recipient_type = args.get("recipient_type") or "Customer" + letter.recipient = args.get("recipient") or "_Test Customer" + letter.letter_type = args.get("letter_type") or "_Test Letter Type" + letter.company = args.get("company") or "_Test Company" + letter.date = args.get("date") or nowdate() + letter.subject = args.get("subject") or "Test Subject" + letter.content = args.get("content") or "Test Content" + return letter diff --git a/erpnext/utilities/doctype/letter_template/__init__.py b/erpnext/utilities/doctype/letter_template/__init__.py new file mode 100644 index 000000000000..f5474eb179e3 --- /dev/null +++ b/erpnext/utilities/doctype/letter_template/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt diff --git a/erpnext/utilities/doctype/letter_template/letter_template.js b/erpnext/utilities/doctype/letter_template/letter_template.js new file mode 100644 index 000000000000..9f26b9d20e34 --- /dev/null +++ b/erpnext/utilities/doctype/letter_template/letter_template.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Letter Template", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/utilities/doctype/letter_template/letter_template.json b/erpnext/utilities/doctype/letter_template/letter_template.json new file mode 100644 index 000000000000..aabef4ab15a3 --- /dev/null +++ b/erpnext/utilities/doctype/letter_template/letter_template.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:title", + "creation": "2025-12-03 22:23:51.934402", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "title", + "letter_type", + "subject", + "content", + "letter_template_help" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Subject", + "reqd": 1 + }, + { + "fieldname": "content", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Content", + "reqd": 1 + }, + { + "fieldname": "letter_template_help", + "fieldtype": "HTML", + "label": "Letter Template Help", + "options": "

Letter Template Example

\n\n
Dear {{ recipient_name }}\nCompany: {{ company }}\n
\n\n

How to get fieldnames

\n\n

The field names you can use in your Letter Template are the fields in the Letter for which you are creating the template. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Letter)

\n\n

Templating

\n\n

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

" + }, + { + "fieldname": "letter_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Letter Type", + "options": "Letter Type", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-12-03 22:53:35.112258", + "modified_by": "Administrator", + "module": "Utilities", + "name": "Letter Template", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/utilities/doctype/letter_template/letter_template.py b/erpnext/utilities/doctype/letter_template/letter_template.py new file mode 100644 index 000000000000..1963781e30b0 --- /dev/null +++ b/erpnext/utilities/doctype/letter_template/letter_template.py @@ -0,0 +1,53 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe.model.document import Document +from frappe.utils.jinja import validate_template + + +class LetterTemplate(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + content: DF.TextEditor + letter_type: DF.Link + subject: DF.Data + title: DF.Data + # end: auto-generated types + + def validate(self): + if self.subject: + validate_template(self.subject) + if self.content: + validate_template(self.content) + + +@frappe.whitelist() +def get_letter_template(template_name: str, doc: str): + if isinstance(doc, str): + try: + doc = json.loads(doc) + except json.JSONDecodeError: + frappe.throw(frappe._("Invalid document data")) + + template = frappe.get_doc("Letter Template", template_name) + template.check_permission("read") + + subject = None + content = None + + if template.subject: + subject = frappe.render_template(template.subject, doc) + + if template.content: + content = frappe.render_template(template.content, doc) + + return {"subject": subject, "content": content} diff --git a/erpnext/utilities/doctype/letter_template/test_letter_template.py b/erpnext/utilities/doctype/letter_template/test_letter_template.py new file mode 100644 index 000000000000..572e01cb858d --- /dev/null +++ b/erpnext/utilities/doctype/letter_template/test_letter_template.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestLetterTemplate(IntegrationTestCase): + """ + Integration tests for LetterTemplate. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/erpnext/utilities/doctype/letter_type/__init__.py b/erpnext/utilities/doctype/letter_type/__init__.py new file mode 100644 index 000000000000..f5474eb179e3 --- /dev/null +++ b/erpnext/utilities/doctype/letter_type/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt diff --git a/erpnext/utilities/doctype/letter_type/letter_type.js b/erpnext/utilities/doctype/letter_type/letter_type.js new file mode 100644 index 000000000000..9b12d5413ec8 --- /dev/null +++ b/erpnext/utilities/doctype/letter_type/letter_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Letter Type", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/utilities/doctype/letter_type/letter_type.json b/erpnext/utilities/doctype/letter_type/letter_type.json new file mode 100644 index 000000000000..d84e18d0de16 --- /dev/null +++ b/erpnext/utilities/doctype/letter_type/letter_type.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:letter_type", + "creation": "2025-12-03 22:14:04.645564", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "letter_type" + ], + "fields": [ + { + "allow_in_quick_entry": 1, + "fieldname": "letter_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Letter Type", + "reqd": 1, + "unique": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-12-03 22:17:49.048251", + "modified_by": "Administrator", + "module": "Utilities", + "name": "Letter Type", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/utilities/doctype/letter_type/letter_type.py b/erpnext/utilities/doctype/letter_type/letter_type.py new file mode 100644 index 000000000000..eb9c640cf2af --- /dev/null +++ b/erpnext/utilities/doctype/letter_type/letter_type.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LetterType(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + letter_type: DF.Data + # end: auto-generated types + + pass diff --git a/erpnext/utilities/doctype/letter_type/test_letter_type.py b/erpnext/utilities/doctype/letter_type/test_letter_type.py new file mode 100644 index 000000000000..bdfa37f73121 --- /dev/null +++ b/erpnext/utilities/doctype/letter_type/test_letter_type.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestLetterType(IntegrationTestCase): + """ + Integration tests for LetterType. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/erpnext/utilities/doctype/letter_type/test_records.json b/erpnext/utilities/doctype/letter_type/test_records.json new file mode 100644 index 000000000000..0242ecbb6449 --- /dev/null +++ b/erpnext/utilities/doctype/letter_type/test_records.json @@ -0,0 +1,6 @@ +[ + { + "doctype": "Letter Type", + "letter_type": "_Test Letter Type" + } +] diff --git a/erpnext/utilities/print_format/__init__.py b/erpnext/utilities/print_format/__init__.py new file mode 100644 index 000000000000..f5474eb179e3 --- /dev/null +++ b/erpnext/utilities/print_format/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt diff --git a/erpnext/utilities/print_format/letter_standard/__init__.py b/erpnext/utilities/print_format/letter_standard/__init__.py new file mode 100644 index 000000000000..f5474eb179e3 --- /dev/null +++ b/erpnext/utilities/print_format/letter_standard/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt diff --git a/erpnext/utilities/print_format/letter_standard/letter_standard.json b/erpnext/utilities/print_format/letter_standard/letter_standard.json new file mode 100644 index 000000000000..5e782c438782 --- /dev/null +++ b/erpnext/utilities/print_format/letter_standard/letter_standard.json @@ -0,0 +1,33 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2025-12-03 23:18:59.607941", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Letter", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "html": "{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}\n {% if letter_head and not no_letterhead %}\n
{{ letter_head }}
\n {% endif %}\n {% if print_heading_template %}\n {{ frappe.render_template(print_heading_template, {\"doc\": doc}) }}\n {% endif %}\n{%- endmacro -%}\n\n{% for page in layout %}\n
\n
\n {{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}\n
\n {%- if doc.meta.is_submittable and doc.docstatus==2 -%}\n
{{ _(\"CANCELLED\") }}
\n {%- endif -%}\n {%- if doc.meta.is_submittable and doc.docstatus==0 and (not print_settings or print_settings.add_draft_heading) -%}\n
{{ _(\"DRAFT\") }}
\n {%- endif -%}\n
\n
\n {{ doc.get_formatted(\"date\") }}\n
\n
\n {% if doc.recipient_name %}{{ doc.recipient_name }}
{% endif %}\n {% if doc.address_display %}{{ doc.address_display }}{% endif %}\n
\n {% if doc.subject %}\n
\n {{ _(\"Subject\") }}: {{ doc.subject }}\n
\n {% endif %}\n
\n {{ doc.content or \"\" }}\n
\n
\n {% if footer %}\n
\n {{ footer }}\n
\n {% endif %}\n
\n{% endfor %}", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 15.0, + "margin_left": 15.0, + "margin_right": 15.0, + "margin_top": 15.0, + "modified": "2025-12-03 23:59:14.089985", + "modified_by": "Administrator", + "module": "Utilities", + "name": "Letter Standard", + "owner": "Administrator", + "page_number": "Hide", + "pdf_generator": "wkhtmltopdf", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_for": "DocType", + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +}