Skip to content

Commit 3565746

Browse files
feat: make the Attach To Field a (optional) setting (#66)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
1 parent bea3e70 commit 3565746

File tree

5 files changed

+136
-4
lines changed

5 files changed

+136
-4
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,19 @@ To change the settings of this app, you can open **PDF on Submit Settings** via
2525

2626
In the _Enabled For_ table, add a row for each DocType you want to enable PDF generation for.
2727

28-
If you don't choose a different configuration, PDFs will be generated with the default **Print Format** and **Letter Head**. The PDF is named like the document name.
28+
If you don't choose a different configuration, PDFs will be generated with the default **Print Format** and **Letter Head**. By default, the PDF is named like the document name.
2929

3030
Alternatively, you can choose a different **Print Format** and **Letter Head** for each DocType. You can also define a custom format for the PDF file name.
3131

32+
If your transaction DocType has an attachment field, you can choose to attach the generated PDF to that field. Please note that the field needs to have the following properties:
33+
34+
- Fieldtype: Attach
35+
- Read Only: Yes
36+
This is to prevent users from adding an attachment that will be overwritten on submit.
37+
- No Copy: Yes
38+
This is to prevent the attachment from being copied to other documents.
39+
- Is Virtual: No
40+
3241
![PDF on Submit Settings](docs/settings.gif)
3342

3443
### Filters and different PDFs for the same DocType

pdf_on_submit/attach_pdf.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def process_enabled_doctype(doc, settings, in_background):
4343
args = {
4444
"doctype": doc.doctype,
4545
"name": doc.name,
46+
"to_field": settings.attach_to_field,
4647
"title": doc.get_title() if doc.meta.title_field else None,
4748
"lang": getattr(doc, "language", fallback_language),
4849
"show_progress": not in_background,
@@ -67,6 +68,7 @@ def process_enabled_doctype(doc, settings, in_background):
6768
def execute(
6869
doctype,
6970
name,
71+
to_field=None,
7072
title=None,
7173
lang=None,
7274
show_progress=True,
@@ -120,7 +122,7 @@ def publish_progress(percent):
120122
if show_progress:
121123
publish_progress(66)
122124

123-
save_and_attach(pdf_data, doctype, name, target_folder, auto_name)
125+
save_and_attach(pdf_data, doctype, name, target_folder, auto_name, to_field)
124126

125127
if show_progress:
126128
publish_progress(100)
@@ -143,7 +145,7 @@ def get_pdf_data(doctype, name, print_format: None, letterhead: None):
143145
return frappe.utils.pdf.get_pdf(html)
144146

145147

146-
def save_and_attach(content, to_doctype, to_name, folder, auto_name=None):
148+
def save_and_attach(content, to_doctype, to_name, folder, auto_name=None, to_field=None):
147149
"""
148150
Save content to disk and create a File document.
149151
@@ -164,8 +166,12 @@ def save_and_attach(content, to_doctype, to_name, folder, auto_name=None):
164166
file.is_private = 1
165167
file.attached_to_doctype = to_doctype
166168
file.attached_to_name = to_name
169+
file.attached_to_field = to_field
167170
file.save()
168171

172+
if to_field:
173+
frappe.db.set_value(to_doctype, to_name, to_field, file.file_url)
174+
169175

170176
def set_name_from_naming_options(autoname, doc):
171177
"""

pdf_on_submit/pdf_on_submit/doctype/enabled_doctype/enabled_doctype.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"engine": "InnoDB",
77
"field_order": [
88
"document_type",
9+
"attach_to_field",
910
"print_format",
1011
"letter_head",
1112
"auto_name",
@@ -65,17 +66,25 @@
6566
"fieldname": "filter_description",
6667
"fieldtype": "HTML",
6768
"options": "Optional: create PDF only if the submitted document matches these filters.<br><br>"
69+
},
70+
{
71+
"description": "If set, the PDF will be stored in this specific Attach field. If not set, it will be attached to the document without a specific field.<br>\n<b>Note:</b> The Attach fields should be 'No Copy' and 'Read Only'. Otherwise they get overwritten on submit.",
72+
"fieldname": "attach_to_field",
73+
"fieldtype": "Select",
74+
"label": "Attach To Field"
6875
}
6976
],
77+
"grid_page_length": 50,
7078
"istable": 1,
7179
"links": [],
72-
"modified": "2025-02-08 01:03:04.756526",
80+
"modified": "2025-06-29 13:32:14.451432",
7381
"modified_by": "Administrator",
7482
"module": "PDF on Submit",
7583
"name": "Enabled DocType",
7684
"owner": "Administrator",
7785
"permissions": [],
7886
"quick_entry": 1,
87+
"row_format": "Dynamic",
7988
"sort_field": "modified",
8089
"sort_order": "DESC",
8190
"states": [],

pdf_on_submit/pdf_on_submit/doctype/pdf_on_submit_settings/pdf_on_submit_settings.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@ frappe.ui.form.on("PDF on Submit Settings", {
1818
},
1919
};
2020
});
21+
22+
frm.doc.enabled_for.forEach((row) => {
23+
set_attach_to_field_options(frm, row.doctype, row.name);
24+
});
2125
},
2226
enabled_for_on_form_rendered(frm) {
2327
const row = frm.cur_grid.doc;
2428
const parent = frm.cur_grid.wrapper.find("[data-fieldname='filter_area']");
2529
parent.empty();
2630

31+
if (!row.document_type) {
32+
return;
33+
}
34+
2735
const filters = row.filters && row.filters !== "[]" ? JSON.parse(row.filters) : [];
2836

2937
frappe.model.with_doctype(row.document_type, () => {
@@ -49,6 +57,7 @@ frappe.ui.form.on("Enabled DocType", {
4957
document_type(frm, cdt, cdn) {
5058
const row = locals[cdt][cdn];
5159
frappe.model.set_value(row.doctype, row.name, "filters", "[]");
60+
frappe.model.set_value(row.doctype, row.name, "attach_to_field", "");
5261

5362
if (row.print_format) {
5463
// Check if the print format is valid for the document type
@@ -63,5 +72,67 @@ frappe.ui.form.on("Enabled DocType", {
6372
if (frm.cur_grid) {
6473
frm.events.enabled_for_on_form_rendered(frm);
6574
}
75+
76+
set_attach_to_field_options(frm, cdt, cdn);
6677
},
6778
});
79+
80+
81+
function set_attach_to_field_options(frm, cdt, cdn) {
82+
const doc = frappe.get_doc(cdt, cdn);
83+
const document_type = doc.document_type;
84+
const grid = frm.fields_dict.enabled_for.grid;
85+
86+
if (!document_type) {
87+
set_field_options(grid, cdn, "attach_to_field", [""]);
88+
return;
89+
}
90+
91+
// set options for `attach_to_field`
92+
frappe.model.with_doctype(document_type, () => {
93+
const meta = frappe.get_meta(document_type);
94+
const fields = [
95+
"",
96+
...meta.fields
97+
.filter(
98+
(field) =>
99+
field.fieldtype === "Attach" &&
100+
field.is_virtual === 0 &&
101+
field.read_only === 1 &&
102+
field.no_copy === 1
103+
)
104+
.map((field) => {
105+
return {
106+
value: field.fieldname,
107+
label: __(field.label),
108+
};
109+
})
110+
.sort((a, b) => a.label.localeCompare(b.label)),
111+
];
112+
113+
set_field_options(grid, cdn, "attach_to_field", fields);
114+
grid.debounced_refresh();
115+
});
116+
}
117+
118+
/**
119+
* Set the options for a field in a specific grid row
120+
* @param {frappe.ui.form.Grid} grid - frm.fields_dict.[child_table_name].grid
121+
* @param {string} row_name - The name of the grid row
122+
* @param {string} fieldname - The fieldname to set the options for
123+
* @param {any[]} options - The options to set for the field
124+
*/
125+
function set_field_options(grid, row_name, fieldname, options) {
126+
for (const row of grid.grid_rows) {
127+
if (row.doc.name !== row_name) {
128+
continue;
129+
}
130+
131+
let docfield = row?.docfields?.find((d) => d.fieldname === fieldname);
132+
if (docfield) {
133+
docfield.options = options;
134+
} else {
135+
throw `field ${fieldname} not found`;
136+
}
137+
}
138+
}

pdf_on_submit/pdf_on_submit/doctype/pdf_on_submit_settings/pdf_on_submit_settings.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,40 @@ def validate(self):
1717
enabled_doctype.idx, enabled_doctype.document_type, e
1818
)
1919
)
20+
21+
self.validate_attach_to_fields()
22+
23+
def validate_attach_to_fields(self):
24+
"""
25+
Validates:
26+
1) attach_to_field is not processed multiple times in one document
27+
2) that the `attach_to_field` is a valid field in the DocType.
28+
Note: The 2nd validation would be more robust (but less performant), if done for each transaction.
29+
"""
30+
attach_to_fields = [
31+
(enabled_doctype.document_type, enabled_doctype.attach_to_field) for enabled_doctype in self.enabled_for
32+
if enabled_doctype.attach_to_field
33+
]
34+
if attach_to_fields:
35+
_check_for_duplicate_fieldnames(attach_to_fields)
36+
_check_if_attach_to_fields_are_valid(attach_to_fields)
37+
38+
39+
def _check_for_duplicate_fieldnames(attach_to_fields):
40+
"""Ensure only one PDF file will be attached to a specific field."""
41+
seen_fields = set()
42+
for pair in attach_to_fields:
43+
if pair in seen_fields:
44+
frappe.throw(
45+
_("It is not allowed to set the attach field {0} in the DocType {1} multiple times.").format(pair[1], pair[0])
46+
)
47+
seen_fields.add(pair)
48+
49+
50+
def _check_if_attach_to_fields_are_valid(attach_to_fields):
51+
for doctype, fieldname in attach_to_fields:
52+
meta = frappe.get_meta(doctype)
53+
if not meta.get("fields", {"fieldtype": "Attach", "fieldname": fieldname}):
54+
frappe.throw(
55+
_("{0} is not a valid field for DocType {1}.").format(fieldname, _(doctype))
56+
)

0 commit comments

Comments
 (0)