Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e24ab72
fix: copy project from first row to new rows (backport #53295) (#54620)
mergify[bot] Apr 29, 2026
9db03bc
fix(selling): blanket order ordered qty recalculation on sales order …
mergify[bot] Apr 29, 2026
d6f2ff6
fix: show correct status in Serial No Ledger (backport #54567) (#54626)
mergify[bot] Apr 29, 2026
808214f
perf: max recursion depth error in serial no (backport #54629) (#54631)
mergify[bot] Apr 29, 2026
48ebb4c
feat(ux): Naming series dialog (#54554)
nishkagosalia Apr 29, 2026
2e43801
Merge pull request #54635 from frappe/mergify/bp/version-16-hotfix/pr…
nishkagosalia Apr 29, 2026
7bd360a
fix: py error on sales forecast doctype (backport #54641) (#54643)
mergify[bot] Apr 29, 2026
6dbc17d
fix: dont show serial/batch button when PR is submitted (backport #54…
mergify[bot] Apr 29, 2026
19a8ebe
fix(payment_entry): convert the date args to string type before escap…
mergify[bot] Apr 29, 2026
86cf256
fix: correct project filter in buying doctypes (backport #54644) (#54…
mergify[bot] Apr 29, 2026
b300159
fix: use RecoverableErrors isinstance check for repost timeout status…
mergify[bot] Apr 29, 2026
d3c893d
fix: skip depreciation rescheduling when asset is fully depreciated o…
khushi8112 Apr 29, 2026
07a957c
fix: skip rescheduling only for asset being disposed
khushi8112 Apr 29, 2026
bd932da
feat: copy terms attachments to transactions (backport #53403) (#54661)
mergify[bot] Apr 29, 2026
c232f1f
Merge pull request #54659 from frappe/mergify/bp/version-16-hotfix/pr…
khushi8112 Apr 30, 2026
d27cf48
fix: show in and out qty in the stock ledger report for stock recos
rohitwaghchaure Apr 30, 2026
38cfeb1
fix: correct titles set to {customer_name} or {supplier_name} text st…
mergify[bot] Apr 30, 2026
2422237
Merge pull request #54671 from frappe/mergify/bp/version-16-hotfix/pr…
rohitwaghchaure Apr 30, 2026
288cdf3
fix(project): use user.email for invitations and skip disabled users.…
mergify[bot] Apr 30, 2026
126e13b
fix: mark item tax templates as not applicable (backport #54673) (#54…
mergify[bot] Apr 30, 2026
a22d773
fix: Backfill `not_applicable` on Item Tax Template Details for Germa…
mergify[bot] May 1, 2026
0dade2c
fix: incorrect expense account book in purchase return (backport #546…
mergify[bot] May 1, 2026
bca893a
fix: add missing fields in set_currency_labels (backport #54689) (#54…
mergify[bot] May 1, 2026
bbb4e79
fix: set valid_from in created Item Price (backport #54696) (#54700)
mergify[bot] May 2, 2026
18006b9
chore: update POT file (#54710)
frappe-pr-bot May 3, 2026
982810a
fix: accounts and account types in German CoA "SKR 03" (backport #547…
mergify[bot] May 3, 2026
2cd4c1a
fix: error when creating quotation from CRM (backport #54722) (#54725)
mergify[bot] May 4, 2026
e60490d
fix: hide payment and payment request buttons based on permissions in…
mergify[bot] May 5, 2026
0f27881
fix: Remove bom stock report link from manufacturing workspace
nishkagosalia May 5, 2026
c985f94
Merge pull request #54743 from frappe/mergify/bp/version-16-hotfix/pr…
nishkagosalia May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: copy terms attachments to transactions (backport #53403) (#54661)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
  • Loading branch information
mergify[bot] and barredterra authored Apr 29, 2026
commit bd932da08b6f3d9d6030245b833478570a65aac4
91 changes: 91 additions & 0 deletions erpnext/selling/doctype/quotation/test_quotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,61 @@ def test_make_quotation_without_terms(self):

self.assertTrue(quotation.payment_schedule)

def test_terms_attachments_are_copied_to_quotation(self):
terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
first_attachment = make_file_attachment(
"Terms and Conditions",
terms.name,
content="First terms attachment",
)

quotation = make_quotation(do_not_save=1)
quotation.tc_name = terms.name
quotation.insert()

self.assertEqual(get_attachment_urls("Quotation", quotation.name), {first_attachment.file_url})

second_attachment = make_file_attachment(
"Terms and Conditions",
terms.name,
content="Second terms attachment",
)
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
quotation.save()

quotation_attachments = get_attachment_urls("Quotation", quotation.name)
self.assertEqual(quotation_attachments, {first_attachment.file_url})
self.assertNotIn(second_attachment.file_url, quotation_attachments)

new_terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
new_terms_attachment = make_file_attachment(
"Terms and Conditions",
new_terms.name,
content="Attachment from updated terms",
)
quotation.tc_name = new_terms.name
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
quotation.save()

self.assertEqual(
get_attachment_urls("Quotation", quotation.name),
{first_attachment.file_url, new_terms_attachment.file_url},
)

def test_terms_attachments_are_not_copied_when_disabled(self):
terms = make_terms_and_conditions(copy_attachments_to_transaction=False)
make_file_attachment(
"Terms and Conditions",
terms.name,
content="Terms attachment should stay on the template",
)

quotation = make_quotation(do_not_save=1)
quotation.tc_name = terms.name
quotation.insert()

self.assertFalse(get_attachment_urls("Quotation", quotation.name))

@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"automatically_fetch_payment_terms": 1},
Expand Down Expand Up @@ -1148,6 +1203,42 @@ def get_quotation_dict(party_name=None, item_code=None):
}


def make_terms_and_conditions(copy_attachments_to_transaction=False):
return frappe.get_doc(
{
"doctype": "Terms and Conditions",
"title": f"_Test Terms and Conditions {frappe.generate_hash(length=8)}",
"selling": 1,
"terms": "Test terms",
"copy_attachments_to_transaction": 1 if copy_attachments_to_transaction else 0,
}
).insert()


def make_file_attachment(doctype, docname, content):
return frappe.get_doc(
{
"doctype": "File",
"file_name": f"terms-attachment-{frappe.generate_hash(length=8)}.txt",
"attached_to_doctype": doctype,
"attached_to_name": docname,
"content": content,
}
).insert()


def get_attachment_urls(doctype, docname):
return {
file.file_url
for file in frappe.get_all(
"File",
filters={"attached_to_doctype": doctype, "attached_to_name": docname},
fields=["file_url"],
)
if file.file_url
}


def make_quotation(**args):
qo = frappe.new_doc("Quotation")
args = frappe._dict(args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"field_order": [
"title",
"disabled",
"column_break_ofhb",
"copy_attachments_to_transaction",
"applicable_modules_section",
"selling",
"buying",
Expand Down Expand Up @@ -72,12 +74,22 @@
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ofhb",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "copy_attachments_to_transaction",
"fieldtype": "Check",
"label": "Copy Attachments to Transaction"
}
],
"icon": "icon-legal",
"idx": 1,
"links": [],
"modified": "2026-04-14 18:22:49.285298",
"modified": "2026-04-29 22:51:49.285298",
"modified_by": "Administrator",
"module": "Setup",
"name": "Terms and Conditions",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class TermsandConditions(Document):
from frappe.types import DF

buying: DF.Check
copy_attachments_to_transaction: DF.Check
disabled: DF.Check
selling: DF.Check
terms: DF.TextEditor | None
Expand Down
58 changes: 58 additions & 0 deletions erpnext/utilities/transaction_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ class UOMMustBeIntegerError(frappe.ValidationError):


class TransactionBase(StatusUpdater):
def on_change(self):
# `on_change` also fires for `db_set()`, so only run during an actual insert/save.
is_real_save = self.flags.in_insert or (self.doctype, self.name) in frappe.flags.currently_saving
if not is_real_save:
return

self.copy_terms_and_conditions_attachments()

def validate_posting_time(self):
# set Edit Posting Date and Time to 1 while data import and restore
if (frappe.flags.in_import or self.flags.from_restore) and self.posting_date:
Expand All @@ -36,6 +44,56 @@ def validate_posting_time(self):
def validate_uom_is_integer(self, uom_field, qty_fields, child_dt=None):
validate_uom_is_integer(self, uom_field, qty_fields, child_dt)

def copy_terms_and_conditions_attachments(self):
if (
not self.name
or not self.meta.has_field("tc_name")
or not self.tc_name
or not self.has_value_changed("tc_name")
):
return

copy_attachments_to_transaction = frappe.db.get_value(
"Terms and Conditions", self.tc_name, "copy_attachments_to_transaction"
)
if not cint(copy_attachments_to_transaction):
return

source_attachments = frappe.get_all(
"File",
filters={
"attached_to_doctype": "Terms and Conditions",
"attached_to_name": self.tc_name,
},
fields=["name", "file_url"],
)
if not source_attachments:
return

existing_file_urls = {
attachment.file_url
for attachment in frappe.get_all(
"File",
filters={
"attached_to_doctype": self.doctype,
"attached_to_name": self.name,
},
fields=["file_url"],
)
if attachment.file_url
}

for source_attachment in source_attachments:
if not source_attachment.file_url or source_attachment.file_url in existing_file_urls:
continue

# Reuse the existing file metadata so the same on-disk blob is shared.
new_attachment = frappe.get_doc("File", source_attachment.name).create_attachment_copy(
attached_to_doctype=self.doctype,
attached_to_name=self.name,
)
existing_file_urls.add(new_attachment.file_url)

def validate_with_previous_doc(self, ref):
self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else []

Expand Down