Merge pull request #62636 from aerele/fix/cheque-print-format-collision

fix(accounts): prevent cheque print format collisions
This commit is contained in:
mergify[bot]
2026-10-29 10:29:18 +00:00
committed by GitHub
3 changed files with 172 additions and 6 deletions
@@ -10,6 +10,7 @@
"field_order": [
"settings",
"has_print_format",
"print_format_creation",
"primary_settings",
"bank_name",
"cheque_size",
@@ -61,6 +62,17 @@
"hidden": 1,
"label": "Has Print Format",
"no_copy": 1,
"permlevel": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "print_format_creation",
"fieldtype": "Datetime",
"hidden": 1,
"label": "Print Format Creation",
"no_copy": 1,
"permlevel": 1,
"print_hide": 1,
"read_only": 1
},
@@ -298,7 +310,7 @@
"icon": "printer",
"links": [],
"max_attachments": 1,
"modified": "2026-06-08 12:10:35.829531",
"modified": "2026-10-29 12:48:46.566372",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cheque Print Template",
@@ -316,6 +328,12 @@
"share": 1,
"write": 1
},
{
"permlevel": 1,
"read": 1,
"role": "System Manager",
"write": 1
},
{
"create": 1,
"delete": 1,
@@ -6,6 +6,7 @@ import re
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_datetime
class ChequePrintTemplate(Document):
@@ -38,6 +39,7 @@ class ChequePrintTemplate(Document):
message_to_show: DF.Data | None
payer_name_from_left_edge: DF.Float
payer_name_from_top_edge: DF.Float
print_format_creation: DF.Datetime | None
scanned_cheque: DF.Attach | None
signatory_from_left_edge: DF.Float
signatory_from_top_edge: DF.Float
@@ -50,6 +52,7 @@ class ChequePrintTemplate(Document):
@frappe.whitelist(methods=["POST"])
def create_or_update_cheque_print_format(template_name: str):
frappe.only_for("System Manager")
doc = frappe.get_doc("Cheque Print Template", template_name)
if not frappe.db.exists("Print Format", template_name):
cheque_print = frappe.new_doc("Print Format")
@@ -65,8 +68,6 @@ def create_or_update_cheque_print_format(template_name: str):
else:
cheque_print = frappe.get_doc("Print Format", template_name)
doc = frappe.get_doc("Cheque Print Template", template_name)
account_payee = ""
if doc.is_account_payable:
account_payee = """
@@ -79,7 +80,7 @@ def create_or_update_cheque_print_format(template_name: str):
message_to_show=frappe.as_json(doc.message_to_show or _("Account Pay Only"), ensure_ascii=False),
)
cheque_print.html = """
cheque_html = """
<style>
.print-format {{
padding: 0px;
@@ -145,9 +146,32 @@ def create_or_update_cheque_print_format(template_name: str):
signatory_from_left_edge=doc.signatory_from_left_edge,
)
if not cheque_print.is_new():
if doc.print_format_creation:
is_generated_format = get_datetime(doc.print_format_creation) == get_datetime(
cheque_print.creation
)
else:
# Adopt an older format only when updating it would preserve its exact HTML.
is_generated_format = cheque_print.html == cheque_html
if not doc.has_print_format or cheque_print.doc_type != "Payment Entry" or not is_generated_format:
frappe.throw(
_(
"Print Format {0} already exists and could not be identified as generated by this template. "
"Rename the existing Print Format before continuing."
).format(frappe.bold(template_name)),
title=_("Print Format Conflict"),
)
cheque_print.html = cheque_html
cheque_print.save(ignore_permissions=True)
frappe.db.set_value("Cheque Print Template", template_name, "has_print_format", 1)
frappe.db.set_value(
"Cheque Print Template",
template_name,
{"has_print_format": 1, "print_format_creation": cheque_print.creation},
)
return cheque_print
@@ -1,9 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from unittest.mock import patch
import frappe
from frappe.desk.form.load import get_meta_bundle
from frappe.translate import print_language
from frappe.utils import fmt_money, money_in_words, nowdate
from frappe.utils import fmt_money, get_datetime, money_in_words, nowdate
from frappe.www.printview import get_rendered_template
from erpnext.accounts.doctype.cheque_print_template.cheque_print_template import (
@@ -65,3 +68,124 @@ class TestChequePrintTemplate(ERPNextTestSuite):
self.assertIn(money_in_words(100.25, "USD"), rendered)
self.assertIn(fmt_money(100.25, currency="USD"), rendered)
self.assertEqual(payment.reload().in_words, money_in_words(100.25, "USD"))
def test_generated_print_format_ownership(self):
template = frappe.get_doc(
doctype="Cheque Print Template", bank_name="_Test Cheque Format Ownership"
).insert()
for operation in ("rename", "delete"):
with self.subTest(operation=operation):
generated = create_or_update_cheque_print_format(template.name)
template.reload()
if operation == "rename":
frappe.rename_doc("Print Format", generated.name, generated.name + " Archived")
else:
frappe.delete_doc("Print Format", generated.name)
self.assertEqual(template.has_print_format, 1)
replacement = frappe.get_doc(
doctype="Print Format",
name=template.name,
doc_type="Payment Entry",
standard="No",
custom_format=1,
print_format_type="Jinja",
html="<h1>Payment remittance advice</h1>",
).insert()
with self.assertRaises(frappe.ValidationError):
create_or_update_cheque_print_format(template.name)
self.assertEqual(replacement.reload().html, "<h1>Payment remittance advice</h1>")
# Reusing the generated HTML must not transfer ownership to the replacement record.
replacement.html = generated.html
replacement.save()
with self.assertRaises(frappe.ValidationError):
create_or_update_cheque_print_format(template.name)
frappe.delete_doc("Print Format", replacement.name)
generated = create_or_update_cheque_print_format(template.name)
template.reload()
template.payer_name_from_left_edge = 5
template.save()
updated = create_or_update_cheque_print_format(template.name)
self.assertEqual(generated.name, updated.name)
self.assertNotEqual(generated.html, updated.html)
self.assertIn("left: 5.0cm", updated.html)
# Existing generated formats can acquire an identity without changing their HTML.
template.db_set("print_format_creation", None)
self.assertEqual(create_or_update_cheque_print_format(template.name).html, updated.html)
self.assertEqual(get_datetime(template.reload().print_format_creation), updated.creation)
# An untracked customized format must not be adopted based on the stale checkbox.
template.db_set("print_format_creation", None)
updated.reload()
updated.html = "<h1>Customized payment layout</h1>"
updated.save()
with self.assertRaises(frappe.ValidationError):
create_or_update_cheque_print_format(template.name)
self.assertEqual(updated.reload().html, "<h1>Customized payment layout</h1>")
with self.set_user("Guest"), patch.dict(frappe.flags, {"in_test": False}):
with self.assertRaises(frappe.PermissionError):
create_or_update_cheque_print_format(template.name)
self.assertEqual(updated.reload().html, "<h1>Customized payment layout</h1>")
def test_print_format_identity_permissions(self):
manager = frappe.get_doc(
doctype="User",
email="cheque-accounts-manager@example.com",
first_name="Cheque Accounts Manager",
enabled=1,
send_welcome_email=0,
roles=[{"role": "Accounts Manager"}],
).insert()
print_format = frappe.get_doc(
doctype="Print Format",
name="_Test Cheque Format Permissions",
doc_type="Payment Entry",
standard="No",
custom_format=1,
print_format_type="Jinja",
html="<h1>Payment remittance advice</h1>",
).insert()
with self.set_user(manager.name):
self.assertFalse(frappe.has_permission("Print Format", "write", doc=print_format))
creation = next(
row["creation"]
for row in get_meta_bundle("Payment Entry")[0]["__print_formats"]
if row["name"] == print_format.name
)
template = frappe.get_doc(
doctype="Cheque Print Template",
bank_name=print_format.name,
has_print_format=1,
print_format_creation=creation,
).insert()
self.assertFalse(template.reload().has_print_format)
self.assertIsNone(template.print_format_creation)
template.has_print_format = 1
template.print_format_creation = creation
template.save()
self.assertFalse(template.reload().has_print_format)
self.assertIsNone(template.print_format_creation)
with self.set_user("test@example.com"):
with self.assertRaises(frappe.ValidationError):
create_or_update_cheque_print_format(template.name)
self.assertEqual(print_format.reload().html, "<h1>Payment remittance advice</h1>")
frappe.delete_doc("Print Format", print_format.name)
with self.set_user("test@example.com"):
generated = create_or_update_cheque_print_format(template.name)
with self.set_user(manager.name):
template.reload()
template.payer_name_from_left_edge = 5
template.has_print_format = 0
template.print_format_creation = None
template.save()
self.assertTrue(template.reload().has_print_format)
self.assertEqual(get_datetime(template.print_format_creation), get_datetime(generated.creation))
with self.set_user("test@example.com"):
self.assertIn("left: 5.0cm", create_or_update_cheque_print_format(template.name).html)