Header / Footer Development

@rmehta Sure but to actually present some starting code in a pull request I’d like to know how I can add a field to the print format doctype and how to then access it from the DB.

Also, this code part belongs to frappe/frappe but where is the print format doctype defined? I didn’t see any way to alter the print format doctype from inside ERPNext so my guess is I have to alter the original doctype definition for it.

Could someone give some hints for those two things?

I can’t remember if developer mode needs to be enabled to modify doctypes but you can enable it by adding “developer_mode”: 1 in your site’s site_config.json. As for adding custom fields to the print format doctype, you have to go to setup/customize/doctype and edit it there.

I ended up going with something a bit different than before. Since the letterhead doctype is used by most doctypes, I added an extra field for a footer html there. It helps when dealing with multiple companies that have different default letterheads.

To have custom headers based on doctypes (invoices in your case) it does make sense to use custom print formats. Accessing something from the db would go something like this, but I’m assuming this won’t handle any references to the doc object. I haven’t looked much into print formats yet. Hope this helps a bit.

header_html = frappe.db.get_value("Print Format", format, "name_of_header_field") or ""
1 Like

@Kar_M

Thanks for the help. On the bright it seems enabling developer_mode allowed me to edit the doctypes however I noticed that the header-html option won’t work for my needs because it needs to access the html code via a URL so I can’t just access the db and pass the value as a variable.

Yeah after you get the html data from the db, you’ll have to create a file with that data. This is a simple way to test with.

fname = "header.html"   
f = open(fname,'w')
letterheadhtml = """<!DOCTYPE html><head></head><body style="margin:0; padding:0;">""" + template + """</body></html>"""
f.write(letterheadhtml)
f.close()

Indeed that’s very helpful, thank you very much. It also gave me the idea to manipulate the DOM instead of bothering with extra fields in the doctypes. That way I can actually set a div in the html like

and then grab it’s contents using BeautifulSoup to add to that file.

I’m working on it a lot to actually make a generic solution on it. Just a few things I need to solve first like adding all the styles the original document has to maintain formatting. Like bootstrap etc.

Do you happen to know how I can store this file to the public assets folder instead?

I believe the file gets created in the sites folder using that code. Frappe does have some methods to get paths for different things like the site base path and the public files folder path. I haven’t tested it out much but you can see all the methods here. I assume get_files_path would do the job. So far I’ve only used get_request_site_address like so. Had to use it to embed the letterhead img I was testing with. Could only get it to work with absolute paths for some reason (might help when you get to that part).

from frappe.utils import get_request_site_address
path = frappe.utils.get_request_site_address()

Awesome thank you very much :slight_smile:

I’m glad to say that with your help @Kar_M I managed to implement the custom header/footer workaround as a generic solution for all my print formats. I decided not to use the database to handle this issue but instead focus on some HTML DOM manipulation to achieve my needs instead by modifying the frappe/utils/pdf.py file

Now all I have to do in my templates is define a section for htmlheader like this:
<div id="htmlheader"> < !--content here --> </div>
<div id="htmlfooter"> < !--content here --> </div>
as well as

<div id="variables" style="display:none;">
    <span id="margintop">120mm</span>
</div>

to adjust the header size.
Then the content is read dynamically from the HTML provided to the get_pdf function. Also I’ve integrated the javascript code from wkhtmltopdf to dynamically substitute variables to header/footer html.

For example

<div id="htmlfooter" class="has-variables">
    <div class="col-xs-12 pull-right" style="text-align:right">
      {{ ("Page") }} <span class="page"></span> {{ _("of") }} <span class="topage"></span>
    </div>
</div>

willl provide us with page number at the footer.

Maybe someone else will find this helpful so I’ll post my whole pdf.py file

My solution though requires to install BeautifulSoup4 with frappe-bench/env/bin/pip install beautifulsoup4

I don’t know if people are interested to integrate this in ERPNext in the future somehow but it will probably require someone way more experienced than me in Python and ERPNext to make it a real bug-free solution although I made sure that standard print formats won’t be affected by this.

I also need to find a way to have a celery task to delete any remaining temporary files created on a daily basis from the site’s public files folder

1 Like

Glad I could help. Being able to adjust header size on the fly is pretty useful. So far I’ve kept all my headers a similar size, but I’m sure I’ll need to add a feature like that at some point. Besides migrating data, figuring out multi document pdfs is keeping me busy. I hope to get back to header and footers eventually.

I don’t have much experience with celery. I believe the tasks are confgured here. I did notice that the temporary pdf file is always deleted (link). I’m not sure if you’d like to go that route for the header. Constantly creating and then deleting a header file for a document that gets printed a lot would probably be inefficient.

At first I thought I could work with one file and leave it there but then I opted against it because you might have two users simultaneously handling two invoices that use the same function for pdf. So I thought I should make a temp file with a 16 character hash code on it that is uniquely named then delete upon completion of the task. This works however if someone clicks reload on the PDF creation preview page like 3-4 times it will create extra files that won’t get deleted. That’s why I wanted a garbage collector function to clear them up using celery at regular intervals. Like a function that will every hour clear files in the public folder that are modified +1 hr. At the moment I use a cron job with

find <path_to_folder> -mmin +59 -exec rm {} \;

Also in my case scenario the invoice header is always changing due to the customer info / shipping info changing so I can’t actually calculate the header size correctly that’s why I wanted it on the fly :smiley:

@Kar_M @gabtzi how about contributing some of it back in Frappé? A lot of users can use it.

I can start a pull request with my solution if you want however it would break Frappe if BeautifulSoup4 is not added to the virtual environment and I still haven’t found a way to solve my problem with the scheduled celery task for garbage file collection.

You just have to add it to requirements.txt and it should work.

We can help you with that! Just start the PR :smile:

OK done. Maybe @Kar_M can also provide his solution too since it might be more robust

Definitely wouldn’t call my solution robust. Fixing some of it up to be a bit more useful to others. Will hopefully do a pull request soon.

I want to share my workaround maybe it will be helpful for someone. I’ve changed function download_pdf in print.py. Solution is not generic with some hardcoded values but it solved my problem. I’m using db to store paths for header and footer and disabling frappe header by (frappe.form_dict.no_letterhead = 1).

@frappe.whitelist()
def download_pdf(doctype, name, format=None):
	options = {}
	base_path = frappe.get_app_path('erpnext')
	header_path = frappe.db.get_global('header-html-path')
	if header_path:
		options['header-html'] = os.path.join(base_path, header_path)
		options['header-spacing'] = '52'
		frappe.form_dict.no_letterhead = 1
	footer_path = frappe.db.get_global('footer-html-path')
	if footer_path:
		options['footer-html'] = os.path.join(base_path, footer_path)
		options['footer-spacing'] = '2'

	html = frappe.get_print(doctype, name, format)
	frappe.local.response.filename = "{name}.pdf".format(name=name.replace(" ", "-").replace("/", "-"))
	frappe.local.response.filecontent = get_pdf(html, options)
	frappe.local.response.type = "download"

Also please mentioned that header could overflows content. To avoid that issue I’ve changed ‘margin-top’: ‘62mm’ in get_pdf function of pdf.py. So you need to play a bit with header-spacing and margin-top, values depends on your header size.

Hi everyone,

@gabtzi great job and thanks for the contributing the header / footer code for PDF generation. Here’s the pull request: Enchancement - provide header/footer HTML code and adjust margins on the fly in custom print formats by gabtzi · Pull Request #1528 · frappe/frappe · GitHub

Since this was considered for paid development, @gabtzi should be rewarded :smile:
@gabtzi can you share your paypal ID so that those who benefited can contribute?


On another note, we have covered up the garbage collection part by storing all files in /tmp folder instead of sitename/files, and cleaning up in the finally block.

Also, we have:

  1. Added Footer in Letter Head doctype
  2. Modified the standard print format to show repeating header and footer, and page nos. in footer for PDF. Here’s an example pdf: https://dl.dropboxusercontent.com/u/29814148/erpnext/PO-00006.pdf

This has been pushed into develop branch and will be released in master next week.

Cheers!

4 Likes

Hello @anand,

Can this be use in script report?

Yes. Will publish the docs soon.

1 Like

That was unexpected and really generous of you anand :slight_smile: Of course I have no objections to anyone who might want to donate

but all’s well even if they don’t. I’m happy I was able to help other users as I was tackling the issue for my needs with the help of @Kar_M . My paypal id is “gabtzi@gmail.com” in any case

About this do you want me to send you the markdown code I used for what I wrote as explanation in github so that you can edit it?

2 Likes