feat: introduce standard and letter_head_for fields in letter head doctype (#38417)
* feat: introduce standard and letter_head_for fields in letter head doctype * feat: introduce a module link field to letterhead doctype to support json creation * feat: make Letter Head importable via sync * test(Letter Head): fix the test_auto_image test case for letter head doctype * fix: make module field depend on standard field value * feat: introduce letter heads for standard reports * fix: letter heads for non-standard reports * fix: letter_head validation in report and letter head doctype edit access based on users * fix: correct validation for standard letter head creation
This commit is contained in:
parent
d6daefb3a3
commit
44b5228598
9 changed files with 139 additions and 14 deletions
|
|
@ -66,6 +66,19 @@ frappe.ui.form.on("Report", {
|
|||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("letter_head", () => {
|
||||
const filters = {
|
||||
letter_head_for: "Report",
|
||||
disabled: 0,
|
||||
};
|
||||
|
||||
if (frm.doc.is_standard === "Yes") {
|
||||
filters.standard = "Yes";
|
||||
}
|
||||
|
||||
return { filters };
|
||||
});
|
||||
},
|
||||
|
||||
ref_doctype: function (frm) {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,6 @@
|
|||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.is_standard == \"No\"",
|
||||
"fieldname": "letter_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Letter Head",
|
||||
|
|
@ -214,7 +213,7 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-31 14:42:49.829920",
|
||||
"modified": "2026-04-10 00:03:15.212213",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Report",
|
||||
|
|
|
|||
|
|
@ -76,16 +76,15 @@ class Report(Document):
|
|||
if frappe.session.user != "Administrator":
|
||||
frappe.throw(_("Only Administrator can save a standard report. Please rename and save."))
|
||||
|
||||
# Letter Head is visible only for non-standard reports.
|
||||
# It should not remain set when it's invisible.
|
||||
self.letter_head = None
|
||||
|
||||
if self.report_type == "Report Builder":
|
||||
self.update_report_json()
|
||||
|
||||
if self.default_print_format and self.has_value_changed("default_print_format"):
|
||||
self.validate_default_print_format()
|
||||
|
||||
if self.letter_head and self.has_value_changed("letter_head"):
|
||||
self.validate_letter_head()
|
||||
|
||||
def before_insert(self):
|
||||
self.set_doctype_roles()
|
||||
|
||||
|
|
@ -93,7 +92,6 @@ class Report(Document):
|
|||
self.export_doc()
|
||||
|
||||
def before_export(self, doc):
|
||||
doc.letter_head = None
|
||||
doc.prepared_report = 0
|
||||
|
||||
def on_trash(self):
|
||||
|
|
@ -429,6 +427,22 @@ class Report(Document):
|
|||
):
|
||||
frappe.throw(_("Selected Print Format is invalid for this Report."))
|
||||
|
||||
def validate_letter_head(self):
|
||||
letter_head = frappe.db.get_value(
|
||||
"Letter Head",
|
||||
self.letter_head,
|
||||
["letter_head_for", "standard", "disabled"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if (
|
||||
not letter_head
|
||||
or letter_head.letter_head_for != "Report"
|
||||
or (self.is_standard == "Yes" and letter_head.standard != "Yes")
|
||||
or letter_head.disabled
|
||||
):
|
||||
frappe.throw(_("Selected Letter Head is invalid for this Report."))
|
||||
|
||||
@frappe.whitelist()
|
||||
def toggle_disable(self, disable: bool):
|
||||
if not self.has_permission("write"):
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ IMPORTABLE_DOCTYPES = [
|
|||
("core", "server_script"),
|
||||
("custom", "custom_field"),
|
||||
("custom", "property_setter"),
|
||||
("printing", "letter_head"),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,22 @@ frappe.ui.form.on("Letter Head", {
|
|||
frm.get_field("instructions").html(INSTRUCTIONS);
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
refresh(frm) {
|
||||
frm.set_intro("");
|
||||
frm.enable_save();
|
||||
|
||||
if (!frappe.boot.developer_mode) {
|
||||
if (frm.is_new()) {
|
||||
frm.toggle_enable("standard", false);
|
||||
}
|
||||
|
||||
if (!frm.is_new() && frm.doc.standard === "Yes") {
|
||||
frm.set_intro(__("Please duplicate this to make changes"));
|
||||
frm.set_read_only();
|
||||
frm.disable_save();
|
||||
}
|
||||
}
|
||||
|
||||
frm.flag_public_attachments = true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -2,15 +2,18 @@
|
|||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:letter_head_name",
|
||||
"creation": "2012-11-22 17:45:46",
|
||||
"creation": "2026-04-07 12:33:33.368499",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"letter_head_for",
|
||||
"letter_head_name",
|
||||
"module",
|
||||
"source",
|
||||
"footer_source",
|
||||
"column_break_3",
|
||||
"standard",
|
||||
"disabled",
|
||||
"is_default",
|
||||
"letter_head_image_section",
|
||||
|
|
@ -196,6 +199,30 @@
|
|||
"fieldtype": "HTML",
|
||||
"label": "Instructions",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "No",
|
||||
"fieldname": "standard",
|
||||
"fieldtype": "Select",
|
||||
"label": "Standard",
|
||||
"options": "No\nYes",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "DocType",
|
||||
"fieldname": "letter_head_for",
|
||||
"fieldtype": "Select",
|
||||
"label": "Letter Head For",
|
||||
"options": "DocType\nReport",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.standard == \"Yes\"",
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module",
|
||||
"mandatory_depends_on": "eval: doc.standard == \"Yes\"",
|
||||
"options": "Module Def"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-font",
|
||||
|
|
@ -203,7 +230,7 @@
|
|||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"max_attachments": 3,
|
||||
"modified": "2026-02-25 14:37:57.061516",
|
||||
"modified": "2026-04-08 13:15:24.935222",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Letter Head",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.utils import export_module_json
|
||||
from frappe.utils import flt, is_image
|
||||
|
||||
|
||||
|
|
@ -31,8 +32,11 @@ class LetterHead(Document):
|
|||
image_height: DF.Float
|
||||
image_width: DF.Float
|
||||
is_default: DF.Check
|
||||
letter_head_for: DF.Literal["DocType", "Report"]
|
||||
letter_head_name: DF.Data
|
||||
module: DF.Link | None
|
||||
source: DF.Literal["Image", "HTML"]
|
||||
standard: DF.Literal["No", "Yes"]
|
||||
# end: auto-generated types
|
||||
|
||||
def before_insert(self):
|
||||
|
|
@ -50,6 +54,7 @@ class LetterHead(Document):
|
|||
def validate(self):
|
||||
self.set_image()
|
||||
self.validate_disabled_and_default()
|
||||
self.validate_standard_letter_head()
|
||||
|
||||
def validate_disabled_and_default(self):
|
||||
if self.disabled and self.is_default:
|
||||
|
|
@ -119,6 +124,7 @@ class LetterHead(Document):
|
|||
|
||||
def on_update(self):
|
||||
self.set_as_default()
|
||||
self.export_letter_head()
|
||||
|
||||
# clear the cache so that the new letter head is uploaded
|
||||
frappe.clear_cache()
|
||||
|
|
@ -136,3 +142,14 @@ class LetterHead(Document):
|
|||
else:
|
||||
frappe.defaults.clear_default("letter_head", self.name)
|
||||
frappe.defaults.clear_default("default_letter_head_content", self.content)
|
||||
|
||||
def export_letter_head(self):
|
||||
return export_module_json(self, self.standard == "Yes", self.module)
|
||||
|
||||
def validate_standard_letter_head(self):
|
||||
if self.standard == "Yes":
|
||||
if not frappe.conf.developer_mode and not self.is_new() and not frappe.flags.in_migrate:
|
||||
frappe.throw(_("Standard Letter Head can be updated in Developer Mode only."))
|
||||
|
||||
if not self.module:
|
||||
frappe.throw(_("Module is required when Standard is set to 'Yes'"))
|
||||
|
|
|
|||
|
|
@ -1,14 +1,45 @@
|
|||
# Copyright (c) 2017, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestLetterHead(IntegrationTestCase):
|
||||
def test_auto_image(self):
|
||||
letter_head = frappe.get_doc(
|
||||
doctype="Letter Head", letter_head_name="Test", source="Image", image="/public/test.png"
|
||||
).insert()
|
||||
doc = frappe.new_doc("Letter Head")
|
||||
doc.letter_head_for = "DocType"
|
||||
doc.letter_head_name = "Test Letter Head"
|
||||
doc.module = "Core"
|
||||
doc.standard = "No"
|
||||
doc.source = "Image"
|
||||
doc.image = "/public/test.png"
|
||||
doc.insert()
|
||||
|
||||
# test if image is automatically set
|
||||
self.assertTrue(letter_head.image in letter_head.content)
|
||||
self.assertTrue(doc.image in doc.content)
|
||||
|
||||
def test_export_letter_head(self):
|
||||
doc = frappe.new_doc("Letter Head")
|
||||
doc.letter_head_for = "DocType"
|
||||
doc.letter_head_name = "Test Letter Head Standard"
|
||||
doc.module = "Core"
|
||||
doc.standard = "No"
|
||||
doc.insert()
|
||||
|
||||
doc.standard = "Yes"
|
||||
|
||||
dev_mode_before = frappe.conf.developer_mode
|
||||
frappe.conf.developer_mode = True
|
||||
|
||||
export_path = doc.export_letter_head()
|
||||
|
||||
frappe.conf.developer_mode = dev_mode_before
|
||||
|
||||
final_path = f"{export_path}.json"
|
||||
self.assertTrue(os.path.exists(final_path))
|
||||
|
||||
dir_path = os.path.dirname(os.path.dirname(final_path))
|
||||
self.addCleanup(shutil.rmtree, dir_path)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,14 @@ frappe.ui.get_print_settings = function (
|
|||
depends_on: "with_letter_head",
|
||||
options: "Letter Head",
|
||||
default: letter_head || default_letter_head,
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
letter_head_for: "Report",
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue